fix some issues - including add ledger page and fix reoccuring completions

This commit is contained in:
JCEEE
2026-08-05 09:03:08 +01:00
parent 6e817be13f
commit d162ea762e
14 changed files with 993 additions and 546 deletions
@@ -3,12 +3,14 @@
import { page } from '$app/state';
import { famStore } from '$lib/stores/fam.svelte';
import { ViewHeader, CardGrid, Card } from '$lib/components';
import { formatShortDate } from '$lib/format';
import { formatHumanDate } from '$lib/format';
import type { ChoreTemplate, AssignedChore, Member, Season, Completion } from '$lib/types';
// ── Chevron SVG icons ──
const CHEVRON_DOWN = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>';
const CHEVRON_UP = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>';
const CHEVRON_DOWN =
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>';
const CHEVRON_UP =
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>';
let { data, form } = $props();
@@ -354,7 +356,6 @@
<p class="empty">No templates yet</p>
{/if}
</div>
</div>
<button class="add-inline" onclick={() => (showCreateModal = true)}>+ New template</button>
</div>
</Card>
@@ -369,53 +370,106 @@
ondragover={handleDragOver}
ondrop={(e) => handleDrop(e, m.id)}
>
<h3>
<span class="dot" style="background:{m.color}"></span>
{m.name}
</h3>
<h3>
<span class="dot" style="background:{m.color}"></span>
{m.name}
</h3>
<!-- TODOS accordion wrapper -->
<div class="accordion-wrapper accordion-todos">
<button class="accordion-head" onclick={() => toggleAccordion(m.id, 'todos')}>
<span class="accordion-label">📋 Todos ({todosForMember(m.id).length})</span>
<span class="accordion-chevron">{@html (accordionState[m.id]?.todos ?? true) ? CHEVRON_UP : CHEVRON_DOWN}</span>
</button>
{#if accordionState[m.id]?.todos ?? true}
<div class="accordion-body">
{#each sortedTodosForMember(m.id) as a}
{@const completed = isTodoCompleted(a.id)}
{@const urgency = todoUrgency(a)}
<div
class="todo-admin-card"
class:todo-completed={completed}
class:todo-blue-bg={!completed && urgency === 'blue'}
class:todo-green-bg={!completed && urgency === 'green'}
class:todo-orange-bg={!completed && urgency === 'orange'}
class:todo-red-bg={!completed && urgency === 'red'}
class:todo-black-bg={!completed && urgency === 'black'}
onclick={() => !completed && openEdit(a)}
role="button"
tabindex={completed ? -1 : 0}
<!-- TODOS accordion wrapper -->
<div class="accordion-wrapper accordion-todos">
<button class="accordion-head" onclick={() => toggleAccordion(m.id, 'todos')}>
<span class="accordion-label">📋 Todos ({todosForMember(m.id).length})</span>
<span class="accordion-chevron"
>{@html (accordionState[m.id]?.todos ?? true) ? CHEVRON_UP : CHEVRON_DOWN}</span
>
</button>
{#if accordionState[m.id]?.todos ?? true}
<div class="accordion-body">
{#each sortedTodosForMember(m.id) as a}
{@const completed = isTodoCompleted(a.id)}
{@const urgency = todoUrgency(a)}
<div
class="todo-admin-card"
class:todo-completed={completed}
class:todo-blue-bg={!completed && urgency === 'blue'}
class:todo-green-bg={!completed && urgency === 'green'}
class:todo-orange-bg={!completed && urgency === 'orange'}
class:todo-red-bg={!completed && urgency === 'red'}
class:todo-black-bg={!completed && urgency === 'black'}
onclick={() => !completed && openEdit(a)}
role="button"
tabindex={completed ? -1 : 0}
>
<div class="todo-admin-body">
<strong class="todo-admin-name">{a.customName || 'Todo'}</strong>
<div class="todo-admin-meta">
{#if a.type === 'emoji'}
<span class="todo-admin-type">🎯 emoji</span>
{:else}
<span class="todo-admin-type"
><span class="badge badge-pts">{a.value} pts</span></span
>
{/if}
{#if a.completeBy}
<span class="todo-admin-deadline"
>due {formatHumanDate(a.completeBy)}</span
>
{/if}
</div>
</div>
{#if completed}
<span class="todo-completed-badge">✅ TBC completed</span>
{:else}
<button
class="del-btn"
title="Remove todo"
onclick={async (e) => {
e.stopPropagation();
const s = page.data.session as any;
if (!s) return;
await fetch(`/api/admin/${s.famId}/assigned-chores/${a.id}`, {
method: 'DELETE',
headers: { 'x-session-famid': s.famId, 'x-session-userid': s.userId }
});
}}>×</button
>
{/if}
</div>
{/each}
<button class="add-inline accordion-add" onclick={() => openTodo(m.id)}
>+ Add a todo</button
>
<div class="todo-admin-body">
<strong class="todo-admin-name">{a.customName || 'Todo'}</strong>
<div class="todo-admin-meta">
{#if a.type === 'emoji'}
<span class="todo-admin-type">🎯 emoji</span>
{:else}
<span class="todo-admin-type"><span class="badge badge-pts">{a.value} pts</span></span>
{/if}
{#if a.completeBy}
<span class="todo-admin-deadline">due {formatShortDate(a.completeBy)}</span>
</div>
{/if}
</div>
<!-- CHORES accordion wrapper -->
<div class="accordion-wrapper accordion-chores">
<button class="accordion-head" onclick={() => toggleAccordion(m.id, 'chores')}>
<span class="accordion-label">Chores ({assignedForMember(m.id).length})</span>
<span class="accordion-chevron"
>{@html (accordionState[m.id]?.chores ?? true) ? CHEVRON_UP : CHEVRON_DOWN}</span
>
</button>
{#if accordionState[m.id]?.chores ?? true}
<div class="accordion-body">
{#each assignedForMember(m.id) as a}
{@const tName = templateName(a.templateId)}
<div class="card assigned" onclick={() => openEdit(a)} role="button" tabindex="0">
<div class="card-body">
<strong>{a.customName || tName}</strong>
<span class="badge">{a.frequency}</span>
<span class="badge type">{a.type}</span>
{#if seasonFilter !== 'all' && isGlobalChore(a)}
<span class="badge season">Add to</span>
{/if}
</div>
</div>
{#if completed}
<span class="todo-completed-badge">✅ TBC completed</span>
{:else}
<div class="card-value">
{a.type === 'money' ? `£${Number(a.value).toFixed(2)}` : `${a.value} pts`}
</div>
<button
class="del-btn"
title="Remove todo"
title="Remove assignment"
onclick={async (e) => {
e.stopPropagation();
const s = page.data.session as any;
@@ -426,57 +480,14 @@
});
}}>×</button
>
{/if}
</div>
{/each}
<button class="add-inline accordion-add" onclick={() => openTodo(m.id)}>+ Add a todo</button>
</div>
{/if}
</div>
<!-- CHORES accordion wrapper -->
<div class="accordion-wrapper accordion-chores">
<button class="accordion-head" onclick={() => toggleAccordion(m.id, 'chores')}>
<span class="accordion-label">Chores ({assignedForMember(m.id).length})</span>
<span class="accordion-chevron">{@html (accordionState[m.id]?.chores ?? true) ? CHEVRON_UP : CHEVRON_DOWN}</span>
</button>
{#if accordionState[m.id]?.chores ?? true}
<div class="accordion-body">
{#each assignedForMember(m.id) as a}
{@const tName = templateName(a.templateId)}
<div class="card assigned" onclick={() => openEdit(a)} role="button" tabindex="0">
<div class="card-body">
<strong>{a.customName || tName}</strong>
<span class="badge">{a.frequency}</span>
<span class="badge type">{a.type}</span>
{#if seasonFilter !== 'all' && isGlobalChore(a)}
<span class="badge season">Add to</span>
{/if}
</div>
<div class="card-value">
{a.type === 'money' ? `£${Number(a.value).toFixed(2)}` : `${a.value} pts`}
</div>
<button
class="del-btn"
title="Remove assignment"
onclick={async (e) => {
e.stopPropagation();
const s = page.data.session as any;
if (!s) return;
await fetch(`/api/admin/${s.famId}/assigned-chores/${a.id}`, {
method: 'DELETE',
headers: { 'x-session-famid': s.famId, 'x-session-userid': s.userId }
});
}}>×</button
>
</div>
{/each}
{#if assignedForMember(m.id).length === 0}
<p class="empty">Drop a chore here</p>
{/if}
</div>
{/if}
</div>
{/each}
{#if assignedForMember(m.id).length === 0}
<p class="empty">Drop a chore here</p>
{/if}
</div>
{/if}
</div>
</div>
{/each}
{#if members.length < 3}
@@ -819,7 +830,9 @@
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
transition:
background 0.15s,
border-color 0.15s;
text-align: center;
}
.template-section > .add-inline {
@@ -828,13 +841,13 @@
.accordion-add {
margin-bottom: 0;
margin-top: 0.35rem;
background: rgba(255,255,255,0.25);
border-color: rgba(255,255,255,0.4);
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.4);
color: #fff;
}
.accordion-add:hover {
background: rgba(255,255,255,0.35);
border-color: rgba(255,255,255,0.6);
background: rgba(255, 255, 255, 0.35);
border-color: rgba(255, 255, 255, 0.6);
color: #fff;
}
.add-inline:hover {
@@ -852,7 +865,10 @@
align-items: center;
gap: 0.5rem;
position: relative;
transition: transform 0.15s, box-shadow 0.15s, border-color 0.15s;
transition:
transform 0.15s,
box-shadow 0.15s,
border-color 0.15s;
}
.card:hover {
transform: translateY(-1px);
@@ -905,7 +921,9 @@
line-height: 1;
flex-shrink: 0;
border-radius: 4px;
transition: color 0.15s, background 0.15s;
transition:
color 0.15s,
background 0.15s;
}
.edit-btn:hover {
color: #6366f1;
@@ -1056,7 +1074,7 @@
width: 100%;
padding: 0.55rem 0.85rem;
border: none;
border-left: 4px solid rgba(0,0,0,0.2);
border-left: 4px solid rgba(0, 0, 0, 0.2);
background: transparent;
cursor: pointer;
font-size: 0.82rem;
@@ -1072,7 +1090,7 @@
}
.accordion-body {
padding: 0.5rem 0.65rem 0.65rem;
background: rgba(0,0,0,0.08);
background: rgba(0, 0, 0, 0.08);
}
.accordion-chevron {
display: flex;
@@ -1093,7 +1111,10 @@
display: flex;
align-items: center;
gap: 0.4rem;
transition: transform 0.15s, box-shadow 0.15s, opacity 0.2s;
transition:
transform 0.15s,
box-shadow 0.15s,
opacity 0.2s;
}
.todo-admin-card:hover {
transform: translateY(-1px);
@@ -1185,8 +1206,8 @@
45deg,
transparent,
transparent 8px,
rgba(0,0,0,0.02) 8px,
rgba(0,0,0,0.02) 16px
rgba(0, 0, 0, 0.02) 8px,
rgba(0, 0, 0, 0.02) 16px
);
border-style: dashed;
opacity: 0.5;