chore: perauset customer project
This commit is contained in:
220
apps/app/src/pages/_authenticated.my.tasks.tsx
Normal file
220
apps/app/src/pages/_authenticated.my.tasks.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user