update ui and other frontend updates
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user