2026-01-26 11:30:40 -05:00

279 lines
8.0 KiB
Svelte

<script lang="ts">
import { slide } from 'svelte/transition';
import AreaSection from './AreaSection.svelte';
import type { SvelteSet } from 'svelte/reactivity';
interface TaskTemplate {
id: string;
description: string;
checklistDescription: string | null;
frequency?: string | null;
estimatedMinutes: number | null;
order: number;
}
interface AreaOrCategory {
id: string;
name: string;
order: number;
taskTemplates: TaskTemplate[];
}
interface Template {
id: string;
name: string;
description: string | null;
isActive: boolean;
areaTemplates?: AreaOrCategory[];
categoryTemplates?: AreaOrCategory[];
}
interface Props {
template: Template;
variant: 'service' | 'project';
expandedAreas: SvelteSet<string>;
editingTemplateName: string | null;
editingAreaName: string | null;
editingTaskId: string | null;
showNewAreaInput: string | null;
showNewTaskInput: string | null;
newAreaName: string;
newTaskDescription: string;
onToggleArea: (areaId: string) => void;
onStartEditTemplateName: () => void;
onCancelEditTemplateName: () => void;
onUpdateTemplateName: (name: string) => void;
onUpdateTemplateDescription: (description: string) => void;
onDeleteTemplate: () => void;
onStartEditAreaName: (areaId: string) => void;
onCancelEditAreaName: () => void;
onUpdateAreaName: (areaId: string, name: string) => void;
onDeleteArea: (areaId: string) => void;
onShowNewArea: () => void;
onHideNewArea: () => void;
onNewAreaNameChange: (name: string) => void;
onCreateArea: () => void;
onShowNewTask: (areaId: string) => void;
onHideNewTask: () => void;
onNewTaskDescriptionChange: (desc: string) => void;
onCreateTask: (areaId: string) => void;
onStartEditTask: (taskId: string) => void;
onCancelEditTask: () => void;
onUpdateTask: (
taskId: string,
updates: {
description: string;
checklistDescription: string;
frequency?: string;
estimatedMinutes: number | null;
}
) => void;
onDeleteTask: (taskId: string) => void;
onMoveAreaUp: (areaId: string) => void;
onMoveAreaDown: (areaId: string) => void;
onMoveTaskUp: (taskId: string, areaId: string) => void;
onMoveTaskDown: (taskId: string, areaId: string) => void;
}
let {
template,
variant,
expandedAreas,
editingTemplateName,
editingAreaName,
editingTaskId,
showNewAreaInput,
showNewTaskInput,
newAreaName,
newTaskDescription,
onToggleArea,
onStartEditTemplateName,
onCancelEditTemplateName,
onUpdateTemplateName,
onUpdateTemplateDescription,
onDeleteTemplate,
onStartEditAreaName,
onCancelEditAreaName,
onUpdateAreaName,
onDeleteArea,
onShowNewArea,
onHideNewArea,
onNewAreaNameChange,
onCreateArea,
onShowNewTask,
onHideNewTask,
onNewTaskDescriptionChange,
onCreateTask,
onStartEditTask,
onCancelEditTask,
onUpdateTask,
onDeleteTask,
onMoveAreaUp,
onMoveAreaDown,
onMoveTaskUp,
onMoveTaskDown
}: Props = $props();
let areas = $derived(
(template.areaTemplates ?? template.categoryTemplates ?? [])
.slice()
.sort((a, b) => a.order - b.order)
);
let isEditingName = $derived(editingTemplateName === template.id);
let isShowingNewArea = $derived(showNewAreaInput === template.id);
let areaLabel = $derived(variant === 'service' ? 'Area' : 'Category');
</script>
<!-- Template Header -->
<div class="border-b border-theme bg-theme-card px-6 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
{#if isEditingName}
<input
type="text"
value={template.name}
class="rounded-lg border border-theme bg-theme px-3 py-1 text-lg font-bold text-theme focus:border-secondary-500 focus:outline-none"
onkeydown={(e) => {
if (e.key === 'Enter') {
onUpdateTemplateName(e.currentTarget.value);
} else if (e.key === 'Escape') {
onCancelEditTemplateName();
}
}}
onblur={(e) => onUpdateTemplateName(e.currentTarget.value)}
/>
{:else}
<h2
class="cursor-pointer text-xl font-bold text-theme"
ondblclick={onStartEditTemplateName}
>
{template.name}
</h2>
{/if}
<span
class="rounded-full px-2 py-0.5 text-xs {template.isActive
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
: 'bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400'}"
>
{template.isActive ? 'Active' : 'Inactive'}
</span>
</div>
<button
onclick={onDeleteTemplate}
class="rounded-lg p-2 text-red-500 transition-colors hover:bg-red-50 dark:hover:bg-red-900/20"
aria-label="Delete template"
>
<svg class="h-5 w-5" style="fill: none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
</div>
<!-- Description -->
<div class="mt-3">
<textarea
placeholder="Add a description..."
rows="2"
class="placeholder-theme-muted w-full resize-none rounded-lg border border-theme bg-theme px-3 py-2 text-sm text-theme-secondary focus:border-secondary-500 focus:text-theme focus:outline-none"
onblur={(e) => {
const newDesc = e.currentTarget.value;
if (newDesc !== (template.description ?? '')) {
onUpdateTemplateDescription(newDesc);
}
}}>{template.description ?? ''}</textarea
>
</div>
</div>
<div class="flex-1 overflow-y-auto p-6">
<!-- Areas/Categories -->
{#each areas as area, index (area.id)}
<AreaSection
{area}
{variant}
isExpanded={expandedAreas.has(area.id)}
{editingAreaName}
{editingTaskId}
{showNewTaskInput}
{newTaskDescription}
onToggle={() => onToggleArea(area.id)}
onUpdateName={(name) => onUpdateAreaName(area.id, name)}
onDelete={() => onDeleteArea(area.id)}
onStartEditName={() => onStartEditAreaName(area.id)}
onCancelEditName={onCancelEditAreaName}
onShowNewTask={() => onShowNewTask(area.id)}
{onHideNewTask}
{onNewTaskDescriptionChange}
onCreateTask={() => onCreateTask(area.id)}
{onStartEditTask}
{onCancelEditTask}
{onUpdateTask}
{onDeleteTask}
onMoveUp={index === 0 ? undefined : () => onMoveAreaUp(area.id)}
onMoveDown={index === areas.length - 1 ? undefined : () => onMoveAreaDown(area.id)}
onMoveTaskUp={(taskId) => onMoveTaskUp(taskId, area.id)}
onMoveTaskDown={(taskId) => onMoveTaskDown(taskId, area.id)}
/>
{/each}
<!-- Add Area/Category Button / Input -->
{#if isShowingNewArea}
<div
class="rounded-lg border border-dashed border-theme bg-theme-card p-4"
transition:slide={{ duration: 150 }}
>
<input
type="text"
value={newAreaName}
oninput={(e) => onNewAreaNameChange(e.currentTarget.value)}
placeholder="{areaLabel} name..."
class="placeholder-theme-muted w-full rounded-lg border border-theme bg-theme px-3 py-2 text-sm text-theme focus:border-secondary-500 focus:outline-none"
onkeydown={(e) => {
if (e.key === 'Enter') {
onCreateArea();
} else if (e.key === 'Escape') {
onHideNewArea();
onNewAreaNameChange('');
}
}}
/>
<div class="mt-2 flex justify-end gap-2">
<button
onclick={() => {
onHideNewArea();
onNewAreaNameChange('');
}}
class="rounded px-3 py-1 text-sm text-theme-muted hover:text-theme"
>
Cancel
</button>
<button
onclick={onCreateArea}
class="rounded bg-secondary-500 px-3 py-1 text-sm text-white hover:bg-secondary-600"
>
Add {areaLabel}
</button>
</div>
</div>
{:else}
<button
onclick={onShowNewArea}
class="flex w-full items-center justify-center gap-2 rounded-lg border border-dashed border-theme p-4 text-sm text-theme-muted transition-colors hover:border-secondary-500 hover:bg-theme-card hover:text-secondary-500"
>
<svg class="h-4 w-4" style="fill: none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/>
</svg>
Add {areaLabel}
</button>
{/if}
</div>