bonuses: templates full-width list, drag-create modal, auto-evaluate, outstanding/completed states
This commit is contained in:
@@ -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
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user