update ui and other frontend updates

This commit is contained in:
JCEEE
2026-07-29 09:41:52 +01:00
parent 04b83925e0
commit 57ef30d3a7
79 changed files with 7323 additions and 429 deletions
+155
View File
@@ -0,0 +1,155 @@
import { pb } from '$lib/pocketbase';
import type {
Member, ChoreTemplate, AssignedChore, Completion,
WeeklyHistory, Reward, BonusConfig, Fam,
} from '$lib/types';
type CollectionName = 'members' | 'chore_templates' | 'assigned_chores' | 'completions' | 'bonus_configs' | 'rewards';
class FamStore {
fam = $state<Fam | null>(null)
members = $state<Member[]>([])
templates = $state<ChoreTemplate[]>([])
assigned = $state<AssignedChore[]>([])
completions = $state<Completion[]>([])
history = $state<WeeklyHistory[]>([])
rewards = $state<Reward[]>([])
bonusConfigs = $state<BonusConfig[]>([])
initialized = $state(false)
famId = $state('')
private unsubs: (() => void)[] = []
private destroyed = false
memberMap(): Map<string, Member> {
return new Map(this.members.map((m) => [m.id, m]))
}
templateMap(): Map<string, ChoreTemplate> {
return new Map(this.templates.map((t) => [t.id, t]))
}
bonusConfigMap(): Map<string, BonusConfig> {
return new Map(this.bonusConfigs.map((b) => [b.id, b]))
}
assignedForMember(memberId: string): AssignedChore[] {
return this.assigned.filter((a) => a.memberId === memberId)
}
completionsForDate(date: string): Completion[] {
return this.completions.filter((c) => (c.date?.slice(0, 10) || c.date) === date)
}
isCompleted(assignedChoreId: string, date: string): boolean {
return this.completions.some((c) => c.assignedChoreId === assignedChoreId && (c.date?.slice(0, 10) || c.date) === date)
}
private initPromise: Promise<void> | null = null
async init(famId: string) {
if (this.initialized && this.famId === famId) return
// Wait for any in-flight init to finish first
if (this.initPromise) {
await this.initPromise
if (this.initialized && this.famId === famId) return
}
this.cleanup()
this.destroyed = false
this.famId = famId
this.initPromise = (async () => {
try {
const [famRes, membersRes, templatesRes, assignedRes, completionsRes, bonusConfigsRes, rewardsRes] =
await Promise.all([
pb.collection('fams').getOne(famId) as Promise<Fam>,
pb.collection('members').getFullList({ filter: `famId = '${famId}'` }) as Promise<Member[]>,
pb.collection('chore_templates').getFullList({ filter: `famId = '${famId}'` }) as Promise<ChoreTemplate[]>,
pb.collection('assigned_chores').getFullList({ filter: `famId = '${famId}'` }) as Promise<AssignedChore[]>,
pb.collection('completions').getFullList({ filter: `famId = '${famId}'` }) as Promise<Completion[]>,
pb.collection('bonus_configs').getFullList({ filter: `famId = '${famId}'` }) as Promise<BonusConfig[]>,
pb.collection('rewards').getFullList({ filter: `famId = '${famId}'` }) as Promise<Reward[]>,
])
this.fam = famRes
this.members = membersRes
this.templates = templatesRes
this.assigned = assignedRes
this.completions = completionsRes
this.bonusConfigs = bonusConfigsRes
this.rewards = rewardsRes
this.initialized = true
} catch (e) {
console.error('FamStore.init failed:', e)
this.initPromise = null
throw e
}
await this.subscribe()
this.initPromise = null
})()
return this.initPromise!
}
private async subscribe() {
const subs: { collection: CollectionName; filter?: string }[] = [
{ collection: 'members', filter: this.famId },
{ collection: 'chore_templates', filter: this.famId },
{ collection: 'assigned_chores', filter: this.famId },
{ collection: 'completions', filter: this.famId },
{ collection: 'bonus_configs', filter: this.famId },
{ collection: 'rewards', filter: this.famId },
]
const promises = subs.map(({ collection, filter }) => {
const filterStr = filter ? `famId = '${filter}'` : ''
return pb.collection(collection).subscribe('*', (data: any) => {
if (this.destroyed) return
this.handleRealtime(collection, data.action, data.record)
}, { filter: filterStr || undefined }).then((unsub) => {
if (this.destroyed) { unsub(); return }
this.unsubs.push(unsub)
}).catch((err: Error) => {
console.error(`[famStore] subscribe failed for ${collection}:`, err)
})
})
await Promise.allSettled(promises)
}
// Called from form action callbacks for instant UI feedback,
// and from PB subscribe SSE for multi-user realtime.
applyRecord(collection: CollectionName, record: any, action: 'create' | 'update' | 'delete') {
const apply = <T extends { id: string }>(list: T[]): T[] => {
if (action === 'create') return [record, ...list]
if (action === 'update') return list.map((x) => (x.id === record.id ? { ...x, ...record } : x))
if (action === 'delete') return list.filter((x) => x.id !== record.id)
return list
}
switch (collection) {
case 'members': this.members = apply(this.members); break
case 'chore_templates': this.templates = apply(this.templates); break
case 'assigned_chores': this.assigned = apply(this.assigned); break
case 'completions': this.completions = apply(this.completions); break
case 'bonus_configs': this.bonusConfigs = apply(this.bonusConfigs); break
case 'rewards': this.rewards = apply(this.rewards); break
}
}
private handleRealtime(collection: string, action: string, record: any) {
this.applyRecord(collection as CollectionName, record, action as 'create' | 'update' | 'delete')
}
cleanup() {
// TODO - we need to properly unsubscribe from pocketbase
this.destroyed = true
for (const unsub of this.unsubs) unsub()
this.unsubs = []
this.initialized = false
}
}
export const famStore = new FamStore()