make major bonus updates
This commit is contained in:
Vendored
+2
@@ -1,4 +1,5 @@
|
||||
import { PocketBase } from 'pocketbase';
|
||||
import type { Session } from './lib/types';
|
||||
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
@@ -8,6 +9,7 @@ declare global {
|
||||
// interface Error {}
|
||||
interface Locals {
|
||||
pb: PocketBase;
|
||||
session?: Session | null;
|
||||
}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
|
||||
+126
-25
@@ -9,15 +9,20 @@ function sessionHeaders(event: RequestEvent): Record<string, string> {
|
||||
return {
|
||||
'x-session-famid': s.famId,
|
||||
'x-session-userid': s.userId,
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
}
|
||||
|
||||
async function request(method: string, path: string, body?: unknown, headers?: Record<string, string>) {
|
||||
async function request(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: unknown,
|
||||
headers?: Record<string, string>
|
||||
) {
|
||||
const res = await fetch(`${HONO_URL}${path}`, {
|
||||
method,
|
||||
headers: headers || { 'Content-Type': 'application/json' },
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
body: body ? JSON.stringify(body) : undefined
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || `${method} ${path} failed`);
|
||||
@@ -29,14 +34,30 @@ export const hono = {
|
||||
async list(event: RequestEvent, resource: string, famId: string) {
|
||||
return request('GET', `/api/admin/${famId}/${resource}`, undefined, sessionHeaders(event));
|
||||
},
|
||||
async create(event: RequestEvent, resource: string, famId: string, data: Record<string, unknown>) {
|
||||
async create(
|
||||
event: RequestEvent,
|
||||
resource: string,
|
||||
famId: string,
|
||||
data: Record<string, unknown>
|
||||
) {
|
||||
return request('POST', `/api/admin/${famId}/${resource}`, data, sessionHeaders(event));
|
||||
},
|
||||
async update(event: RequestEvent, resource: string, famId: string, id: string, data: Record<string, unknown>) {
|
||||
async update(
|
||||
event: RequestEvent,
|
||||
resource: string,
|
||||
famId: string,
|
||||
id: string,
|
||||
data: Record<string, unknown>
|
||||
) {
|
||||
return request('PATCH', `/api/admin/${famId}/${resource}/${id}`, data, sessionHeaders(event));
|
||||
},
|
||||
async remove(event: RequestEvent, resource: string, famId: string, id: string) {
|
||||
return request('DELETE', `/api/admin/${famId}/${resource}/${id}`, undefined, sessionHeaders(event));
|
||||
return request(
|
||||
'DELETE',
|
||||
`/api/admin/${famId}/${resource}/${id}`,
|
||||
undefined,
|
||||
sessionHeaders(event)
|
||||
);
|
||||
},
|
||||
async renameFam(event: RequestEvent, famId: string, name: string) {
|
||||
return request('PATCH', `/api/admin/${famId}/fam`, { name }, sessionHeaders(event));
|
||||
@@ -63,46 +84,126 @@ export const hono = {
|
||||
return request('GET', `/api/admin/${famId}/rewards`, undefined, sessionHeaders(event));
|
||||
},
|
||||
async claimReward(event: RequestEvent, famId: string, rewardId: string) {
|
||||
return request('POST', `/api/admin/${famId}/rewards/${rewardId}/claim`, undefined, sessionHeaders(event));
|
||||
return request(
|
||||
'POST',
|
||||
`/api/admin/${famId}/rewards/${rewardId}/claim`,
|
||||
undefined,
|
||||
sessionHeaders(event)
|
||||
);
|
||||
},
|
||||
async issueAllRewards(event: RequestEvent, famId: string, memberId: string, message?: string) {
|
||||
return request('POST', `/api/admin/${famId}/rewards/issue-all`, { memberId, message }, sessionHeaders(event));
|
||||
return request(
|
||||
'POST',
|
||||
`/api/admin/${famId}/rewards/issue-all`,
|
||||
{ memberId, message },
|
||||
sessionHeaders(event)
|
||||
);
|
||||
},
|
||||
async bonusConfigs(event: RequestEvent, famId: string) {
|
||||
return request('GET', `/api/admin/${famId}/bonus-configs`, undefined, sessionHeaders(event));
|
||||
},
|
||||
async bonusConfigProgress(event: RequestEvent, famId: string) {
|
||||
return request('GET', `/api/admin/${famId}/bonus-configs/progress`, undefined, sessionHeaders(event));
|
||||
return request(
|
||||
'GET',
|
||||
`/api/admin/${famId}/bonus-configs/progress`,
|
||||
undefined,
|
||||
sessionHeaders(event)
|
||||
);
|
||||
},
|
||||
async evaluateBonusConfig(event: RequestEvent, famId: string, configId?: string) {
|
||||
return request('POST', `/api/admin/${famId}/bonus-configs/evaluate`, { configId }, sessionHeaders(event));
|
||||
return request(
|
||||
'POST',
|
||||
`/api/admin/${famId}/bonus-configs/evaluate`,
|
||||
{ configId },
|
||||
sessionHeaders(event)
|
||||
);
|
||||
},
|
||||
async triggerBonusConfig(event: RequestEvent, famId: string, configId: string, memberId?: string) {
|
||||
return request('POST', `/api/admin/${famId}/bonus-configs/${configId}/trigger`, { memberId }, sessionHeaders(event));
|
||||
async triggerBonusConfig(
|
||||
event: RequestEvent,
|
||||
famId: string,
|
||||
configId: string,
|
||||
memberId?: string
|
||||
) {
|
||||
return request(
|
||||
'POST',
|
||||
`/api/admin/${famId}/bonus-configs/${configId}/trigger`,
|
||||
{ memberId },
|
||||
sessionHeaders(event)
|
||||
);
|
||||
},
|
||||
async assignBonusConfig(event: RequestEvent, famId: string, configId: string, data: Record<string, unknown>) {
|
||||
return request('POST', `/api/admin/${famId}/bonus-configs/${configId}/assign`, data, sessionHeaders(event));
|
||||
async assignBonusConfig(
|
||||
event: RequestEvent,
|
||||
famId: string,
|
||||
configId: string,
|
||||
data: Record<string, unknown>
|
||||
) {
|
||||
return request(
|
||||
'POST',
|
||||
`/api/admin/${famId}/bonus-configs/${configId}/assign`,
|
||||
data,
|
||||
sessionHeaders(event)
|
||||
);
|
||||
},
|
||||
async completeBonusConfig(event: RequestEvent, famId: string, configId: string, phase?: string) {
|
||||
return request('POST', `/api/admin/${famId}/bonus-configs/${configId}/complete`, { phase }, sessionHeaders(event));
|
||||
async completeBonusConfig(event: RequestEvent, famId: string, configId: string) {
|
||||
return request(
|
||||
'POST',
|
||||
`/api/admin/${famId}/bonus-configs/${configId}/complete`,
|
||||
undefined,
|
||||
sessionHeaders(event)
|
||||
);
|
||||
},
|
||||
async destroyBonusConfig(event: RequestEvent, famId: string, configId: string) {
|
||||
return request('POST', `/api/admin/${famId}/bonus-configs/${configId}/destroy`, undefined, sessionHeaders(event));
|
||||
return request(
|
||||
'POST',
|
||||
`/api/admin/${famId}/bonus-configs/${configId}/destroy`,
|
||||
undefined,
|
||||
sessionHeaders(event)
|
||||
);
|
||||
},
|
||||
async bonusConfigTallies(event: RequestEvent, famId: string) {
|
||||
return request('GET', `/api/admin/${famId}/bonus-configs/tallies`, undefined, sessionHeaders(event));
|
||||
return request(
|
||||
'GET',
|
||||
`/api/admin/${famId}/bonus-configs/tallies`,
|
||||
undefined,
|
||||
sessionHeaders(event)
|
||||
);
|
||||
},
|
||||
async revokeCompletion(event: RequestEvent, famId: string, completionId: string) {
|
||||
return request('POST', `/api/admin/${famId}/completions/${completionId}/revoke`, undefined, sessionHeaders(event));
|
||||
return request(
|
||||
'POST',
|
||||
`/api/admin/${famId}/completions/${completionId}/revoke`,
|
||||
undefined,
|
||||
sessionHeaders(event)
|
||||
);
|
||||
},
|
||||
async sendMessage(event: RequestEvent, famId: string, memberId: string, message: string) {
|
||||
return request('POST', `/api/admin/${famId}/send-message`, { memberId, message }, sessionHeaders(event));
|
||||
return request(
|
||||
'POST',
|
||||
`/api/admin/${famId}/send-message`,
|
||||
{ memberId, message },
|
||||
sessionHeaders(event)
|
||||
);
|
||||
},
|
||||
async memberChores(event: RequestEvent, famId: string, memberId: string) {
|
||||
return request('GET', `/api/admin/${famId}/members/${memberId}/chores`, undefined, sessionHeaders(event));
|
||||
return request(
|
||||
'GET',
|
||||
`/api/admin/${famId}/members/${memberId}/chores`,
|
||||
undefined,
|
||||
sessionHeaders(event)
|
||||
);
|
||||
},
|
||||
async updateMember(event: RequestEvent, famId: string, memberId: string, data: Record<string, unknown>) {
|
||||
return request('PATCH', `/api/admin/${famId}/members/${memberId}`, data, sessionHeaders(event));
|
||||
async updateMember(
|
||||
event: RequestEvent,
|
||||
famId: string,
|
||||
memberId: string,
|
||||
data: Record<string, unknown>
|
||||
) {
|
||||
return request(
|
||||
'PATCH',
|
||||
`/api/admin/${famId}/members/${memberId}`,
|
||||
data,
|
||||
sessionHeaders(event)
|
||||
);
|
||||
},
|
||||
async getProfile(event: RequestEvent, famId: string) {
|
||||
return request('GET', `/api/admin/${famId}/profile`, undefined, sessionHeaders(event));
|
||||
@@ -115,6 +216,6 @@ export const hono = {
|
||||
},
|
||||
async request(event: RequestEvent, method: string, path: string, body?: unknown) {
|
||||
return request(method, path, body, sessionHeaders(event));
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,99 +1,145 @@
|
||||
import { pb } from '$lib/pocketbase';
|
||||
import type {
|
||||
Member, ChoreTemplate, AssignedChore, Completion,
|
||||
WeeklyHistory, Reward, BonusConfig, Fam, Season,
|
||||
Member,
|
||||
ChoreTemplate,
|
||||
AssignedChore,
|
||||
Completion,
|
||||
WeeklyHistory,
|
||||
Reward,
|
||||
BonusConfig,
|
||||
BonusTemplate,
|
||||
Fam,
|
||||
Season
|
||||
} from '$lib/types';
|
||||
|
||||
type CollectionName = 'members' | 'chore_templates' | 'assigned_chores' | 'completions' | 'bonus_configs' | 'rewards' | 'seasons';
|
||||
type CollectionName =
|
||||
| 'members'
|
||||
| 'chore_templates'
|
||||
| 'assigned_chores'
|
||||
| 'completions'
|
||||
| 'bonus_configs'
|
||||
| 'bonus_templates'
|
||||
| 'rewards'
|
||||
| 'seasons';
|
||||
|
||||
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[]>([])
|
||||
seasons = $state<Season[]>([])
|
||||
initialized = $state(false)
|
||||
famId = $state('')
|
||||
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[]>([]);
|
||||
bonusTemplates = $state<BonusTemplate[]>([]);
|
||||
seasons = $state<Season[]>([]);
|
||||
initialized = $state(false);
|
||||
famId = $state('');
|
||||
|
||||
private unsubs: (() => void)[] = []
|
||||
private destroyed = false
|
||||
private unsubs: (() => void)[] = [];
|
||||
private destroyed = false;
|
||||
|
||||
memberMap(): Map<string, Member> {
|
||||
return new Map(this.members.map((m) => [m.id, m]))
|
||||
return new Map(this.members.map((m) => [m.id, m]));
|
||||
}
|
||||
|
||||
templateMap(): Map<string, ChoreTemplate> {
|
||||
return new Map(this.templates.map((t) => [t.id, t]))
|
||||
return new Map(this.templates.map((t) => [t.id, t]));
|
||||
}
|
||||
|
||||
bonusConfigMap(): Map<string, BonusConfig> {
|
||||
return new Map(this.bonusConfigs.map((b) => [b.id, b]))
|
||||
return new Map(this.bonusConfigs.map((b) => [b.id, b]));
|
||||
}
|
||||
|
||||
assignedForMember(memberId: string): AssignedChore[] {
|
||||
return this.assigned.filter((a) => a.memberId === memberId)
|
||||
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)
|
||||
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)
|
||||
return this.completions.some(
|
||||
(c) => c.assignedChoreId === assignedChoreId && (c.date?.slice(0, 10) || c.date) === date
|
||||
);
|
||||
}
|
||||
|
||||
private initPromise: Promise<void> | null = null
|
||||
private initPromise: Promise<void> | null = null;
|
||||
|
||||
async init(famId: string) {
|
||||
if (this.initialized && this.famId === famId) return
|
||||
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
|
||||
await this.initPromise;
|
||||
if (this.initialized && this.famId === famId) return;
|
||||
}
|
||||
|
||||
this.cleanup()
|
||||
this.destroyed = false
|
||||
this.famId = famId
|
||||
this.cleanup();
|
||||
this.destroyed = false;
|
||||
this.famId = famId;
|
||||
|
||||
this.initPromise = (async () => {
|
||||
try {
|
||||
const [famRes, membersRes, templatesRes, assignedRes, completionsRes, bonusConfigsRes, rewardsRes, seasonsRes] =
|
||||
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[]>,
|
||||
pb.collection('seasons').getFullList({ filter: `famId = '${famId}'` }) as Promise<Season[]>,
|
||||
])
|
||||
this.fam = famRes
|
||||
this.members = membersRes
|
||||
this.templates = templatesRes
|
||||
this.assigned = assignedRes
|
||||
this.completions = completionsRes
|
||||
this.bonusConfigs = bonusConfigsRes
|
||||
this.rewards = rewardsRes
|
||||
this.seasons = seasonsRes
|
||||
this.initialized = true
|
||||
const [
|
||||
famRes,
|
||||
membersRes,
|
||||
templatesRes,
|
||||
assignedRes,
|
||||
completionsRes,
|
||||
bonusConfigsRes,
|
||||
bonusTemplatesRes,
|
||||
rewardsRes,
|
||||
seasonsRes
|
||||
] = 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('bonus_templates').getFullList({ filter: `famId = '${famId}'` }) as Promise<
|
||||
BonusTemplate[]
|
||||
>,
|
||||
pb.collection('rewards').getFullList({ filter: `famId = '${famId}'` }) as Promise<
|
||||
Reward[]
|
||||
>,
|
||||
pb.collection('seasons').getFullList({ filter: `famId = '${famId}'` }) as Promise<
|
||||
Season[]
|
||||
>
|
||||
]);
|
||||
this.fam = famRes;
|
||||
this.members = membersRes;
|
||||
this.templates = templatesRes;
|
||||
this.assigned = assignedRes;
|
||||
this.completions = completionsRes;
|
||||
this.bonusConfigs = bonusConfigsRes;
|
||||
this.bonusTemplates = bonusTemplatesRes;
|
||||
this.rewards = rewardsRes;
|
||||
this.seasons = seasonsRes;
|
||||
this.initialized = true;
|
||||
} catch (e) {
|
||||
console.error('FamStore.init failed:', e)
|
||||
this.initPromise = null
|
||||
throw e
|
||||
console.error('FamStore.init failed:', e);
|
||||
this.initPromise = null;
|
||||
throw e;
|
||||
}
|
||||
|
||||
await this.subscribe()
|
||||
this.initPromise = null
|
||||
})()
|
||||
await this.subscribe();
|
||||
this.initPromise = null;
|
||||
})();
|
||||
|
||||
return this.initPromise!
|
||||
return this.initPromise!;
|
||||
}
|
||||
|
||||
private async subscribe() {
|
||||
@@ -103,58 +149,92 @@ class FamStore {
|
||||
{ collection: 'assigned_chores', filter: this.famId },
|
||||
{ collection: 'completions', filter: this.famId },
|
||||
{ collection: 'bonus_configs', filter: this.famId },
|
||||
{ collection: 'bonus_templates', filter: this.famId },
|
||||
{ collection: 'rewards', filter: this.famId },
|
||||
{ collection: 'seasons', filter: this.famId },
|
||||
]
|
||||
{ collection: 'seasons', 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)
|
||||
})
|
||||
})
|
||||
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)
|
||||
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
|
||||
}
|
||||
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
|
||||
case 'seasons': this.seasons = apply(this.seasons); break
|
||||
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 'bonus_templates':
|
||||
this.bonusTemplates = apply(this.bonusTemplates);
|
||||
break;
|
||||
case 'rewards':
|
||||
this.rewards = apply(this.rewards);
|
||||
break;
|
||||
case 'seasons':
|
||||
this.seasons = apply(this.seasons);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private handleRealtime(collection: string, action: string, record: any) {
|
||||
this.applyRecord(collection as CollectionName, record, action as 'create' | 'update' | 'delete')
|
||||
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
|
||||
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()
|
||||
export const famStore = new FamStore();
|
||||
|
||||
+132
-117
@@ -1,156 +1,171 @@
|
||||
export type Frequency = 'daily' | 'weekly'
|
||||
export type RewardType = 'points' | 'money'
|
||||
export type BonusTarget = 'individual' | 'competitive' | 'collaborative'
|
||||
export type BonusType = 'threshold' | 'count' | 'manual'
|
||||
export type BonusOccurrence = 'recurring' | 'once'
|
||||
export type BonusRewardType = 'points' | 'cash' | 'prize'
|
||||
export type BonusPeriod = 'weekly' | 'monthly'
|
||||
export type BonusStatus = 'active' | 'archived'
|
||||
export type BonusState = 'pending' | 'unclaimed' | 'claimed'
|
||||
export type Frequency = 'daily' | 'weekly';
|
||||
export type RewardType = 'points' | 'money';
|
||||
export type BonusTarget = 'individual' | 'competitive' | 'collaborative';
|
||||
export type BonusType = 'threshold' | 'count' | 'manual';
|
||||
export type BonusOccurrence = 'recurring' | 'once';
|
||||
export type BonusRewardType = 'points' | 'cash' | 'prize';
|
||||
export type BonusPeriod = 'weekly' | 'monthly' | 'daily';
|
||||
export type BonusStatus = 'active' | 'completed' | 'disabled';
|
||||
export type BonusState = 'pending' | 'unclaimed' | 'claimed';
|
||||
|
||||
export interface BonusTemplate {
|
||||
id: string;
|
||||
famId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
target: BonusTarget;
|
||||
type: BonusType;
|
||||
occurrence: BonusOccurrence;
|
||||
rewardType: BonusRewardType;
|
||||
rewardValue: string;
|
||||
criteriaValue?: number;
|
||||
period?: BonusPeriod;
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export interface Season {
|
||||
id: string
|
||||
famId: string
|
||||
name: string
|
||||
color: string
|
||||
active: boolean
|
||||
autoDisable?: string
|
||||
autoStart?: string
|
||||
created: string
|
||||
updated: string
|
||||
id: string;
|
||||
famId: string;
|
||||
name: string;
|
||||
color: string;
|
||||
active: boolean;
|
||||
autoDisable?: string;
|
||||
autoStart?: string;
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export interface Fam {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
inviteCode: string
|
||||
stripeCustomerId?: string
|
||||
featureFlags: Record<string, boolean>
|
||||
created: string
|
||||
updated: string
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
inviteCode: string;
|
||||
stripeCustomerId?: string;
|
||||
featureFlags: Record<string, boolean>;
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export interface Member {
|
||||
id: string
|
||||
famId: string
|
||||
name: string
|
||||
color: string
|
||||
deviceToken: string
|
||||
deviceTokenHint: string
|
||||
created: string
|
||||
updated: string
|
||||
id: string;
|
||||
famId: string;
|
||||
name: string;
|
||||
color: string;
|
||||
deviceToken: string;
|
||||
deviceTokenHint: string;
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export interface ChoreTemplate {
|
||||
id: string
|
||||
famId: string
|
||||
name: string
|
||||
description?: string
|
||||
defaultFrequency: Frequency
|
||||
defaultType: RewardType
|
||||
defaultValue: number
|
||||
created: string
|
||||
updated: string
|
||||
id: string;
|
||||
famId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
defaultFrequency: Frequency;
|
||||
defaultType: RewardType;
|
||||
defaultValue: number;
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export interface AssignedChore {
|
||||
id: string
|
||||
famId: string
|
||||
memberId: string
|
||||
templateId: string
|
||||
frequency: Frequency
|
||||
type: RewardType
|
||||
value: number
|
||||
customName?: string
|
||||
seasonIds?: string[]
|
||||
created: string
|
||||
updated: string
|
||||
id: string;
|
||||
famId: string;
|
||||
memberId: string;
|
||||
templateId: string;
|
||||
frequency: Frequency;
|
||||
type: RewardType;
|
||||
value: number;
|
||||
customName?: string;
|
||||
seasonIds?: string[];
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export interface Completion {
|
||||
id: string
|
||||
famId: string
|
||||
memberId: string
|
||||
assignedChoreId: string
|
||||
date: string
|
||||
completedAt: string
|
||||
id: string;
|
||||
famId: string;
|
||||
memberId: string;
|
||||
assignedChoreId: string;
|
||||
date: string;
|
||||
completedAt: string;
|
||||
}
|
||||
|
||||
export interface WeeklyHistory {
|
||||
id: string
|
||||
famId: string
|
||||
memberId: string
|
||||
weekStart: string
|
||||
pointsEarned: number
|
||||
moneyEarned: number
|
||||
choresCompleted: number
|
||||
bonusEarned: number
|
||||
id: string;
|
||||
famId: string;
|
||||
memberId: string;
|
||||
weekStart: string;
|
||||
pointsEarned: number;
|
||||
moneyEarned: number;
|
||||
choresCompleted: number;
|
||||
bonusEarned: number;
|
||||
}
|
||||
|
||||
export interface BonusConfig {
|
||||
id: string
|
||||
famId: string
|
||||
name: string
|
||||
description?: string
|
||||
target: BonusTarget
|
||||
memberId?: string
|
||||
type: BonusType
|
||||
occurrence: BonusOccurrence
|
||||
rewardType: BonusRewardType
|
||||
rewardValue: string
|
||||
criteriaValue?: number
|
||||
period?: BonusPeriod
|
||||
status: BonusStatus
|
||||
phase?: 'template' | 'ready' | 'active' | 'completed'
|
||||
created: string
|
||||
updated: string
|
||||
id: string;
|
||||
famId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
target: BonusTarget;
|
||||
memberId?: string;
|
||||
type: BonusType;
|
||||
occurrence: BonusOccurrence;
|
||||
rewardType: BonusRewardType;
|
||||
rewardValue: string;
|
||||
criteriaValue?: number;
|
||||
period?: BonusPeriod;
|
||||
status: BonusStatus;
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export interface Reward {
|
||||
id: string
|
||||
famId: string
|
||||
memberId: string
|
||||
bonusConfigId?: string
|
||||
label: string
|
||||
value: number
|
||||
rewardType: BonusRewardType
|
||||
status: 'unclaimed' | 'requested' | 'claimed'
|
||||
claimedAt?: string
|
||||
requestedAt?: string
|
||||
date: string
|
||||
created: string
|
||||
updated: string
|
||||
id: string;
|
||||
famId: string;
|
||||
memberId: string;
|
||||
bonusConfigId?: string;
|
||||
label: string;
|
||||
value: number;
|
||||
rewardType: BonusRewardType;
|
||||
status: 'unclaimed' | 'requested' | 'claimed';
|
||||
claimedAt?: string;
|
||||
requestedAt?: string;
|
||||
date: string;
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export interface BonusProgress {
|
||||
memberId: string
|
||||
memberName: string
|
||||
memberColor: string
|
||||
current: number
|
||||
criteriaValue: number
|
||||
reward: { id: string; status: string } | null
|
||||
state: BonusState
|
||||
achieved: boolean
|
||||
memberId: string;
|
||||
memberName: string;
|
||||
memberColor: string;
|
||||
current: number;
|
||||
criteriaValue: number;
|
||||
reward: { id: string; status: string } | null;
|
||||
state: BonusState;
|
||||
achieved: boolean;
|
||||
}
|
||||
|
||||
export interface BonusConfigWithProgress {
|
||||
config: BonusConfig
|
||||
progress: BonusProgress[]
|
||||
periodStart: string
|
||||
periodEnd: string
|
||||
config: BonusConfig;
|
||||
progress: BonusProgress[];
|
||||
periodStart: string;
|
||||
periodEnd: string;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
id: string
|
||||
famId: string
|
||||
webhookUrl?: string
|
||||
id: string;
|
||||
famId: string;
|
||||
webhookUrl?: string;
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
famId: string
|
||||
userId: string
|
||||
famSlug: string
|
||||
memberName?: string
|
||||
role?: string
|
||||
famId: string;
|
||||
userId: string;
|
||||
famSlug: string;
|
||||
memberName?: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
@@ -118,16 +118,22 @@
|
||||
}
|
||||
|
||||
function rewardLabel(r: any): string {
|
||||
if (r.rewardType === 'cash') return `£${(Number(r.value) / 100).toFixed(2)}`;
|
||||
if (r.rewardType === 'cash') return `£${Number(r.value).toFixed(2)}`;
|
||||
if (r.rewardType === 'points') return `${r.value} pts`;
|
||||
return r.label;
|
||||
}
|
||||
|
||||
function manualConfigs() {
|
||||
return parentBonusConfigs.filter(
|
||||
(b: any) =>
|
||||
(b.phase === 'active' || (!b.phase && b.status === 'active')) && b.type === 'manual'
|
||||
);
|
||||
return parentBonusConfigs.filter((b: any) => {
|
||||
if (!(b.status === 'active' && b.type === 'manual')) return false;
|
||||
if (b.occurrence === 'once') {
|
||||
const hasReward = parentRewards.some(
|
||||
(r: any) => r.bonusConfigId === b.id && (!b.memberId || r.memberId === b.memberId)
|
||||
);
|
||||
if (hasReward) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Child View (kanban) ───
|
||||
@@ -140,11 +146,29 @@
|
||||
let nameInput = $state('');
|
||||
let showColorPicker = $state(false);
|
||||
|
||||
let templates = $derived(famStore.initialized ? (famStore.templates as ChoreTemplate[]) : (data.templates as ChoreTemplate[]));
|
||||
let assigned = $derived(famStore.initialized ? (famStore.assigned as AssignedChore[]) : (data.assigned as AssignedChore[]));
|
||||
let completions = $derived(famStore.initialized ? (famStore.completions as Completion[]) : (data.completions as Completion[]));
|
||||
let rewards = $derived(famStore.initialized ? (famStore.rewards as Reward[]) : (data.rewards as Reward[]));
|
||||
let bonusConfigs = $derived(famStore.initialized ? (famStore.bonusConfigs as BonusConfig[]) : (data.bonusConfigs as BonusConfig[]));
|
||||
let templates = $derived(
|
||||
famStore.initialized
|
||||
? (famStore.templates as ChoreTemplate[])
|
||||
: (data.templates as ChoreTemplate[])
|
||||
);
|
||||
let assigned = $derived(
|
||||
famStore.initialized
|
||||
? (famStore.assigned as AssignedChore[])
|
||||
: (data.assigned as AssignedChore[])
|
||||
);
|
||||
let completions = $derived(
|
||||
famStore.initialized
|
||||
? (famStore.completions as Completion[])
|
||||
: (data.completions as Completion[])
|
||||
);
|
||||
let rewards = $derived(
|
||||
famStore.initialized ? (famStore.rewards as Reward[]) : (data.rewards as Reward[])
|
||||
);
|
||||
let bonusConfigs = $derived(
|
||||
famStore.initialized
|
||||
? (famStore.bonusConfigs as BonusConfig[])
|
||||
: (data.bonusConfigs as BonusConfig[])
|
||||
);
|
||||
|
||||
let loading = $state(true);
|
||||
let error = $state('');
|
||||
@@ -187,8 +211,12 @@
|
||||
let completedToday = $derived(
|
||||
completions.filter((c) => c.memberId === memberId && c.date?.slice(0, 10) === todayChild)
|
||||
);
|
||||
let claimableRewardsChild = $derived(rewards.filter((r) => r.memberId === memberId && r.status === 'unclaimed'));
|
||||
let requestedRewardsChild = $derived(rewards.filter((r) => r.memberId === memberId && r.status === 'requested'));
|
||||
let claimableRewardsChild = $derived(
|
||||
rewards.filter((r) => r.memberId === memberId && r.status === 'unclaimed')
|
||||
);
|
||||
let requestedRewardsChild = $derived(
|
||||
rewards.filter((r) => r.memberId === memberId && r.status === 'requested')
|
||||
);
|
||||
|
||||
let weeklyTotal = $derived.by(() => {
|
||||
const dailyChores = memberChores.filter((a) => a.frequency === 'daily');
|
||||
@@ -211,9 +239,7 @@
|
||||
const chore = assigned.find((a) => a.id === c.assignedChoreId);
|
||||
return sum + (chore?.type === 'money' ? Number(chore.value) : 0);
|
||||
}, 0) +
|
||||
myRewards
|
||||
.filter((r) => r.rewardType === 'cash')
|
||||
.reduce((sum, r) => sum + Number(r.value), 0)
|
||||
myRewards.filter((r) => r.rewardType === 'cash').reduce((sum, r) => sum + Number(r.value), 0)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -248,13 +274,12 @@
|
||||
const myCompletions = completions.filter((c) => c.memberId === memberId);
|
||||
const activeConfigs = bonusConfigs.filter(
|
||||
(b: BonusConfig) =>
|
||||
(b.phase === 'active' || (!b.phase && b.status === 'active')) &&
|
||||
b.status === 'active' &&
|
||||
b.target === 'individual' &&
|
||||
(!b.memberId || b.memberId === memberId)
|
||||
);
|
||||
return activeConfigs.map((cfg) => {
|
||||
const pStart =
|
||||
cfg.period === 'weekly' ? ws : cfg.period === 'monthly' ? monthStartStr() : '';
|
||||
const pStart = cfg.period === 'weekly' ? ws : cfg.period === 'monthly' ? monthStartStr() : '';
|
||||
const pEnd = pStart
|
||||
? (() => {
|
||||
if (cfg.period === 'weekly') {
|
||||
@@ -274,8 +299,7 @@
|
||||
const periodCompletions = pStart
|
||||
? myCompletions.filter(
|
||||
(c) =>
|
||||
(c.date?.slice(0, 10) || c.date) >= pStart &&
|
||||
(c.date?.slice(0, 10) || c.date) <= pEnd
|
||||
(c.date?.slice(0, 10) || c.date) >= pStart && (c.date?.slice(0, 10) || c.date) <= pEnd
|
||||
)
|
||||
: myCompletions;
|
||||
|
||||
@@ -374,14 +398,18 @@
|
||||
famStore.applyRecord('completions', existing, 'delete');
|
||||
}
|
||||
} else {
|
||||
famStore.applyRecord('completions', {
|
||||
id: 'optimistic-' + chore.id,
|
||||
famId,
|
||||
memberId,
|
||||
assignedChoreId: chore.id,
|
||||
date: todayChild,
|
||||
completedAt: new Date().toISOString()
|
||||
} as Completion, 'create');
|
||||
famStore.applyRecord(
|
||||
'completions',
|
||||
{
|
||||
id: 'optimistic-' + chore.id,
|
||||
famId,
|
||||
memberId,
|
||||
assignedChoreId: chore.id,
|
||||
date: todayChild,
|
||||
completedAt: new Date().toISOString()
|
||||
} as Completion,
|
||||
'create'
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -392,14 +420,18 @@
|
||||
}
|
||||
} catch (e) {
|
||||
if (wasCompleted) {
|
||||
famStore.applyRecord('completions', {
|
||||
id: 'revert-' + chore.id + '-' + Date.now(),
|
||||
famId,
|
||||
memberId,
|
||||
assignedChoreId: chore.id,
|
||||
date: todayChild,
|
||||
completedAt: new Date().toISOString()
|
||||
} as Completion, 'create');
|
||||
famStore.applyRecord(
|
||||
'completions',
|
||||
{
|
||||
id: 'revert-' + chore.id + '-' + Date.now(),
|
||||
famId,
|
||||
memberId,
|
||||
assignedChoreId: chore.id,
|
||||
date: todayChild,
|
||||
completedAt: new Date().toISOString()
|
||||
} as Completion,
|
||||
'create'
|
||||
);
|
||||
} else {
|
||||
const optimistic = completions.find((c) => c.id === 'optimistic-' + chore.id);
|
||||
if (optimistic) {
|
||||
@@ -421,7 +453,7 @@
|
||||
}
|
||||
|
||||
function rewardLabelChild(r: Reward): string {
|
||||
if (r.rewardType === 'cash') return `£${(Number(r.value) / 100).toFixed(2)}`;
|
||||
if (r.rewardType === 'cash') return `£${Number(r.value).toFixed(2)}`;
|
||||
if (r.rewardType === 'points') return `${r.value} pts`;
|
||||
return r.label;
|
||||
}
|
||||
@@ -468,7 +500,14 @@
|
||||
{#each todays as c}
|
||||
<li>
|
||||
✅ {choreNameFor(c.assignedChoreId)}
|
||||
<form method="POST" action="?/revoke" use:enhance={() => { return async (args) => handleResult(args); }} class="revoke-form">
|
||||
<form
|
||||
method="POST"
|
||||
action="?/revoke"
|
||||
use:enhance={() => {
|
||||
return async (args) => handleResult(args);
|
||||
}}
|
||||
class="revoke-form"
|
||||
>
|
||||
<input type="hidden" name="id" value={c.id} />
|
||||
<button type="submit" class="revoke-btn" title="Revoke">↩</button>
|
||||
</form>
|
||||
@@ -487,7 +526,7 @@
|
||||
{#each manualConfigs() as bc}
|
||||
{@const val =
|
||||
bc.rewardType === 'cash'
|
||||
? `£${(Number(bc.rewardValue) / 100).toFixed(2)}`
|
||||
? `£${Number(bc.rewardValue).toFixed(2)}`
|
||||
: bc.rewardType === 'points'
|
||||
? `${bc.rewardValue} pts`
|
||||
: bc.rewardValue}
|
||||
@@ -500,7 +539,13 @@
|
||||
<span class="trigger-value">{val}</span>
|
||||
</div>
|
||||
{#if targeted}
|
||||
<form method="POST" action="?/trigger" use:enhance={() => { return async (args: any) => handleResult(args); }}>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/trigger"
|
||||
use:enhance={() => {
|
||||
return async (args: any) => handleResult(args);
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="configId" value={bc.id} />
|
||||
<input type="hidden" name="memberId" value={targeted.id} />
|
||||
<div class="trigger-row">
|
||||
@@ -511,7 +556,13 @@
|
||||
</div>
|
||||
</form>
|
||||
{:else}
|
||||
<form method="POST" action="?/trigger" use:enhance={() => { return async (args: any) => handleResult(args); }}>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/trigger"
|
||||
use:enhance={() => {
|
||||
return async (args: any) => handleResult(args);
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="configId" value={bc.id} />
|
||||
<div class="trigger-row">
|
||||
<select name="memberId" class="trigger-select">
|
||||
@@ -561,6 +612,7 @@
|
||||
<div class="payment-right">
|
||||
<span class="value">{rewardLabel(r)}</span>
|
||||
<Button type="submit" size="sm" variant="primary">Issue</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{/each}
|
||||
@@ -569,13 +621,14 @@
|
||||
method="POST"
|
||||
action="?/issueAll"
|
||||
use:enhance={() => {
|
||||
return async (args: any) =>
|
||||
handleResult(args);
|
||||
return async (args: any) => handleResult(args);
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="memberId" value={m.id} />
|
||||
<input type="hidden" name="message" value={selectedMessage[m.id] || ''} />
|
||||
<Button type="submit" size="sm" variant="secondary">Issue All ({requested.length})</Button>
|
||||
<Button type="submit" size="sm" variant="secondary"
|
||||
>Issue All ({requested.length})</Button
|
||||
>
|
||||
</form>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -746,11 +799,11 @@
|
||||
</div>
|
||||
<div class="badge-card cash">
|
||||
<span class="badge-label">Total Cash</span>
|
||||
<span class="badge-value">£{(totalCash / 100).toFixed(2)}</span>
|
||||
<span class="badge-value">£{totalCash.toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="badge-card pending">
|
||||
<span class="badge-label">Pending Cash</span>
|
||||
<span class="badge-value">£{(pendingCash / 100).toFixed(2)}</span>
|
||||
<span class="badge-value">£{pendingCash.toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="badge-card points">
|
||||
<span class="badge-label">Total Points</span>
|
||||
@@ -1440,9 +1493,16 @@
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.request-all-btn:hover { background: #1d4ed8; }
|
||||
.request-col { border-color: #93c5fd; background: #eff6ff; }
|
||||
.claim-card.requested { opacity: 0.7; }
|
||||
.request-all-btn:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
.request-col {
|
||||
border-color: #93c5fd;
|
||||
background: #eff6ff;
|
||||
}
|
||||
.claim-card.requested {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.requested-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 8px;
|
||||
@@ -1465,7 +1525,9 @@
|
||||
color: #2563eb;
|
||||
margin: 0 0 0.25rem;
|
||||
}
|
||||
.payment-row.outstanding { opacity: 0.6; }
|
||||
.payment-row.outstanding {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
h1 {
|
||||
display: flex;
|
||||
|
||||
@@ -4,21 +4,95 @@ import { hono } from '$lib/server/hono';
|
||||
export async function load(event) {
|
||||
if (!event.locals.session) throw redirect(303, '/login');
|
||||
const famId = event.locals.session.famId;
|
||||
const [configs, members, progress, rewards] = await Promise.all([
|
||||
const [configs, templates, members, progress, rewards] = await Promise.all([
|
||||
hono.admin.bonusConfigs(event, famId),
|
||||
hono.admin.list(event, 'bonus-templates', famId),
|
||||
hono.admin.list(event, 'members', famId),
|
||||
hono.admin.bonusConfigProgress(event, famId),
|
||||
hono.admin.rewards(event, famId),
|
||||
hono.admin.rewards(event, famId)
|
||||
]);
|
||||
return { configs, members, progress, rewards };
|
||||
return { configs, templates, members, progress, rewards };
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
createTemplate: async (event) => {
|
||||
if (!event.locals.session) throw redirect(303, '/login');
|
||||
const famId = event.locals.session.famId;
|
||||
const fd = await event.request.formData();
|
||||
const data: Record<string, unknown> = {
|
||||
name: fd.get('name'),
|
||||
description: fd.get('description') || '',
|
||||
target: fd.get('target'),
|
||||
type: fd.get('type'),
|
||||
occurrence: fd.get('occurrence'),
|
||||
rewardType: fd.get('rewardType'),
|
||||
rewardValue: fd.get('rewardValue')
|
||||
};
|
||||
const criteriaValue = fd.get('criteriaValue');
|
||||
if (criteriaValue) data.criteriaValue = parseInt(criteriaValue as string, 10) || 0;
|
||||
const period = fd.get('period');
|
||||
if (period !== null) data.period = period;
|
||||
if (data.occurrence === 'once') data.period = '';
|
||||
try {
|
||||
const record = await hono.admin.create(event, 'bonus-templates', famId, data);
|
||||
return { record };
|
||||
} catch (e) {
|
||||
return fail(400, { error: e instanceof Error ? e.message : 'Failed to create template' });
|
||||
}
|
||||
},
|
||||
|
||||
updateTemplate: async (event) => {
|
||||
if (!event.locals.session) throw redirect(303, '/login');
|
||||
const famId = event.locals.session.famId;
|
||||
const fd = await event.request.formData();
|
||||
const id = fd.get('id') as string;
|
||||
const data: Record<string, unknown> = {};
|
||||
const name = fd.get('name');
|
||||
if (name) data.name = name;
|
||||
const target = fd.get('target');
|
||||
if (target) data.target = target;
|
||||
const type = fd.get('type');
|
||||
if (type) data.type = type;
|
||||
const occurrence = fd.get('occurrence');
|
||||
if (occurrence) data.occurrence = occurrence;
|
||||
const rewardType = fd.get('rewardType');
|
||||
if (rewardType) data.rewardType = rewardType;
|
||||
const rewardValue = fd.get('rewardValue');
|
||||
if (rewardValue) data.rewardValue = rewardValue;
|
||||
const criteriaValue = fd.get('criteriaValue');
|
||||
if (criteriaValue) data.criteriaValue = parseInt(criteriaValue as string, 10) || 0;
|
||||
const period = fd.get('period');
|
||||
if (period !== null) data.period = period;
|
||||
if (occurrence === 'once') data.period = '';
|
||||
const description = fd.get('description');
|
||||
if (description) data.description = description;
|
||||
try {
|
||||
const record = await hono.admin.update(event, 'bonus-templates', famId, id, data);
|
||||
return { record };
|
||||
} catch (e) {
|
||||
return fail(400, { error: e instanceof Error ? e.message : 'Failed to update template' });
|
||||
}
|
||||
},
|
||||
|
||||
deleteTemplate: async (event) => {
|
||||
if (!event.locals.session) throw redirect(303, '/login');
|
||||
const famId = event.locals.session.famId;
|
||||
const fd = await event.request.formData();
|
||||
const id = fd.get('id') as string;
|
||||
try {
|
||||
await hono.admin.remove(event, 'bonus-templates', famId, id);
|
||||
return { deleted: true };
|
||||
} catch (e) {
|
||||
return fail(400, { error: e instanceof Error ? e.message : 'Failed to delete template' });
|
||||
}
|
||||
},
|
||||
|
||||
createConfig: async (event) => {
|
||||
if (!event.locals.session) throw redirect(303, '/login');
|
||||
const famId = event.locals.session.famId;
|
||||
const fd = await event.request.formData();
|
||||
const phase = fd.get('phase') as string;
|
||||
const startMode = fd.get('startMode') as string;
|
||||
const status = startMode === 'disabled' ? 'disabled' : 'active';
|
||||
const data: Record<string, unknown> = {
|
||||
name: fd.get('name'),
|
||||
target: fd.get('target'),
|
||||
@@ -26,12 +100,13 @@ export const actions = {
|
||||
occurrence: fd.get('occurrence'),
|
||||
rewardType: fd.get('rewardType'),
|
||||
rewardValue: fd.get('rewardValue'),
|
||||
phase: phase || 'ready',
|
||||
status
|
||||
};
|
||||
const criteriaValue = fd.get('criteriaValue');
|
||||
if (criteriaValue) data.criteriaValue = parseInt(criteriaValue as string, 10) || 0;
|
||||
const period = fd.get('period');
|
||||
if (period !== null) data.period = period;
|
||||
if (data.occurrence === 'once') data.period = '';
|
||||
const description = fd.get('description');
|
||||
if (description) data.description = description;
|
||||
const memberId = fd.get('memberId');
|
||||
@@ -66,6 +141,7 @@ export const actions = {
|
||||
if (criteriaValue) data.criteriaValue = parseInt(criteriaValue as string, 10) || 0;
|
||||
const period = fd.get('period');
|
||||
if (period !== null) data.period = period;
|
||||
if (occurrence === 'once') data.period = '';
|
||||
const description = fd.get('description');
|
||||
if (description) data.description = description;
|
||||
const memberId = fd.get('memberId');
|
||||
@@ -87,7 +163,7 @@ export const actions = {
|
||||
await hono.admin.remove(event, 'bonus-configs', famId, id);
|
||||
return { deleted: true };
|
||||
} catch (e) {
|
||||
return fail(400, { error: e instanceof Error ? e.message : 'Failed to archive config' });
|
||||
return fail(400, { error: e instanceof Error ? e.message : 'Failed to delete config' });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -95,6 +171,8 @@ export const actions = {
|
||||
if (!event.locals.session) throw redirect(303, '/login');
|
||||
const famId = event.locals.session.famId;
|
||||
const fd = await event.request.formData();
|
||||
const startMode = fd.get('startMode') as string;
|
||||
const status = startMode === 'disabled' ? 'disabled' : 'active';
|
||||
const data: Record<string, unknown> = {
|
||||
name: fd.get('name'),
|
||||
description: fd.get('description') || '',
|
||||
@@ -106,13 +184,16 @@ export const actions = {
|
||||
criteriaValue: parseInt(fd.get('criteriaValue') as string, 10) || 0,
|
||||
period: fd.get('period') || '',
|
||||
memberId: fd.get('memberId') || '',
|
||||
phase: 'active',
|
||||
status
|
||||
};
|
||||
if (data.occurrence === 'once') data.period = '';
|
||||
try {
|
||||
const record = await hono.admin.create(event, 'bonus-configs', famId, data);
|
||||
return { record };
|
||||
} catch (e) {
|
||||
return fail(400, { error: e instanceof Error ? e.message : 'Failed to create bonus from template' });
|
||||
return fail(400, {
|
||||
error: e instanceof Error ? e.message : 'Failed to create bonus from template'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -124,7 +205,10 @@ export const actions = {
|
||||
const target = fd.get('target') as string;
|
||||
const memberId = fd.get('memberId') as string;
|
||||
try {
|
||||
const record = await hono.admin.assignBonusConfig(event, famId, id, { target, memberId: memberId || null });
|
||||
const record = await hono.admin.assignBonusConfig(event, famId, id, {
|
||||
target,
|
||||
memberId: memberId || null
|
||||
});
|
||||
return { record };
|
||||
} catch (e) {
|
||||
return fail(400, { error: e instanceof Error ? e.message : 'Failed to assign bonus' });
|
||||
@@ -136,12 +220,11 @@ export const actions = {
|
||||
const famId = event.locals.session.famId;
|
||||
const fd = await event.request.formData();
|
||||
const id = fd.get('id') as string;
|
||||
const phase = fd.get('phase') as string;
|
||||
try {
|
||||
const record = await hono.admin.completeBonusConfig(event, famId, id, phase || 'completed');
|
||||
const record = await hono.admin.completeBonusConfig(event, famId, id);
|
||||
return { record };
|
||||
} catch (e) {
|
||||
return fail(400, { error: e instanceof Error ? e.message : 'Failed to update bonus phase' });
|
||||
return fail(400, { error: e instanceof Error ? e.message : 'Failed to complete bonus' });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -158,6 +241,23 @@ export const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
toggleConfig: async (event) => {
|
||||
if (!event.locals.session) throw redirect(303, '/login');
|
||||
const famId = event.locals.session.famId;
|
||||
const fd = await event.request.formData();
|
||||
const id = fd.get('id') as string;
|
||||
const currentStatus = fd.get('currentStatus') as string;
|
||||
const newStatus = currentStatus === 'disabled' ? 'active' : 'disabled';
|
||||
try {
|
||||
const record = await hono.admin.update(event, 'bonus-configs', famId, id, {
|
||||
status: newStatus
|
||||
});
|
||||
return { record };
|
||||
} catch (e) {
|
||||
return fail(400, { error: e instanceof Error ? e.message : 'Failed to toggle bonus' });
|
||||
}
|
||||
},
|
||||
|
||||
claimReward: async (event) => {
|
||||
if (!event.locals.session) throw redirect(303, '/login');
|
||||
const famId = event.locals.session.famId;
|
||||
@@ -178,5 +278,5 @@ export const actions = {
|
||||
} catch (e) {
|
||||
return { error: e instanceof Error ? e.message : 'Failed to evaluate' };
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
function rewardAmount(r: any): string {
|
||||
if (r.rewardType === 'cash') return `£${(Number(r.value) / 100).toFixed(2)}`;
|
||||
if (r.rewardType === 'cash') return `£${Number(r.value).toFixed(2)}`;
|
||||
if (r.rewardType === 'points') return `${r.value} pts`;
|
||||
return r.label;
|
||||
}
|
||||
@@ -112,7 +112,7 @@
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="num"><strong>Outstanding cash</strong></td>
|
||||
<td class="num"><strong>£{(totalOutstanding / 100).toFixed(2)}</strong></td>
|
||||
<td class="num"><strong>£{totalOutstanding.toFixed(2)}</strong></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user