remove payday debug and add multiple ui fixes
This commit is contained in:
@@ -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 ── */
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user