279 lines
8.0 KiB
Svelte
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>
|