bonuses: templates full-width list, drag-create modal, auto-evaluate, outstanding/completed states

This commit is contained in:
JCEEE
2026-07-30 15:27:03 +01:00
parent c55c090260
commit 8834281b4d
8 changed files with 524 additions and 676 deletions
+1 -1
View File
@@ -129,7 +129,7 @@ export interface BonusProgress {
memberColor: string memberColor: string
current: number current: number
criteriaValue: number criteriaValue: number
reward: { id: string; claimed: boolean } | null reward: { id: string; status: string } | null
state: BonusState state: BonusState
achieved: boolean achieved: boolean
} }
@@ -95,24 +95,20 @@ export const actions = {
if (!event.locals.session) throw redirect(303, '/login'); if (!event.locals.session) throw redirect(303, '/login');
const famId = event.locals.session.famId; const famId = event.locals.session.famId;
const fd = await event.request.formData(); const fd = await event.request.formData();
const templateId = fd.get('templateId') as string; 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'),
criteriaValue: parseInt(fd.get('criteriaValue') as string, 10) || 0,
period: fd.get('period') || '',
memberId: fd.get('memberId') || '',
phase: 'active',
};
try { try {
const templates = await hono.admin.bonusConfigs(event, famId);
const tmpl = (Array.isArray(templates) ? templates : []).find((c: any) => c.id === templateId);
if (!tmpl) return fail(400, { error: 'Template not found' });
const data: Record<string, unknown> = {
name: tmpl.name,
description: tmpl.description || '',
target: tmpl.target,
type: tmpl.type,
occurrence: tmpl.occurrence,
rewardType: tmpl.rewardType,
rewardValue: tmpl.rewardValue,
criteriaValue: tmpl.criteriaValue || 0,
period: tmpl.period || '',
memberId: tmpl.memberId || '',
phase: 'ready',
};
const record = await hono.admin.create(event, 'bonus-configs', famId, data); const record = await hono.admin.create(event, 'bonus-configs', famId, data);
return { record }; return { record };
} catch (e) { } catch (e) {
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,12 @@
import { hono } from '$lib/server/hono';
import { json } from '@sveltejs/kit';
export async function GET(event) {
const famId = event.params.fam;
try {
const progress = await hono.admin.bonusConfigProgress(event, famId);
return json(progress);
} catch {
return json([]);
}
}
@@ -134,15 +134,12 @@
} }
function enhanceCreate() { function enhanceCreate() {
return async ({ result, formData }: any) => { return async ({ result }: any) => {
if (result.type === 'success') { if (result.type === 'success') {
showCreateModal = false showCreateModal = false
const rec = result.data?.record const rec = result.data?.record
if (rec) { if (rec) {
templates = [rec as ChoreTemplate, ...templates] templates = [rec as ChoreTemplate, ...templates]
if (famStore.fam?.featureFlags?.optimisticUpdates) {
famStore.applyRecord('chore_templates', rec, 'create')
}
} }
} }
} }
@@ -161,9 +158,6 @@
customName: editCustomName || undefined, customName: editCustomName || undefined,
seasonIds: editSeasonIds.length > 0 ? editSeasonIds : [], seasonIds: editSeasonIds.length > 0 ? editSeasonIds : [],
} as AssignedChore } as AssignedChore
if (famStore.fam?.featureFlags?.optimisticUpdates) {
famStore.applyRecord('assigned_chores', assigned[idx], 'update')
}
} }
} }
closeEdit() closeEdit()
@@ -175,27 +169,24 @@
if (result.type === 'success' && editingTemplate) { if (result.type === 'success' && editingTemplate) {
const tid = editingTemplate!.id const tid = editingTemplate!.id
const idx = templates.findIndex((t: any) => t.id === tid) const idx = templates.findIndex((t: any) => t.id === tid)
if (idx !== -1) { if (idx !== -1) {
templates[idx] = { templates[idx] = {
...templates[idx], ...templates[idx],
name: editTplName, name: editTplName,
defaultFrequency: editTplFreq, defaultFrequency: editTplFreq,
defaultType: editTplType, defaultType: editTplType,
defaultValue: editTplValue, defaultValue: editTplValue,
} as ChoreTemplate } as ChoreTemplate
}
assigned = assigned.map((a: any) => assigned = assigned.map((a: any) =>
a.templateId === tid a.templateId === tid
? { ...a, frequency: editTplFreq, type: editTplType, value: editTplValue } ? { ...a, frequency: editTplFreq, type: editTplType, value: editTplValue }
: a : a
) as AssignedChore[] ) as AssignedChore[]
if (famStore.fam?.featureFlags?.optimisticUpdates) {
famStore.applyRecord('chore_templates', templates[idx], 'update')
}
closeEditTemplate() closeEditTemplate()
} }
} }
} }
}
</script> </script>
@@ -239,7 +230,7 @@
</div> </div>
<div class="card-actions"> <div class="card-actions">
<button onclick={() => openEditTemplate(t)} class="edit-btn" title="Edit template"></button> <button onclick={() => openEditTemplate(t)} class="edit-btn" title="Edit template"></button>
<form method="POST" action="?/deleteTemplate" use:enhance={() => { return async ({ result, formData }) => { if (result.type === 'success') { const id = formData.get('id'); templates = templates.filter((t) => t.id !== id) as ChoreTemplate[]; if (famStore.fam?.featureFlags?.optimisticUpdates) { const rec = result.data.record; if (rec) famStore.applyRecord('chore_templates', { ...rec, id }, 'delete'); } } }; }}> <form method="POST" action="?/deleteTemplate" use:enhance={() => { return async ({ result, formData }) => { if (result.type === 'success') { const id = formData.get('id'); templates = templates.filter((t) => t.id !== id) as ChoreTemplate[]; } }; }}>
<input type="hidden" name="id" value={t.id} /> <input type="hidden" name="id" value={t.id} />
<button type="submit" class="del-btn" title="Delete template">×</button> <button type="submit" class="del-btn" title="Delete template">×</button>
</form> </form>
@@ -98,7 +98,7 @@
</td> </td>
<td> <td>
{#if outstanding} {#if outstanding}
<form method="POST" action="?/claim" use:enhance={() => { return async (args: any) => { const d = args.result.data || {}; if (d.error) showToast(d.error); else if (args.result.type === 'success') { if (d.record && famStore.fam?.featureFlags?.optimisticUpdates) famStore.applyRecord('rewards', d.record, 'update'); fire(); } }; }}> <form method="POST" action="?/claim" use:enhance={() => { return async (args: any) => { const d = args.result.data || {}; if (d.error) showToast(d.error); else if (args.result.type === 'success') { fire(); } }; }}>
<input name="id" type="hidden" value={r.id} /> <input name="id" type="hidden" value={r.id} />
<Button type="submit" size="sm">Claim</Button> <Button type="submit" size="sm">Claim</Button>
</form> </form>
@@ -167,16 +167,11 @@ import { onDestroy } from 'svelte';
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog"> <div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
<h3>Delete "{deletingSeason.name}"?</h3> <h3>Delete "{deletingSeason.name}"?</h3>
<p class="warning">All chores assigned to this season will also be removed. This cannot be undone.</p> <p class="warning">All chores assigned to this season will also be removed. This cannot be undone.</p>
<form method="POST" action="?/deleteSeason" use:enhance={() => { return async ({ result }) => { <form method="POST" action="?/deleteSeason" use:enhance={() => { return async ({ result }) => {
if (result.type === 'success') { if (result.type === 'success') {
const r = result.data; deletingSeason = null;
if (r?.deletedChoreIds) { }
r.deletedChoreIds.forEach((id: string) => famStore.applyRecord('assigned_chores', { id }, 'delete')); }}}>
}
famStore.applyRecord('seasons', { id: deletingSeason.id }, 'delete');
deletingSeason = null;
}
}}}>
<input type="hidden" name="id" value={deletingSeason.id} /> <input type="hidden" name="id" value={deletingSeason.id} />
<div class="modal-actions"> <div class="modal-actions">
<button type="button" onclick={() => deletingSeason = null}>Cancel</button> <button type="button" onclick={() => deletingSeason = null}>Cancel</button>
+4 -6
View File
@@ -454,7 +454,7 @@ app.get("/api/admin/:famId/weekly-summary", requireAdmin, async (c) => {
} }
} }
const summaries = members.items.map((m: any) => { const summaries = await Promise.all(members.items.map(async (m: any) => {
const memberAssignments = assignedList.filter((a: any) => a.memberId === m.id); const memberAssignments = assignedList.filter((a: any) => a.memberId === m.id);
const memberCompletions = completionsList.filter((c: any) => c.memberId === m.id); const memberCompletions = completionsList.filter((c: any) => c.memberId === m.id);
@@ -505,11 +505,9 @@ app.get("/api/admin/:famId/weekly-summary", requireAdmin, async (c) => {
dayPoints, dayPoints,
dayCompletions, dayCompletions,
}; };
}); }));
return c.json({ weekStart: ws, daysInWeek, summaries }); return c.json({ weekStart: ws, daysInWeek, summaries });
return c.json({ weekStart: ws, summaries });
} catch (err) { return handleError(c, err); } } catch (err) { return handleError(c, err); }
}); });
@@ -749,7 +747,7 @@ async function evaluateFam(famId: string): Promise<void> {
famId, memberId: m.id, bonusConfigId: cfg.id, famId, memberId: m.id, bonusConfigId: cfg.id,
label, value: Number(cfg.rewardValue) || 0, label, value: Number(cfg.rewardValue) || 0,
rewardType: cfg.rewardType, rewardType: cfg.rewardType,
claimed: cfg.rewardType === "points", status: cfg.rewardType === "points" ? "claimed" : "unclaimed",
claimedAt: cfg.rewardType === "points" ? now : null, claimedAt: cfg.rewardType === "points" ? now : null,
date: now.slice(0, 10), date: now.slice(0, 10),
}); });
@@ -807,7 +805,7 @@ async function evaluateFam(famId: string): Promise<void> {
: `${cfg.name} ${cfg.rewardValue}`; : `${cfg.name} ${cfg.rewardValue}`;
const now = new Date().toISOString(); const now = new Date().toISOString();
await pb.create("rewards", { await pb.create("rewards", {
famId, memberId: m.id, bonusConfigId: cfg.id, famId, memberId: winner.memberId, bonusConfigId: cfg.id,
label, value: Number(cfg.rewardValue) || 0, label, value: Number(cfg.rewardValue) || 0,
rewardType: cfg.rewardType, rewardType: cfg.rewardType,
status: cfg.rewardType === "points" ? "claimed" : "unclaimed", status: cfg.rewardType === "points" ? "claimed" : "unclaimed",