update ui and other frontend updates

This commit is contained in:
JCEEE
2026-07-29 09:41:52 +01:00
parent 04b83925e0
commit 57ef30d3a7
79 changed files with 7323 additions and 429 deletions
@@ -0,0 +1,402 @@
<script lang="ts">
import { enhance } from '$app/forms';
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';
let { data, form } = $props();
let showCreateModal = $state(false)
let showEditModal = $state(false)
let showEditTemplateModal = $state(false)
let editingAssignment = $state<AssignedChore | null>(null)
let editingTemplate = $state<ChoreTemplate | null>(null)
let editMemberId = $state('')
let draggedTemplateId = $state<string | null>(null)
let createName = $state('')
let createFreq = $state('daily')
let createType = $state('points')
let createValue = $state(10)
let editFreq = $state('daily')
let editType = $state('points')
let editValue = $state(10)
let editCustomName = $state('')
let editTplName = $state('')
let editTplFreq = $state('daily')
let editTplType = $state('points')
let editTplValue = $state(10)
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[] || []))
function assignedForMember(memberId: string): AssignedChore[] {
return assigned.filter(a => a.memberId === memberId)
}
function templateName(templateId: string): string {
return famStore.templateMap().get(templateId)?.name || '?'
}
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 }
return { frequency: t.defaultFrequency, type: t.defaultType, value: t.defaultValue }
}
function handleDragStart(e: DragEvent, templateId: string) {
draggedTemplateId = templateId
e.dataTransfer?.setData('text/plain', templateId)
if (e.dataTransfer) e.dataTransfer.effectAllowed = 'copy'
}
function handleDragOver(e: DragEvent) {
e.preventDefault()
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'
}
async function handleDrop(e: DragEvent, memberId: string) {
e.preventDefault()
const tid = e.dataTransfer?.getData('text/plain') || draggedTemplateId
if (!tid) return
draggedTemplateId = null
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`, {
method: 'POST',
body: fd,
})
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) {
editingAssignment = a
editFreq = a.frequency
editType = a.type
editValue = a.value
editCustomName = a.customName || ''
showEditModal = true
}
function closeEdit() {
showEditModal = false
editingAssignment = null
}
function openEditTemplate(t: ChoreTemplate) {
editingTemplate = t
editTplName = t.name
editTplFreq = t.defaultFrequency
editTplType = t.defaultType
editTplValue = t.defaultValue
showEditTemplateModal = true
}
function closeEditTemplate() {
showEditTemplateModal = false
editingTemplate = null
}
function enhanceCreate() {
return async ({ result, formData }: any) => {
if (result.type === 'success') {
showCreateModal = false
const rec = result.data?.record
if (rec) {
templates = [rec as ChoreTemplate, ...templates]
famStore.applyRecord('chore_templates', rec, 'create')
}
}
}
}
function enhanceUpdateAssigned() {
return async ({ result }: any) => {
if (result.type === 'success' && editingAssignment) {
const idx = assigned.findIndex((a: any) => a.id === editingAssignment!.id)
if (idx !== -1) {
assigned[idx] = {
...assigned[idx],
frequency: editFreq,
type: editType,
value: editValue,
customName: editCustomName || undefined,
} as AssignedChore
famStore.applyRecord('assigned_chores', assigned[idx], 'update')
}
}
closeEdit()
}
}
function enhanceUpdateTemplate() {
return async ({ result, formData }: any) => {
if (result.type === 'success' && editingTemplate) {
const tid = editingTemplate!.id
const idx = templates.findIndex((t: any) => t.id === tid)
if (idx !== -1) {
templates[idx] = {
...templates[idx],
name: editTplName,
defaultFrequency: editTplFreq,
defaultType: editTplType,
defaultValue: editTplValue,
} as ChoreTemplate
}
assigned = assigned.map((a: any) =>
a.templateId === tid
? { ...a, frequency: editTplFreq, type: editTplType, value: editTplValue }
: a
) as AssignedChore[]
famStore.applyRecord('chore_templates', templates[idx], 'update')
closeEditTemplate()
}
}
}
</script>
<ViewHeader title="Chore Assignment" />
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<CardGrid>
<Card cols={3}>
<div style="margin-bottom:0.75rem">
<Button onclick={() => showCreateModal = true}>+ New Chore Template</Button>
</div>
<div class="kanban">
<div class="column templates-col">
<h3>Templates</h3>
{#each templates as t}
<div
class="card template"
draggable="true"
ondragstart={(e) => handleDragStart(e, t.id)}
>
<div class="card-body" onclick={() => openEditTemplate(t)} role="button" tabindex="0">
<strong>{t.name}</strong>
<span class="badge">{t.defaultFrequency}</span>
<span class="badge type">{t.defaultType}</span>
</div>
<div class="card-value">
{t.defaultType === 'money' ? `£${Number(t.defaultValue).toFixed(2)}` : t.defaultValue}
</div>
<div class="card-actions">
<button onclick={() => openEditTemplate(t)} class="edit-btn" title="Edit template">✎</button>
<form method="POST" action="?/deleteTemplate" use:enhance={() => { return async ({ result, formData }) => { if (result.type === 'success') { const id = formData.get('id'); templates = templates.filter((t) => t.id !== id) as ChoreTemplate[]; const rec = result.data.record; if (rec) famStore.applyRecord('chore_templates', { ...rec, id }, 'delete'); } }; }}>
<input type="hidden" name="id" value={t.id} />
<button type="submit" class="del-btn" title="Delete template">×</button>
</form>
</div>
</div>
{/each}
{#if templates.length === 0}
<p class="empty">No templates yet</p>
{/if}
</div>
{#each members as m}
<div
class="column member-col"
style="border-top-color: {m.color}"
ondragover={handleDragOver}
ondrop={(e) => handleDrop(e, m.id)}
>
<h3>
<span class="dot" style="background:{m.color}"></span>
{m.name}
</h3>
{#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>
</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>
{/each}
{#if assignedForMember(m.id).length === 0}
<p class="empty">Drop a chore here</p>
{/if}
</div>
{/each}
</div>
</Card></CardGrid>
<!-- Create Template Modal -->
{#if showCreateModal}
<div class="overlay" onclick={() => showCreateModal = false} role="presentation">
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
<h3>New Chore Template</h3>
<form method="POST" action="?/createTemplate" use:enhance={enhanceCreate}>
<label>
Name
<input name="name" bind:value={createName} required />
</label>
<label>
Frequency
<select name="defaultFrequency" bind:value={createFreq}>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
</select>
</label>
<label>
Type
<select name="defaultType" bind:value={createType}>
<option value="points">Points</option>
<option value="money">Money</option>
</select>
</label>
<label>
Default Value
<input name="defaultValue" type="number" step="any" bind:value={createValue} required />
</label>
<div class="modal-actions">
<button type="button" onclick={() => showCreateModal = false}>Cancel</button>
<button type="submit">Create</button>
</div>
</form>
</div>
</div>
{/if}
<!-- Edit Template Modal -->
{#if showEditTemplateModal && editingTemplate}
<div class="overlay" onclick={closeEditTemplate} role="presentation">
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
<h3>Edit Template: {editingTemplate.name}</h3>
<p class="hint">Changes will also update all existing assigned chores using this template.</p>
<form method="POST" action="?/updateTemplate" use:enhance={enhanceUpdateTemplate}>
<input name="id" type="hidden" value={editingTemplate.id} />
<label>
Name
<input name="name" bind:value={editTplName} required />
</label>
<label>
Frequency
<select name="defaultFrequency" bind:value={editTplFreq}>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
</select>
</label>
<label>
Type
<select name="defaultType" bind:value={editTplType}>
<option value="points">Points</option>
<option value="money">Money</option>
</select>
</label>
<label>
Default Value
<input name="defaultValue" type="number" step="any" bind:value={editTplValue} required />
</label>
<div class="modal-actions">
<button type="button" onclick={closeEditTemplate}>Cancel</button>
<button type="submit">Save</button>
</div>
</form>
</div>
</div>
{/if}
<!-- Edit Assignment Modal -->
{#if showEditModal && editingAssignment}
<div class="overlay" onclick={closeEdit} role="presentation">
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
<h3>Edit: {editingAssignment.customName || templateName(editingAssignment.templateId)}</h3>
<form method="POST" action="?/updateAssignedChore" use:enhance={enhanceUpdateAssigned}>
<input name="id" type="hidden" value={editingAssignment.id} />
<label>
Custom Name
<input name="customName" bind:value={editCustomName} placeholder="Override template name" />
</label>
<label>
Frequency
<select name="frequency" bind:value={editFreq}>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
</select>
</label>
<label>
Type
<select name="type" bind:value={editType}>
<option value="points">Points</option>
<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">
<button type="button" onclick={closeEdit}>Cancel</button>
<button type="submit">Save</button>
</div>
</form>
</div>
</div>
{/if}
<style>
.error { color: #dc2626; padding: 0.5rem; background: #fef2f2; border-radius: 4px; margin: 0.5rem 0; }
.kanban { display: flex; gap: 1rem; overflow-x: auto; align-items: flex-start; }
.column { flex: 1; min-width: 200px; max-width: 280px; border: 2px solid #e5e7eb; border-top-width: 4px; border-radius: 8px; padding: 0.75rem; background: #fafafa; min-height: 200px; }
.column h3 { margin: 0 0 0.75rem; font-size: 1rem; display: flex; align-items: center; gap: 0.4rem; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.empty { color: #9ca3af; font-size: 0.85rem; text-align: center; padding: 1rem; border: 1px dashed #d1d5db; border-radius: 6px; }
.card { background: white; border: 1px solid #e5e7eb; border-radius: 6px; padding: 0.5rem 0.75rem; margin-bottom: 0.5rem; display: flex; align-items: center; gap: 0.5rem; position: relative; }
.card-body { flex: 1; min-width: 0; }
.card-body strong { display: block; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.card-value { font-size: 0.85rem; color: #6b7280; font-weight: 600; white-space: nowrap; }
.badge { font-size: 0.7rem; background: #e0e7ff; color: #4338ca; padding: 1px 6px; border-radius: 10px; margin-right: 4px; }
.badge.type { background: #fce7f3; color: #be185d; }
.card-actions { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }
.edit-btn, .del-btn { background: none; border: none; color: #9ca3af; cursor: pointer; font-size: 1rem; padding: 0 4px; line-height: 1; flex-shrink: 0; }
.edit-btn:hover { color: #6366f1; }
.del-btn:hover { color: #dc2626; }
.hint { color: #6b7280; font-size: 0.85rem; margin-bottom: 1rem; }
.template { cursor: grab; border-left: 3px solid #6366f1; }
.template:active { cursor: grabbing; opacity: 0.8; }
.assigned { cursor: pointer; border-left: 3px solid #10b981; }
.assigned:hover { background: #f3f4f6; }
.overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: flex; align-items: center; justify-content: center; z-index: 100; }
.modal { background: white; border-radius: 12px; padding: 1.5rem; min-width: 320px; max-width: 440px; box-shadow: 0 10px 25px rgba(0,0,0,0.15); }
.modal h3 { margin: 0 0 1rem; }
.modal label { display: block; margin-bottom: 0.75rem; font-size: 0.9rem; color: #374151; }
.modal input, .modal select { display: block; width: 100%; margin-top: 0.25rem; padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.9rem; box-sizing: border-box; }
.modal-actions { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 1rem; }
.modal-actions button { padding: 0.5rem 1rem; border: none; border-radius: 6px; cursor: pointer; font-size: 0.9rem; }
.modal-actions button[type="button"] { background: #e5e7eb; color: #374151; }
.modal-actions button[type="submit"] { background: #6366f1; color: white; }
</style>