230 lines
8.1 KiB
Svelte
230 lines
8.1 KiB
Svelte
<script lang="ts">
|
|
import { enhance } from '$app/forms';
|
|
import { resolve } from '$app/paths';
|
|
import type { ActionData, PageData } from './$types';
|
|
import { m } from '$lib/paraglide/messages.js';
|
|
import { Badge } from '$lib/components/ui/badge';
|
|
import { Button } from '$lib/components/ui/button';
|
|
import { Card, CardContent } from '$lib/components/ui/card';
|
|
import { Input } from '$lib/components/ui/input';
|
|
import { Label } from '$lib/components/ui/label';
|
|
import { Separator } from '$lib/components/ui/separator';
|
|
import { ArrowLeft } from 'lucide-svelte';
|
|
import type { GroomingAction, GroomingSchedule } from '$lib/types';
|
|
|
|
let { data, form }: { data: PageData; form: ActionData } = $props();
|
|
let { schedule } = $derived(data);
|
|
|
|
let showAddForm = $state(false);
|
|
let editingId = $state<string | null>(null);
|
|
|
|
const DAY_NAMES = $derived([
|
|
m["grooming_dayMonday"](),
|
|
m["grooming_dayTuesday"](),
|
|
m["grooming_dayWednesday"](),
|
|
m["grooming_dayThursday"](),
|
|
m["grooming_dayFriday"](),
|
|
m["grooming_daySaturday"](),
|
|
m["grooming_daySunday"]()
|
|
]);
|
|
|
|
const ACTION_LABELS = $derived({
|
|
shaving_razor: m["grooming_actionShavingRazor"](),
|
|
shaving_oneblade: m["grooming_actionShavingOneblade"](),
|
|
dermarolling: m["grooming_actionDermarolling"]()
|
|
} as Record<GroomingAction, string>);
|
|
|
|
const ALL_ACTIONS: GroomingAction[] = ['shaving_razor', 'shaving_oneblade', 'dermarolling'];
|
|
|
|
const byDay = $derived(
|
|
DAY_NAMES.map((name, idx) => ({
|
|
name,
|
|
day: idx,
|
|
entries: schedule.filter((e: GroomingSchedule) => e.day_of_week === idx)
|
|
})).filter((d) => d.entries.length > 0)
|
|
);
|
|
</script>
|
|
|
|
<svelte:head><title>{m.grooming_title()} — innercontext</title></svelte:head>
|
|
|
|
<div class="editorial-page space-y-4">
|
|
<section class="editorial-hero reveal-1 space-y-3">
|
|
<a href={resolve('/routines')} class="editorial-backlink"><ArrowLeft class="size-4" /> {m["grooming_backToRoutines"]()}</a>
|
|
<p class="editorial-kicker">{m["nav_appSubtitle"]()}</p>
|
|
<h2 class="editorial-title text-[clamp(1.8rem,3vw,2.4rem)]">{m.grooming_title()}</h2>
|
|
<div class="editorial-toolbar">
|
|
<Button variant="outline" size="sm" onclick={() => (showAddForm = !showAddForm)}>
|
|
{showAddForm ? m.common_cancel() : m["grooming_addEntry"]()}
|
|
</Button>
|
|
</div>
|
|
</section>
|
|
|
|
{#if form?.error}
|
|
<div class="editorial-alert editorial-alert--error">{form.error}</div>
|
|
{/if}
|
|
{#if form?.created}
|
|
<div class="editorial-alert editorial-alert--success">{m["grooming_entryAdded"]()}</div>
|
|
{/if}
|
|
{#if form?.updated}
|
|
<div class="editorial-alert editorial-alert--success">{m["grooming_entryUpdated"]()}</div>
|
|
{/if}
|
|
{#if form?.deleted}
|
|
<div class="editorial-alert editorial-alert--success">{m["grooming_entryDeleted"]()}</div>
|
|
{/if}
|
|
|
|
<!-- Add form -->
|
|
{#if showAddForm}
|
|
<Card>
|
|
<CardContent class="pt-4">
|
|
<form
|
|
method="POST"
|
|
action="?/create"
|
|
use:enhance={() => {
|
|
return async ({ update }) => {
|
|
await update();
|
|
showAddForm = false;
|
|
};
|
|
}}
|
|
class="grid grid-cols-2 gap-4"
|
|
>
|
|
<div class="space-y-1">
|
|
<Label for="add_day">{m["grooming_dayOfWeek"]()}</Label>
|
|
<select
|
|
id="add_day"
|
|
name="day_of_week"
|
|
required
|
|
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
>
|
|
{#each DAY_NAMES as name, idx (idx)}
|
|
<option value={idx}>{name}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<Label for="add_action">{m.grooming_action()}</Label>
|
|
<select
|
|
id="add_action"
|
|
name="action"
|
|
required
|
|
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
>
|
|
{#each ALL_ACTIONS as action (action)}
|
|
<option value={action}>{ACTION_LABELS[action]}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
<div class="col-span-2 space-y-1">
|
|
<Label for="add_notes">{m["grooming_notesOptional"]()}</Label>
|
|
<Input id="add_notes" name="notes" placeholder={m["grooming_notesPlaceholder"]()} />
|
|
</div>
|
|
<div class="col-span-2 flex gap-2">
|
|
<Button type="submit" size="sm">{m.common_add()}</Button>
|
|
<Button type="button" variant="ghost" size="sm" onclick={() => (showAddForm = false)}>
|
|
{m.common_cancel()}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
{/if}
|
|
|
|
<!-- Entries grouped by day -->
|
|
{#if schedule.length === 0}
|
|
<p class="text-sm text-muted-foreground">{m["grooming_noEntries"]()}</p>
|
|
{:else}
|
|
<div class="space-y-4">
|
|
{#each byDay as { name, entries, day } (day)}
|
|
<div class="space-y-2">
|
|
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">{name}</h3>
|
|
{#each entries as entry (entry.id)}
|
|
<div class="rounded-md border border-border text-sm">
|
|
<!-- Entry row -->
|
|
<div class="flex items-center justify-between px-4 py-3">
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<Badge variant="secondary">{ACTION_LABELS[entry.action]}</Badge>
|
|
{#if entry.notes}
|
|
<span class="text-muted-foreground">{entry.notes}</span>
|
|
{/if}
|
|
</div>
|
|
<div class="flex items-center gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onclick={() => (editingId = editingId === entry.id ? null : entry.id)}
|
|
>
|
|
{editingId === entry.id ? m.common_cancel() : m.common_edit()}
|
|
</Button>
|
|
<form
|
|
method="POST"
|
|
action="?/delete"
|
|
use:enhance
|
|
onsubmit={(e) => {
|
|
if (!confirm(m["grooming_confirmDelete"]())) e.preventDefault();
|
|
}}
|
|
>
|
|
<input type="hidden" name="id" value={entry.id} />
|
|
<Button variant="ghost" size="sm" type="submit" class="text-destructive hover:text-destructive">
|
|
{m.common_delete()}
|
|
</Button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Inline edit form -->
|
|
{#if editingId === entry.id}
|
|
<div class="border-t border-border bg-muted/30 px-4 py-3">
|
|
<form
|
|
method="POST"
|
|
action="?/update"
|
|
use:enhance={() => {
|
|
return async ({ update }) => {
|
|
await update();
|
|
editingId = null;
|
|
};
|
|
}}
|
|
class="grid grid-cols-2 gap-3"
|
|
>
|
|
<input type="hidden" name="id" value={entry.id} />
|
|
<div class="space-y-1">
|
|
<Label>{m["grooming_dayOfWeek"]()}</Label>
|
|
<select
|
|
name="day_of_week"
|
|
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
>
|
|
{#each DAY_NAMES as dayName, idx (idx)}
|
|
<option value={idx} selected={entry.day_of_week === idx}>{dayName}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<Label>{m.grooming_action()}</Label>
|
|
<select
|
|
name="action"
|
|
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
>
|
|
{#each ALL_ACTIONS as a (a)}
|
|
<option value={a} selected={entry.action === a}>{ACTION_LABELS[a]}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
<div class="col-span-2 space-y-1">
|
|
<Label>{m.inventory_notes()}</Label>
|
|
<Input name="notes" value={entry.notes ?? ''} placeholder={m["common_optional_notes"]()} />
|
|
</div>
|
|
<div class="col-span-2 flex gap-2">
|
|
<Button type="submit" size="sm">{m.common_save()}</Button>
|
|
<Button type="button" variant="ghost" size="sm" onclick={() => (editingId = null)}>
|
|
{m.common_cancel()}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
<Separator />
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|