181 lines
6.2 KiB
TypeScript
181 lines
6.2 KiB
TypeScript
import { createFileRoute, Link } from '@tanstack/react-router'
|
|
import { Title, Text, Card, Group, Stack, Badge, Table, Anchor, Box } from '@pikku/mantine/core'
|
|
import { LayoutTemplate } from 'lucide-react'
|
|
import { asI18n } from '@pikku/react'
|
|
import { useLocale } from '@/i18n/config'
|
|
import { m } from '@/i18n/messages'
|
|
import { CreateTemplateForm } from '@/components/create-template-form'
|
|
|
|
export const Route = createFileRoute('/_authenticated/tasks/templates')({
|
|
component: TaskTemplatesPage,
|
|
})
|
|
|
|
const CATEGORY_COLORS: Record<string, string> = {
|
|
housekeeping: 'pink',
|
|
kitchen: 'yellow',
|
|
maintenance: 'orange',
|
|
operations: 'blue',
|
|
retreat: 'violet',
|
|
transport: 'cyan',
|
|
}
|
|
|
|
interface TaskTemplate {
|
|
templateId: string
|
|
title: string
|
|
description: string | null
|
|
category: string
|
|
isClaimable: boolean
|
|
defaultLocation: string | null
|
|
estimatedMinutes: number | null
|
|
recurrenceCron: string | null
|
|
}
|
|
|
|
function TaskTemplatesPage() {
|
|
useLocale()
|
|
// There is no list-templates RPC; new templates are created via the form below.
|
|
const templates: TaskTemplate[] = []
|
|
const error: string | null = null
|
|
|
|
return (
|
|
<Stack gap="lg">
|
|
<Group justify="space-between" align="flex-end">
|
|
<div>
|
|
<Anchor
|
|
component={Link}
|
|
to="/tasks"
|
|
size="sm"
|
|
c="dimmed"
|
|
style={{ textDecoration: 'none', marginBottom: 8, display: 'block' }}
|
|
>
|
|
{m.tasks_templates_back()}
|
|
</Anchor>
|
|
<Title order={2} style={{ fontFamily: '"Cormorant", serif', color: '#3D2B1F' }}>
|
|
{m.tasks_templates_title()}
|
|
</Title>
|
|
<Text size="sm" c="dimmed" mt={4}>
|
|
{m.tasks_templates_description()}
|
|
</Text>
|
|
</div>
|
|
</Group>
|
|
|
|
{error && (
|
|
<Card padding="md" radius="md" style={{ backgroundColor: '#fff3f3' }}>
|
|
<Text size="sm" c="red">
|
|
{error}
|
|
</Text>
|
|
</Card>
|
|
)}
|
|
|
|
<CreateTemplateForm />
|
|
|
|
{templates.length === 0 && !error ? (
|
|
<Card
|
|
padding="xl"
|
|
radius="md"
|
|
style={{
|
|
backgroundColor: '#ffffff',
|
|
boxShadow: '0 2px 12px rgba(61, 43, 31, 0.08), 0 1px 4px rgba(61, 43, 31, 0.04)',
|
|
textAlign: 'center',
|
|
}}
|
|
>
|
|
<Stack align="center" gap="sm">
|
|
<LayoutTemplate size={40} color="#aaa" />
|
|
<Text size="sm" c="dimmed">
|
|
{m.tasks_templates_empty()}
|
|
</Text>
|
|
</Stack>
|
|
</Card>
|
|
) : (
|
|
<Card
|
|
padding={0}
|
|
radius="md"
|
|
style={{
|
|
backgroundColor: '#ffffff',
|
|
boxShadow: '0 2px 12px rgba(61, 43, 31, 0.08), 0 1px 4px rgba(61, 43, 31, 0.04)',
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
{/* Desktop table */}
|
|
<Box visibleFrom="sm">
|
|
<Table striped highlightOnHover>
|
|
<Table.Thead>
|
|
<Table.Tr>
|
|
<Table.Th>{m.tasks_templates_col_title()}</Table.Th>
|
|
<Table.Th>{m.tasks_templates_col_category()}</Table.Th>
|
|
<Table.Th>{m.tasks_templates_col_claimable()}</Table.Th>
|
|
<Table.Th>{m.tasks_templates_col_recurrence()}</Table.Th>
|
|
<Table.Th>{m.tasks_templates_col_duration()}</Table.Th>
|
|
</Table.Tr>
|
|
</Table.Thead>
|
|
<Table.Tbody>
|
|
{templates.map((t) => (
|
|
<Table.Tr key={t.templateId}>
|
|
<Table.Td>
|
|
<Text fw={500} size="sm">
|
|
{asI18n(t.title)}
|
|
</Text>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Badge size="sm" variant="light" color={CATEGORY_COLORS[t.category] ?? 'gray'}>
|
|
{asI18n(t.category)}
|
|
</Badge>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Badge size="sm" variant="light" color={t.isClaimable ? 'teal' : 'gray'}>
|
|
{t.isClaimable ? m.tasks_templates_yes() : m.tasks_templates_no()}
|
|
</Badge>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Text size="sm" c="dimmed">
|
|
{asI18n(t.recurrenceCron ?? m.tasks_templates_one_time())}
|
|
</Text>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Text size="sm" c="dimmed">
|
|
{asI18n(t.estimatedMinutes ? m.tasks_templates_minutes({ minutes: t.estimatedMinutes }) : '--')}
|
|
</Text>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
))}
|
|
</Table.Tbody>
|
|
</Table>
|
|
</Box>
|
|
|
|
{/* Mobile cards */}
|
|
<Box hiddenFrom="sm" p="md">
|
|
<Stack gap="sm">
|
|
{templates.map((t) => (
|
|
<Card key={t.templateId} padding="sm" radius="sm" withBorder>
|
|
<Group justify="space-between" mb={4}>
|
|
<Text fw={600} size="sm">
|
|
{asI18n(t.title)}
|
|
</Text>
|
|
<Badge size="xs" variant="light" color={CATEGORY_COLORS[t.category] ?? 'gray'}>
|
|
{asI18n(t.category)}
|
|
</Badge>
|
|
</Group>
|
|
<Group gap="xs">
|
|
<Badge size="xs" variant="light" color={t.isClaimable ? 'teal' : 'gray'}>
|
|
{t.isClaimable ? m.tasks_templates_claimable() : m.tasks_templates_not_claimable()}
|
|
</Badge>
|
|
{t.recurrenceCron && (
|
|
<Badge size="xs" variant="light" color="grape">
|
|
{m.tasks_templates_recurring()}
|
|
</Badge>
|
|
)}
|
|
{t.estimatedMinutes && (
|
|
<Text size="xs" c="dimmed">
|
|
{m.tasks_templates_minutes({ minutes: t.estimatedMinutes })}
|
|
</Text>
|
|
)}
|
|
</Group>
|
|
</Card>
|
|
))}
|
|
</Stack>
|
|
</Box>
|
|
</Card>
|
|
)}
|
|
</Stack>
|
|
)
|
|
}
|