Die Einstellungen bekommen Reiter: Profil, Team, Fahrer (nur Teamleitung) und Flag-Typen. /dashboard/teams wandert dorthin. Neu ist die Hauptroute Fahrer - das Verzeichnis des Teams, fuer alle sichtbar und fuer niemanden dort aenderbar. Angelegt, geaendert und mit einem Konto verknuepft wird in den Einstellungen; serverseitig setzen das die neuen Regeln der Collection durch. Ein Fahrer bekommt sein Login entweder aus den vorhandenen Teammitgliedern oder direkt neu angelegt. Die Einladung per E-Mail-Suche entfaellt: Sie konnte nie funktionieren, weil users.listRule nur den eigenen Datensatz freigibt - daher auch die abgeschnittenen IDs in der alten Mitgliederliste. Events zeigen statt eines Status ihr Datum, mit Checkbox fuer mehrtaegig. Gespeichert wird ein Enddatum nur, wenn es sich vom Beginn unterscheidet - es ist damit zugleich das Kennzeichen fuer mehrtaegig statt einer zweiten Wahrheit ueber dieselbe Sache. Listen gruppieren nach laufend, anstehend, ohne Termin und vorbei. Im Kopf teilen sich Name, Design-Umschalter und Logout eine Zeile, das Team-Dropdown sitzt darunter; das Logo passt jetzt in die Leiste, statt beschnitten zu wirken. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
181 lines
5.8 KiB
TypeScript
181 lines
5.8 KiB
TypeScript
import { getContext, setContext } from 'svelte'
|
|
import { api } from './pocketbase.svelte'
|
|
import { app } from './app.svelte'
|
|
import { getTeamContext } from './teams.svelte'
|
|
import type { RidersResponse, UsersResponse } from '$lib/types'
|
|
|
|
const KEY = Symbol('rider')
|
|
|
|
function sortByNumber(records: RidersResponse[]) {
|
|
return [...records].sort((a, b) => {
|
|
const na = parseInt(a.number ?? '0', 10)
|
|
const nb = parseInt(b.number ?? '0', 10)
|
|
return na - nb
|
|
})
|
|
}
|
|
|
|
export function fullName(r: RidersResponse) {
|
|
const parts = [r.firstname, r.lastname].filter(Boolean)
|
|
return parts.join(' ').trim() || r.name || ''
|
|
}
|
|
|
|
export class RiderStore {
|
|
records = $state<RidersResponse[]>([])
|
|
loading = $state(false)
|
|
error = $state<string | null>(null)
|
|
searchQuery = $state('')
|
|
|
|
private unsubscribe: (() => void) | null = null
|
|
private teams = getTeamContext()
|
|
|
|
get scoped(): RidersResponse[] {
|
|
const teamId = this.teams.activeId
|
|
if (!teamId) return []
|
|
return this.records.filter((r) => r.team === teamId)
|
|
}
|
|
|
|
/**
|
|
* Anlegen, ändern und löschen darf nur die Teamleitung — dieselbe Regel
|
|
* wie in createRule/updateRule/deleteRule der Collection. Die Prüfung
|
|
* hier blendet nur UI aus; durchgesetzt wird sie serverseitig.
|
|
*/
|
|
get canManage(): boolean {
|
|
return this.teams.isAdmin()
|
|
}
|
|
|
|
async load() {
|
|
this.loading = true
|
|
this.error = null
|
|
try {
|
|
const raw = await api.collection('riders').getFullList({ requestKey: null })
|
|
this.records = sortByNumber(raw)
|
|
} catch (e: any) {
|
|
this.error = e.message ?? 'Fehler beim Laden der Fahrer'
|
|
console.error(e)
|
|
} finally {
|
|
this.loading = false
|
|
}
|
|
}
|
|
|
|
subscribe() {
|
|
if (this.unsubscribe) return
|
|
api.collection('riders').subscribe('*', (e) => {
|
|
const idx = this.records.findIndex((r) => r.id === e.record.id)
|
|
if (e.action === 'create' && idx === -1) {
|
|
this.records = sortByNumber([...this.records, e.record])
|
|
} else if (e.action === 'update' && idx !== -1) {
|
|
this.records[idx] = e.record
|
|
this.records = sortByNumber(this.records)
|
|
} else if (e.action === 'delete' && idx !== -1) {
|
|
this.records = this.records.filter((r) => r.id !== e.record.id)
|
|
}
|
|
}).then((unsub) => {
|
|
this.unsubscribe = unsub
|
|
}).catch((e) => console.warn('riders subscribe failed:', e))
|
|
}
|
|
|
|
async create(data: Partial<RidersResponse>) {
|
|
const teamId = this.teams.activeId
|
|
if (!teamId) throw new Error('Kein aktives Team')
|
|
return await api.collection('riders').create({ ...data, team: teamId })
|
|
}
|
|
|
|
async edit(id: string, data: Partial<RidersResponse>) {
|
|
return await api.collection('riders').update(id, data)
|
|
}
|
|
|
|
remove(record: RidersResponse) {
|
|
return new Promise<void>((resolve, reject) => {
|
|
app.confirm.request({
|
|
title: 'Fahrer löschen?',
|
|
text: `Möchten Sie „#${record.number} ${fullName(record)}" wirklich löschen? Alle zugehörigen Zeiten werden ebenfalls gelöscht.`,
|
|
yes: async () => {
|
|
try {
|
|
await api.collection('riders').delete(record.id)
|
|
app.confirm.close()
|
|
resolve()
|
|
} catch (e) {
|
|
app.confirm.close()
|
|
reject(e)
|
|
}
|
|
},
|
|
no: () => {
|
|
app.confirm.close()
|
|
resolve()
|
|
},
|
|
})
|
|
})
|
|
}
|
|
|
|
getById(id: string) {
|
|
return this.records.find((r) => r.id === id)
|
|
}
|
|
|
|
/** Fahrer, der zu einem Login gehört — für „das bin ich"-Ansichten. */
|
|
getByUser(userId: string) {
|
|
return this.scoped.filter((r) => r.user === userId)
|
|
}
|
|
|
|
/** Bestehendes Konto zuordnen oder die Zuordnung lösen. */
|
|
async linkUser(riderId: string, userId: string | null) {
|
|
return await this.edit(riderId, { user: userId ?? '' } as Partial<RidersResponse>)
|
|
}
|
|
|
|
/**
|
|
* Legt dem Fahrer ein Login an und verknüpft es. Das Konto entsteht im
|
|
* Team-Store — es ist zuerst ein Teammitglied und erst dann dieser Fahrer.
|
|
*/
|
|
async createLogin(
|
|
rider: RidersResponse,
|
|
data: { email: string; password: string; name?: string },
|
|
): Promise<UsersResponse> {
|
|
const user = await this.teams.createMember({
|
|
...data,
|
|
name: data.name?.trim() || fullName(rider),
|
|
})
|
|
|
|
await this.linkUser(rider.id, user.id)
|
|
|
|
return user
|
|
}
|
|
|
|
getByEvent(eventId: string) {
|
|
return this.scoped.filter((r) => r.event === eventId)
|
|
}
|
|
|
|
isNumberTaken(number: string, eventId: string, excludeId?: string) {
|
|
return this.scoped.some(
|
|
(r) => r.number === number && r.event === eventId && r.id !== excludeId,
|
|
)
|
|
}
|
|
|
|
get filtered() {
|
|
const q = this.searchQuery.toLowerCase().trim()
|
|
if (!q) return this.scoped
|
|
return this.scoped.filter((r) => {
|
|
return (
|
|
(r.number ?? '').toLowerCase().includes(q) ||
|
|
(r.firstname ?? '').toLowerCase().includes(q) ||
|
|
(r.lastname ?? '').toLowerCase().includes(q) ||
|
|
(r.name ?? '').toLowerCase().includes(q)
|
|
)
|
|
})
|
|
}
|
|
|
|
destroy() {
|
|
if (this.unsubscribe) {
|
|
this.unsubscribe()
|
|
this.unsubscribe = null
|
|
}
|
|
}
|
|
}
|
|
|
|
export function setRiderContext() {
|
|
const store = new RiderStore()
|
|
setContext(KEY, store)
|
|
return store
|
|
}
|
|
|
|
export function getRiderContext(): RiderStore {
|
|
return getContext<RiderStore>(KEY)
|
|
}
|