221 lines
6.2 KiB
TypeScript
221 lines
6.2 KiB
TypeScript
import { createFileRoute } from '@tanstack/react-router'
|
||
import {
|
||
Title,
|
||
Text,
|
||
Card,
|
||
Stack,
|
||
Badge,
|
||
Group,
|
||
Loader,
|
||
Center,
|
||
SimpleGrid,
|
||
ThemeIcon,
|
||
} from '@pikku/mantine/core'
|
||
import { MapPin, Clock, CheckSquare, Smile } from 'lucide-react'
|
||
import { usePikkuQuery } from '@perauset/functions-sdk/pikku/api.gen'
|
||
import { TaskActions } from '@/components/task-actions'
|
||
import { m, mKey } from '@/i18n/messages'
|
||
import { useLocale } from '@/i18n/config'
|
||
import { asI18n, type I18nNode } from '@pikku/react'
|
||
|
||
export const Route = createFileRoute('/_authenticated/my/tasks')({
|
||
component: MyTasksPage,
|
||
})
|
||
|
||
const STATUS_CONFIG: Record<string, { labelKey: string; color: string }> = {
|
||
in_progress: { labelKey: 'my.tasks_in_progress', color: 'orange' },
|
||
assigned: { labelKey: 'my.tasks_assigned', color: 'blue' },
|
||
open: { labelKey: 'my.tasks_open', color: 'teal' },
|
||
blocked: { labelKey: 'my.tasks_blocked', color: 'red' },
|
||
}
|
||
|
||
const CATEGORY_COLORS: Record<string, string> = {
|
||
housekeeping: 'pink',
|
||
kitchen: 'yellow',
|
||
maintenance: 'orange',
|
||
operations: 'blue',
|
||
retreat: 'violet',
|
||
transport: 'cyan',
|
||
}
|
||
|
||
const DISPLAY_STATUSES = ['in_progress', 'assigned', 'open', 'blocked']
|
||
|
||
interface TaskItem {
|
||
taskId: string
|
||
templateId: string | null
|
||
title: string
|
||
description: string | null
|
||
category: string
|
||
status: string
|
||
location: string | null
|
||
scheduledStartAt: string | Date | null
|
||
scheduledEndAt: string | Date | null
|
||
createdByUserId: string
|
||
}
|
||
|
||
function formatTime(val: string | Date | null): string | null {
|
||
if (!val) return null
|
||
const d = typeof val === 'string' ? new Date(val) : val
|
||
return d.toLocaleDateString('en-US', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
hour: 'numeric',
|
||
minute: '2-digit',
|
||
})
|
||
}
|
||
|
||
const cardStyle = {
|
||
backgroundColor: '#ffffff',
|
||
boxShadow: '0 2px 12px rgba(61, 43, 31, 0.08), 0 1px 4px rgba(61, 43, 31, 0.04)',
|
||
maxWidth: 'calc(40rem * var(--mantine-scale))',
|
||
}
|
||
|
||
function TaskCard({ task }: { task: TaskItem }) {
|
||
const cfg = STATUS_CONFIG[task.status]
|
||
|
||
return (
|
||
<Card padding="lg" radius="md" style={cardStyle}>
|
||
<Stack gap="sm">
|
||
<Group justify="space-between" align="flex-start" wrap="nowrap">
|
||
<Text
|
||
fw={600}
|
||
lineClamp={2}
|
||
style={{ fontFamily: '"Cormorant", serif', fontSize: 18, color: '#3D2B1F' }}
|
||
>
|
||
{asI18n(task.title)}
|
||
</Text>
|
||
<Badge size="sm" color={cfg?.color ?? 'gray'} variant="light" style={{ flexShrink: 0 }}>
|
||
{cfg ? mKey(cfg.labelKey) : asI18n(task.status)}
|
||
</Badge>
|
||
</Group>
|
||
|
||
<Group gap="xs">
|
||
<Badge size="sm" variant="dot" color={CATEGORY_COLORS[task.category] ?? 'gray'}>
|
||
{asI18n(task.category)}
|
||
</Badge>
|
||
</Group>
|
||
|
||
{task.description && (
|
||
<Text size="sm" c="dimmed" lineClamp={2}>
|
||
{asI18n(task.description)}
|
||
</Text>
|
||
)}
|
||
|
||
{task.location && (
|
||
<Group gap={6}>
|
||
<ThemeIcon size="sm" variant="transparent" color="gray">
|
||
<MapPin size={14} />
|
||
</ThemeIcon>
|
||
<Text size="sm" c="dimmed">
|
||
{asI18n(task.location)}
|
||
</Text>
|
||
</Group>
|
||
)}
|
||
|
||
{task.scheduledStartAt && (
|
||
<Group gap={6}>
|
||
<ThemeIcon size="sm" variant="transparent" color="gray">
|
||
<Clock size={14} />
|
||
</ThemeIcon>
|
||
<Text size="sm" c="dimmed">
|
||
{asI18n(
|
||
`${formatTime(task.scheduledStartAt)}${
|
||
task.scheduledEndAt ? ` – ${formatTime(task.scheduledEndAt)}` : ''
|
||
}`,
|
||
)}
|
||
</Text>
|
||
</Group>
|
||
)}
|
||
|
||
<TaskActions taskId={task.taskId} status={task.status} />
|
||
</Stack>
|
||
</Card>
|
||
)
|
||
}
|
||
|
||
function EmptyState({ message }: { message: I18nNode }) {
|
||
return (
|
||
<Card padding="xl" radius="md" style={{ ...cardStyle, textAlign: 'center' as const }}>
|
||
<Stack align="center" gap="sm">
|
||
<ThemeIcon size={48} variant="light" color="gray" radius="xl">
|
||
<Smile size={24} />
|
||
</ThemeIcon>
|
||
<Text c="dimmed" size="sm">
|
||
{message}
|
||
</Text>
|
||
</Stack>
|
||
</Card>
|
||
)
|
||
}
|
||
|
||
function MyTasksPage() {
|
||
useLocale()
|
||
const { data, isLoading } = usePikkuQuery('listTasks', { limit: 50, offset: 0 })
|
||
|
||
const tasks = (data?.items ?? []) as TaskItem[]
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<Center py="xl">
|
||
<Loader color="teal" />
|
||
</Center>
|
||
)
|
||
}
|
||
|
||
const grouped: Record<string, TaskItem[]> = {}
|
||
for (const status of DISPLAY_STATUSES) {
|
||
grouped[status] = []
|
||
}
|
||
for (const task of tasks) {
|
||
if (DISPLAY_STATUSES.includes(task.status)) {
|
||
grouped[task.status]!.push(task)
|
||
}
|
||
}
|
||
|
||
const hasActiveTasks = DISPLAY_STATUSES.some((s) => (grouped[s]?.length ?? 0) > 0)
|
||
|
||
return (
|
||
<Stack gap="lg">
|
||
<div>
|
||
<Title order={2} style={{ fontFamily: '"Cormorant", serif', color: '#3D2B1F' }}>
|
||
{m.my_tasks_title()}
|
||
</Title>
|
||
<Text size="sm" c="dimmed" mt={4}>
|
||
{m.my_tasks_description()}
|
||
</Text>
|
||
</div>
|
||
|
||
{!hasActiveTasks ? (
|
||
<EmptyState message={m.my_no_active_tasks()} />
|
||
) : (
|
||
DISPLAY_STATUSES.map((status) => {
|
||
const items = grouped[status]!
|
||
if (items.length === 0) return null
|
||
const cfg = STATUS_CONFIG[status]!
|
||
|
||
return (
|
||
<div key={status}>
|
||
<Group gap="xs" mb="md">
|
||
<ThemeIcon size="md" variant="light" color={cfg.color} radius="xl">
|
||
<CheckSquare size={16} />
|
||
</ThemeIcon>
|
||
<Title order={4} style={{ fontFamily: '"Cormorant", serif', color: '#3D2B1F' }}>
|
||
{mKey(cfg.labelKey)}
|
||
</Title>
|
||
<Badge size="sm" color={cfg.color} variant="light">
|
||
{items.length}
|
||
</Badge>
|
||
</Group>
|
||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
|
||
{items.map((task) => (
|
||
<TaskCard key={task.taskId} task={task} />
|
||
))}
|
||
</SimpleGrid>
|
||
</div>
|
||
)
|
||
})
|
||
)}
|
||
</Stack>
|
||
)
|
||
}
|