update ui and other frontend updates
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user