Files
perauset-e2e-mrfzptpr/apps/app/src/pages/_authenticated.my.tasks.tsx
2026-07-11 08:35:17 +02:00

221 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
)
}