chore: perauset customer project

This commit is contained in:
e2e
2026-07-11 08:35:17 +02:00
commit 37eea09bf0
293 changed files with 28412 additions and 0 deletions

View File

@@ -0,0 +1,220 @@
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>
)
}