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
@@ -0,0 +1,27 @@
import { redirect } from '@sveltejs/kit';
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 [rewards, members] = await Promise.all([
hono.admin.rewards(event, famId),
hono.admin.list(event, 'members', famId),
]);
return { rewards, members };
}
export const actions = {
claim: async (event) => {
if (!event.locals.session) throw redirect(303, '/login');
const famId = event.locals.session.famId;
const fd = await event.request.formData();
const rewardId = fd.get('id') as string;
try {
const record = await hono.admin.claimReward(event, famId, rewardId);
return { record };
} catch (e) {
return { error: e instanceof Error ? e.message : 'Failed to claim reward' };
}
},
};
@@ -0,0 +1,137 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { famStore } from '$lib/stores/fam.svelte';
import { ViewHeader, CardGrid, Card, Button } from '$lib/components';
let { data } = $props();
let rewards = $state(famStore.initialized ? famStore.rewards : (data.rewards || []))
let members = $state(famStore.initialized ? famStore.members : (data.members || []))
function memberName(memberId: string): string {
return famStore.memberMap().get(memberId)?.name || members.find((m: any) => m.id === memberId)?.name || '?';
}
function memberColor(memberId: string): string {
return famStore.memberMap().get(memberId)?.color || members.find((m: any) => m.id === memberId)?.color || '#6366f1';
}
function rewardAmount(r: any): string {
if (r.rewardType === 'cash') return ${(Number(r.value) / 100).toFixed(2)}`;
if (r.rewardType === 'points') return `${r.value} pts`;
return r.label;
}
function rewardStatus(r: any): string {
if (!r.claimed) return 'Outstanding';
if (r.claimedAt?.slice(0, 10) === r.date) return 'Auto';
return 'Claimed';
}
function isOutstanding(r: any): boolean { return !r.claimed; }
let sorted = $derived([...rewards].sort((a: any, b: any) => {
const da = a.date || a.id || '';
const db = b.date || b.id || '';
return da < db ? 1 : da > db ? -1 : 0;
}))
let toast = $state('')
async function fire() {
const { default: confetti } = await import('@hiseb/confetti');
confetti({ count: 80, size: 4, velocity: 500, fade: true, position: { x: window.innerWidth / 2, y: 0 } });
}
function showToast(msg: string) {
toast = msg;
setTimeout(() => toast = '', 4000);
}
let totalOutstanding = $derived(
rewards
.filter((r: any) => isOutstanding(r))
.filter((r: any) => r.rewardType === 'cash')
.reduce((sum: number, r: any) => sum + Number(r.value), 0)
)
</script>
<ViewHeader title="Reward Ledger" subtitle="All rewards earned by members" />
{#if toast}
<div class="toast">{toast}</div>
{/if}
<CardGrid>
<Card cols={3}>
{#if rewards.length === 0}
<p style="color:#9ca3af;text-align:center;padding:2rem">No rewards yet. Points earned will automatically create bonus rewards when thresholds are met.</p>
{:else}
<table>
<thead>
<tr>
<th>Date</th>
<th>Member</th>
<th>Description</th>
<th class="num">Amount</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
{#each sorted as r}
{@const outstanding = isOutstanding(r)}
<tr class:claimed={!outstanding}>
<td class="date">{r.date?.slice(0, 10) || '—'}</td>
<td>
<span class="dot" style="background:{memberColor(r.memberId)}"></span>
{memberName(r.memberId)}
</td>
<td>{r.label}</td>
<td class="num">{rewardAmount(r)}</td>
<td>
<span class="status-badge" class:outstanding class:claimed-status={!outstanding}>
{rewardStatus(r)}
</span>
</td>
<td>
{#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.applyRecord('rewards', d.record, 'update'); fire(); } }; }}>
<input name="id" type="hidden" value={r.id} />
<Button type="submit" size="sm">Claim</Button>
</form>
{/if}
</td>
</tr>
{/each}
</tbody>
<tfoot>
<tr>
<td></td>
<td></td>
<td class="num"><strong>Outstanding cash</strong></td>
<td class="num"><strong>£{(totalOutstanding / 100).toFixed(2)}</strong></td>
<td></td>
<td></td>
</tr>
</tfoot>
</table>
{/if}
</Card>
</CardGrid>
<style>
.toast { position: fixed; top: 3.5rem; right: 1.5rem; background: #dc2626; color: white; padding: 0.6rem 1rem; border-radius: 8px; font-size: 0.85rem; z-index: 999; box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
th, td { padding: 0.5rem 0.6rem; border-bottom: 1px solid #e5e7eb; text-align: left; }
th { background: #f9fafb; font-weight: 600; position: sticky; top: 0; }
.num { text-align: right; font-variant-numeric: tabular-nums; }
.date { font-family: monospace; font-size: 0.85rem; color: #6b7280; white-space: nowrap; }
tr.claimed { opacity: 0.5; }
tr.claimed td { color: #9ca3af; }
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 0.3rem; }
.status-badge { font-size: 0.75rem; padding: 2px 8px; border-radius: 10px; }
.status-badge.outstanding { background: #fef3c7; color: #92400e; }
.status-badge.claimed-status { background: #d1fae5; color: #065f46; }
tfoot td { border-top: 2px solid #d1d5db; padding-top: 0.6rem; }
</style>