remove payday debug and add multiple ui fixes

This commit is contained in:
JCEEE
2026-08-28 19:32:32 +01:00
parent bcdd3805d5
commit d303eda038
19 changed files with 705 additions and 697 deletions
@@ -35,7 +35,7 @@ export async function load(event) {
s.crud.list('rewards', famId),
s.crud.list('bonus-configs', famId),
s.crud.list('completions', famId),
s.settings.get(famId).catch(() => ({ simulateEow: false, webhookUrl: '' }))
s.settings.get(famId)
]);
return {
role: 'parent',
@@ -105,8 +105,7 @@ export async function load(event) {
tallies: {} as Record<string, unknown>,
payday: chores.payday,
paydayTime: chores.paydayTime || '18:00',
timezone: chores.timezone || 'auto',
simulateEow: !!chores.simulateEow
timezone: chores.timezone || 'auto'
};
} catch {
return empty;
@@ -114,33 +113,6 @@ export async function load(event) {
}
export const actions = {
setEow: async (event) => {
if (!event.locals.user) throw redirect(303, '/login');
const famId = event.locals.user.famId;
const fd = await event.request.formData();
const on = fd.get('on') === 'true';
try {
const s = createServices(pbUser(event), event.locals.user);
const result = await s.settings.update(famId, { simulateEow: on });
return { simulateEow: result.simulateEow };
} catch (e) {
return { error: e instanceof Error ? e.message : 'Failed to update settings' };
}
},
previewEow: async (event) => {
if (!event.locals.user) throw redirect(303, '/login');
const famId = event.locals.user.famId;
try {
const s = createServices(pbUser(event), event.locals.user);
const preview = await s.fam.eowPreview(famId);
await s.settings.update(famId, { simulateEow: true });
return { preview, simulateEow: true };
} catch (e) {
return { error: e instanceof Error ? e.message : 'Failed to preview payday' };
}
},
claim: async (event) => {
if (!event.locals.user) throw redirect(303, '/login');
const famId = event.locals.user.famId;
File diff suppressed because it is too large Load Diff
@@ -4,7 +4,7 @@
import { famStore } from '$lib/stores/fam.svelte';
import { ViewHeader, CardGrid, Card } from '$lib/components';
import TemplateIcon from '$lib/components/TemplateIcon.svelte';
import { choresIcon, todoIcon } from '$lib/components/icons';
import { choresIcon, todoIcon, checkCircleIcon } from '$lib/components/icons';
import { formatHumanDate } from '$lib/format';
import { TEMPLATE_COLORS, ICON_NAMES, accentBg, outlineColor } from '$lib/templateIcons';
import type { ChoreTemplate, AssignedChore, Member, Season, Completion } from '$lib/types';
@@ -48,8 +48,26 @@
// Curated celebration emojis for todos
const EMOJI_CHOICES = [
'🎉', '⭐', '🏆', '💪', '🧹', '📚', '🦸', '✨', '🌟', '🥇',
'🎯', '💚', '🚀', '🦄', '🌈', '🍀', '🎨', '🎸', '⚽', '🍰'
'🎉',
'⭐',
'🏆',
'💪',
'🧹',
'📚',
'🦸',
'✨',
'🌟',
'🥇',
'🎯',
'💚',
'🚀',
'🦄',
'🌈',
'🍀',
'🎨',
'🎸',
'⚽',
'🍰'
];
let editFreq = $state('daily');
@@ -68,9 +86,11 @@
// Platform-owned templates only — families assign these, they don't create.
let templates = $derived(
((famStore.initialized ? (famStore.templates as ChoreTemplate[]) : (data.templates as ChoreTemplate[])) || []).filter(
(t: ChoreTemplate) => t.global
)
(
(famStore.initialized
? (famStore.templates as ChoreTemplate[])
: (data.templates as ChoreTemplate[])) || []
).filter((t: ChoreTemplate) => t.global)
);
let members = $state(
famStore.initialized ? (famStore.members as Member[]) : (data.members as Member[]) || []
@@ -85,7 +105,7 @@
// Only active seasons are selectable for assignment / filtering.
let activeSeasons = $derived((seasons || []).filter((s) => s.active !== false));
let completions = $state((data.completions as Completion[]) || []);
// $effect(() => console.log(seasons, activeSeasons));
let filteredAssigned = $derived(
seasonFilter === 'all' ? assigned : assigned.filter((a) => a.seasonIds?.includes(seasonFilter))
);
@@ -366,7 +386,10 @@
</h3>
<!-- TODOS accordion wrapper -->
<div class="accordion-wrapper accordion-todos" class:open={accordionState[m.id]?.todos ?? true}>
<div
class="accordion-wrapper accordion-todos"
class:open={accordionState[m.id]?.todos ?? true}
>
<button class="accordion-head" onclick={() => toggleAccordion(m.id, 'todos')}>
<span class="accordion-title">
<span class="accordion-icon">{@html todoIcon}</span>
@@ -376,7 +399,9 @@
>{@html (accordionState[m.id]?.todos ?? true) ? CHEVRON_UP : CHEVRON_DOWN}</span
>
</button>
<span class="accordion-count" style="background:{m.color}">{todosForMember(m.id).length}</span>
<span class="accordion-count" style="background:{m.color}"
>{todosForMember(m.id).length}</span
>
{#if accordionState[m.id]?.todos ?? true}
<div class="accordion-body">
{#each sortedTodosForMember(m.id) as a}
@@ -414,7 +439,8 @@
</div>
</div>
{#if completed}
<span class="todo-completed-badge">✅ TBC completed</span>
<span class="todo-completed-badge">{@html checkCircleIcon} TBC completed</span
>
{:else}
<button
class="del-btn"
@@ -440,7 +466,10 @@
</div>
<!-- CHORES accordion wrapper -->
<div class="accordion-wrapper accordion-chores" class:open={accordionState[m.id]?.chores ?? true}>
<div
class="accordion-wrapper accordion-chores"
class:open={accordionState[m.id]?.chores ?? true}
>
<button class="accordion-head" onclick={() => toggleAccordion(m.id, 'chores')}>
<span class="accordion-title">
<span class="accordion-icon">{@html choresIcon}</span>
@@ -450,7 +479,9 @@
>{@html (accordionState[m.id]?.chores ?? true) ? CHEVRON_UP : CHEVRON_DOWN}</span
>
</button>
<span class="accordion-count" style="background:{m.color}">{assignedForMember(m.id).length}</span>
<span class="accordion-count" style="background:{m.color}"
>{assignedForMember(m.id).length}</span
>
{#if accordionState[m.id]?.chores ?? true}
<div class="accordion-body">
{#each assignedForMember(m.id) as a}
@@ -460,14 +491,20 @@
onclick={() => openEdit(a)}
role="button"
tabindex="0"
style="border-left-color:{outlineColor(a.color)}; background:{accentBg(a.color)}"
style="border-left-color:{outlineColor(a.color)}; background:{accentBg(
a.color
)}"
>
<div class="card-body">
<TemplateIcon name={a.icon} size={16} color={outlineColor(a.color)} />
<strong>{a.customName || tName}</strong>
<span class="badge">{a.frequency}</span>
{#each a.seasonIds || [] as sid}
<span class="season-dot" title={seasonName(sid)} style="background:{seasonColor(sid)}"></span>
<span
class="season-dot"
title={seasonName(sid)}
style="background:{seasonColor(sid)}"
></span>
{/each}
{#if (a.seasonIds || []).length === 0}
<span class="badge season muted">All year</span>
@@ -509,14 +546,16 @@
</CardGrid>
<!-- Season pill selector (reused by assign + edit modals) -->
{#snippet seasonPills(list: Season[], selected: string[], set: (v: string[]) => void)}
<div class="season-pills">
{#snippet seasonPills(list: Season[] = [], selected: string[] = [], set: (v: string[]) => void)}
<div class="season-pills" role="radiogroup" aria-label="Seasons">
<button
type="button"
class="season-pill"
class:selected={selected.length === 0}
style="--pill:#9ca3af"
onclick={() => set([])}>All year</button
role="radio"
aria-checked={selected.length === 0}
onclick={() => set([])}>Any</button
>
{#each list as s}
<button
@@ -524,6 +563,8 @@
class="season-pill"
class:selected={selected[0] === s.id}
style="--pill:{s.color}"
role="radio"
aria-checked={selected[0] === s.id}
onclick={() => set([s.id])}>{s.name}</button
>
{/each}
@@ -532,16 +573,25 @@
<!-- Assign from Template Modal -->
{#if assignModal}
<div class="overlay" onclick={(e) => { if (e.target === e.currentTarget) assignModal = null; }} role="presentation">
<div
class="overlay"
onclick={(e) => {
if (e.target === e.currentTarget) assignModal = null;
}}
role="presentation"
>
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
<h3>Assign: {assignModal.t.name}</h3>
<p class="hint">
Copies this platform template into {members.find((m) => m.id === assignModal?.memberId)?.name}'s
chores. Amend, then Assign.
Copies this platform template into {members.find((m) => m.id === assignModal?.memberId)
?.name}'s chores. Amend, then Assign.
</p>
<label>Name <input bind:value={assignName} placeholder="Task name" /></label>
<label
>Description <input bind:value={assignDescription} placeholder="Optional description" /></label
>Description <input
bind:value={assignDescription}
placeholder="Optional description"
/></label
>
<div class="color-field">
<span class="field-label">Colour</span>
@@ -556,7 +606,8 @@
? '#e5e7eb'
: c.value}"
title={c.label}
onclick={() => (assignColor = c.value)}></button>
onclick={() => (assignColor = c.value)}
></button>
{/each}
</div>
</div>
@@ -589,7 +640,7 @@
</label>
<div class="season-field">
<span class="field-label">Seasons</span>
<seasonPills activeSeasons selected={assignSeasonIds} set={(v: string[]) => (assignSeasonIds = v)} />
{@render seasonPills(seasons, assignSeasonIds, (v: string[]) => (assignSeasonIds = v))}
</div>
<div class="modal-actions">
<button type="button" onclick={() => (assignModal = null)}>Cancel</button>
@@ -601,7 +652,13 @@
<!-- Edit Assignment Modal -->
{#if showEditModal && editingAssignment}
<div class="overlay" onclick={(e) => { if (e.target === e.currentTarget) closeEdit(); }} role="presentation">
<div
class="overlay"
onclick={(e) => {
if (e.target === e.currentTarget) closeEdit();
}}
role="presentation"
>
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
<h3>
{#if editingAssignment.isTodo}
@@ -619,7 +676,11 @@
</label>
<label
>Description
<input name="description" bind:value={editDescription} placeholder="Optional description" /></label
<input
name="description"
bind:value={editDescription}
placeholder="Optional description"
/></label
>
{#if editingAssignment.isTodo}
@@ -639,7 +700,14 @@
{:else if editType === 'money'}
<label>
Cash amount (£)
<input name="value" type="number" step="0.01" min="0.01" bind:value={editValue} required />
<input
name="value"
type="number"
step="0.01"
min="0.01"
bind:value={editValue}
required
/>
</label>
{:else}
<input type="hidden" name="emoji" value={editEmoji} />
@@ -648,7 +716,8 @@
<button
type="button"
class="link-btn"
onclick={() => (showTodoEmojiPicker = !showTodoEmojiPicker)}>
onclick={() => (showTodoEmojiPicker = !showTodoEmojiPicker)}
>
{showTodoEmojiPicker ? 'Hide' : 'Choose'}
</button>
</div>
@@ -691,20 +760,21 @@
? '#e5e7eb'
: c.value}"
title={c.label}
onclick={() => (editColor = c.value)}></button>
onclick={() => (editColor = c.value)}
></button>
{/each}
</div>
<input type="hidden" name="color" value={editColor} />
</div>
<label
><span class="icon-preview"
><TemplateIcon name={editIcon} size={22} color={outlineColor(editColor)} /></span
>
<select name="icon" bind:value={editIcon}>
<option value="">None</option>
{#each ICON_NAMES as n}<option value={n}>{n}</option>{/each}
</select>
</label>
<label class="icon-row"
><span class="icon-preview"
><TemplateIcon name={editIcon} size={22} color={outlineColor(editColor)} /></span
>
<select name="icon" bind:value={editIcon}>
<option value="">None</option>
{#each ICON_NAMES as n}<option value={n}>{n}</option>{/each}
</select>
</label>
<label>
Frequency
<select name="frequency" bind:value={editFreq}>
@@ -725,7 +795,7 @@
</label>
<div class="season-field">
<span class="field-label">Seasons</span>
<seasonPills activeSeasons selected={editSeasonIds} set={(v: string[]) => (editSeasonIds = v)} />
{@render seasonPills(seasons, editSeasonIds, (v: string[]) => (editSeasonIds = v))}
</div>
{/if}
<div class="modal-actions">
@@ -739,7 +809,13 @@
<!-- Add Todo Modal -->
{#if showTodoModal && todoMemberId}
<div class="overlay" onclick={(e) => { if (e.target === e.currentTarget) showTodoModal = false; }} role="presentation">
<div
class="overlay"
onclick={(e) => {
if (e.target === e.currentTarget) showTodoModal = false;
}}
role="presentation"
>
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
<h3>Add Todo for {members.find((m) => m.id === todoMemberId)?.name || 'Member'}</h3>
<form
@@ -785,13 +861,24 @@
{:else if todoType === 'money'}
<label>
Cash amount (£)
<input name="value" type="number" step="0.01" min="0.01" bind:value={todoValue} required />
<input
name="value"
type="number"
step="0.01"
min="0.01"
bind:value={todoValue}
required
/>
</label>
{:else}
<input type="hidden" name="emoji" value={todoEmoji} />
<div class="emoji-field">
<span class="emoji-preview">{todoEmoji}</span>
<button type="button" class="link-btn" onclick={() => (showTodoEmojiPicker = !showTodoEmojiPicker)}>
<button
type="button"
class="link-btn"
onclick={() => (showTodoEmojiPicker = !showTodoEmojiPicker)}
>
{showTodoEmojiPicker ? 'Hide' : 'Choose'}
</button>
</div>
@@ -1464,6 +1551,13 @@
border-radius: 4px;
flex-shrink: 0;
white-space: nowrap;
display: inline-flex;
align-items: center;
gap: 3px;
}
.todo-completed-badge :global(svg) {
width: 0.95em;
height: 0.95em;
}
/* ── Points badge ── */
+9 -1
View File
@@ -2,6 +2,7 @@
import { page } from '$app/state';
import { enhance } from '$app/forms';
import { Button } from '$lib/components';
import { homeIcon } from '$lib/components/icons';
const famSlug = page.params.fam;
let name = $state('');
@@ -14,7 +15,7 @@
<main class="mx-auto flex min-h-screen max-w-md flex-col items-center justify-center px-6">
<section class="w-full rounded-2xl border border-slate-200 bg-white p-8 text-center shadow-sm">
<div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-indigo-100 text-2xl">
🏠
<span class="home-badge">{@html homeIcon}</span>
</div>
<h1 class="text-xl font-bold text-slate-900">Join {famSlug}</h1>
<p class="mt-1 text-sm text-slate-500">
@@ -63,3 +64,10 @@
</p>
</section>
</main>
<style>
.home-badge :global(svg) {
width: 1.75rem;
height: 1.75rem;
}
</style>
@@ -2,6 +2,7 @@
import { page } from '$app/state';
import { enhance } from '$app/forms';
import { Button } from '$lib/components';
import { homeIcon } from '$lib/components/icons';
const famSlug = page.params.fam;
const username = page.params.username;
@@ -14,7 +15,7 @@
<main class="mx-auto flex min-h-screen max-w-md flex-col items-center justify-center px-6">
<section class="w-full rounded-2xl border border-slate-200 bg-white p-8 text-center shadow-sm">
<div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-indigo-100 text-2xl">
🏠
<span class="home-badge">{@html homeIcon}</span>
</div>
<h1 class="text-xl font-bold text-slate-900">Welcome to {famSlug}!</h1>
<p class="mt-1 text-sm text-slate-500">
@@ -54,4 +55,10 @@
Code is valid for 20 minutes. Ask your parent for a new one if it expires.
</p>
</section>
</main>
</main>
<style>
.home-badge :global(svg) {
width: 1.75rem;
height: 1.75rem;
}
</style>