add fixes to claims and seasons

This commit is contained in:
JCEEE
2026-07-30 11:12:35 +01:00
parent 57ef30d3a7
commit b254564436
24 changed files with 2517 additions and 704 deletions
@@ -3,7 +3,7 @@
import { page } from '$app/state';
import { famStore } from '$lib/stores/fam.svelte';
import { ViewHeader, CardGrid, Card, Button } from '$lib/components';
import type { ChoreTemplate, AssignedChore, Member } from '$lib/types';
import type { ChoreTemplate, AssignedChore, Member, Season } from '$lib/types';
let { data, form } = $props();
@@ -30,18 +30,45 @@
let editTplType = $state('points')
let editTplValue = $state(10)
let seasonFilter = $state('all')
let editSeasonIds = $state<string[]>([])
let templates = $state(famStore.initialized ? (famStore.templates as ChoreTemplate[]) : (data.templates as ChoreTemplate[] || []))
let members = $state(famStore.initialized ? (famStore.members as Member[]) : (data.members as Member[] || []))
let assigned = $state(famStore.initialized ? (famStore.assigned as AssignedChore[]) : (data.assigned as AssignedChore[] || []))
let assigned = $derived(famStore.initialized ? (famStore.assigned as AssignedChore[]) : (data.assigned as AssignedChore[] || []))
let seasons = $state(famStore.initialized ? famStore.seasons : (data.seasons || []))
let filteredAssigned = $derived(
seasonFilter === 'all'
? assigned
: assigned.filter(a => a.seasonIds?.includes(seasonFilter))
)
function assignedForMember(memberId: string): AssignedChore[] {
return filteredAssigned.filter(a => a.memberId === memberId)
}
function assignedForMemberAll(memberId: string): AssignedChore[] {
return assigned.filter(a => a.memberId === memberId)
}
function isGlobalChore(a: AssignedChore): boolean {
return !a.seasonIds || a.seasonIds.length === 0
}
function templateName(templateId: string): string {
return famStore.templateMap().get(templateId)?.name || '?'
}
function seasonName(id: string): string {
return seasons.find(s => s.id === id)?.name || id
}
function seasonNames(a: AssignedChore): string {
return (a.seasonIds || []).map(id => seasonName(id)).join(', ') || 'Global'
}
function getDefault(templateId: string): { frequency: string; type: string; value: number } {
const t = famStore.templateMap().get(templateId)
if (!t) return { frequency: 'daily', type: 'points', value: 10 }
@@ -65,25 +92,16 @@
if (!tid) return
draggedTemplateId = null
const s = page.data.session as any
if (!s) return
const def = getDefault(tid)
const fd = new FormData()
fd.set('memberId', memberId)
fd.set('templateId', tid)
fd.set('frequency', def.frequency)
fd.set('type', def.type)
fd.set('value', String(def.value))
const res = await fetch(`/${page.params.fam}/${page.params.username}/chores?/assignChore`, {
const body: Record<string, unknown> = { memberId, templateId: tid, frequency: def.frequency, type: def.type, value: def.value }
if (seasonFilter !== 'all') body.seasonIds = [seasonFilter]
await fetch(`/api/admin/${s.famId}/assigned-chores`, {
method: 'POST',
body: fd,
headers: { 'Content-Type': 'application/json', 'x-session-famid': s.famId, 'x-session-userid': s.userId },
body: JSON.stringify(body),
})
if (res.ok) {
const body = await res.json()
if (body.record) {
assigned = [body.record as AssignedChore, ...assigned]
famStore.applyRecord('assigned_chores', body.record, 'create')
}
}
}
function openEdit(a: AssignedChore) {
@@ -92,6 +110,7 @@
editType = a.type
editValue = a.value
editCustomName = a.customName || ''
editSeasonIds = a.seasonIds ? [...a.seasonIds] : []
showEditModal = true
}
@@ -138,6 +157,7 @@
type: editType,
value: editValue,
customName: editCustomName || undefined,
seasonIds: editSeasonIds.length > 0 ? editSeasonIds : [],
} as AssignedChore
famStore.applyRecord('assigned_chores', assigned[idx], 'update')
}
@@ -179,6 +199,16 @@
<p class="error">{form.error}</p>
{/if}
<div class="season-filter">
<label>Season:</label>
<select bind:value={seasonFilter}>
<option value="all">All Chores</option>
{#each seasons as s}
<option value={s.id}>{s.name}</option>
{/each}
</select>
</div>
<CardGrid>
<Card cols={3}>
<div style="margin-bottom:0.75rem">
@@ -233,12 +263,28 @@
<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}</div>
<form method="POST" action="?/unassignChore" onclick={(e) => e.stopPropagation()} use:enhance={() => { return async ({ result }) => { if (result.type === 'success') { const id = result.data.record?.id; if (id) { assigned = assigned.filter((x) => x.id !== id) as AssignedChore[]; famStore.applyRecord('assigned_chores', { id }, 'delete'); } } }; }}>
<input type="hidden" name="id" value={a.id} />
<button type="submit" class="del-btn" title="Remove assignment">×</button>
</form>
<div class="card-meta">
<span class="season-tags">{seasonNames(a)}</span>
</div>
<button
class="del-btn"
title="Remove assignment"
onclick={async (e) => {
e.stopPropagation();
const s = page.data.session as any;
if (!s) return;
const res = await fetch(`/api/admin/${s.famId}/assigned-chores/${a.id}`, {
method: 'DELETE',
headers: { 'x-session-famid': s.famId, 'x-session-userid': s.userId },
});
if (res.ok) famStore.applyRecord('assigned_chores', { id: a.id }, 'delete');
}}
>×</button>
</div>
{/each}
{#if assignedForMember(m.id).length === 0}
@@ -333,6 +379,7 @@
<h3>Edit: {editingAssignment.customName || templateName(editingAssignment.templateId)}</h3>
<form method="POST" action="?/updateAssignedChore" use:enhance={enhanceUpdateAssigned}>
<input name="id" type="hidden" value={editingAssignment.id} />
<input name="seasonIds" type="hidden" value={editSeasonIds.join(',')} />
<label>
Custom Name
<input name="customName" bind:value={editCustomName} placeholder="Override template name" />
@@ -351,11 +398,33 @@
<option value="money">Money</option>
</select>
</label>
<label>
Value
<input name="value" type="number" step="any" bind:value={editValue} required />
</label>
<div class="modal-actions">
<label>
Value
<input name="value" type="number" step="any" bind:value={editValue} required />
</label>
<label>
Seasons
<div class="season-checklist">
{#each seasons as s}
<label class="check-item">
<input type="checkbox" checked={editSeasonIds.includes(s.id)} onchange={() => {
if (editSeasonIds.includes(s.id)) {
editSeasonIds = editSeasonIds.filter(x => x !== s.id);
} else {
editSeasonIds = [...editSeasonIds, s.id];
}
}} />
<span class="dot" style="background:{s.color}"></span>
{s.name}
</label>
{/each}
<label class="check-item">
<input type="checkbox" checked={editSeasonIds.length === 0} onchange={() => editSeasonIds = []} />
Global (no season)
</label>
</div>
</label>
<div class="modal-actions">
<button type="button" onclick={closeEdit}>Cancel</button>
<button type="submit">Save</button>
</div>
@@ -384,6 +453,15 @@
.edit-btn:hover { color: #6366f1; }
.del-btn:hover { color: #dc2626; }
.hint { color: #6b7280; font-size: 0.85rem; margin-bottom: 1rem; }
.season-filter { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem; }
.season-filter label { font-size: 0.85rem; color: #374151; font-weight: 500; }
.season-filter select { padding: 0.35rem 0.6rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.85rem; }
.badge.season { background: #e0f2fe; color: #0369a1; }
.card-meta { font-size: 0.7rem; color: #9ca3af; flex-shrink: 0; }
.season-tags { font-size: 0.7rem; color: #9ca3af; }
.season-checklist { display: flex; flex-direction: column; gap: 0.3rem; margin-top: 0.25rem; }
.check-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.9rem; cursor: pointer; }
.check-item input[type="checkbox"] { width: auto; margin: 0; }
.template { cursor: grab; border-left: 3px solid #6366f1; }
.template:active { cursor: grabbing; opacity: 0.8; }