158 lines
5.0 KiB
TypeScript
158 lines
5.0 KiB
TypeScript
import { Link, createFileRoute } from '@tanstack/react-router'
|
||
import { m } from '@/i18n/messages'
|
||
import { bookingStatus } from '@/i18n/i18n-enum.gen'
|
||
import { useLocale, getLocale } from '@/i18n/config'
|
||
import { asI18n } from '@pikku/react'
|
||
import { Box, Container, Group, Paper, Stack, Text } from '@pikku/mantine/core'
|
||
import { usePikkuQuery } from '@project/functions-sdk/pikku/api.gen'
|
||
import { usePageTitle } from '../components/AppLayout'
|
||
import { PublicShell } from '../components/PublicShell'
|
||
import { BOOKING_STATUS, type BookingStatus } from '../lib/status'
|
||
import { fmtDateShort } from '../lib/dates'
|
||
|
||
const COLUMNS: BookingStatus[] = ['enquiry', 'reserved', 'confirmed', 'ended', 'cancelled']
|
||
|
||
type Booking = {
|
||
bookingId: string
|
||
eventName: string
|
||
startDate: Date
|
||
endDate: Date
|
||
status: string
|
||
clientName: string
|
||
isStammgruppe: boolean
|
||
depositPaid: boolean
|
||
hasDeposit: boolean
|
||
halfHouse: boolean
|
||
}
|
||
|
||
function BookingCard({ booking, locale }: { booking: Booking; locale: string }) {
|
||
useLocale()
|
||
const palette = BOOKING_STATUS[booking.status as BookingStatus]
|
||
|
||
return (
|
||
<Link
|
||
to="/admin/bookings/$bookingId"
|
||
params={{ bookingId: booking.bookingId }}
|
||
style={{ textDecoration: 'none', display: 'block' }}
|
||
>
|
||
<Paper
|
||
withBorder
|
||
radius="md"
|
||
p="sm"
|
||
style={{
|
||
borderLeft: `3px solid ${palette.dot}`,
|
||
cursor: 'pointer',
|
||
}}
|
||
>
|
||
<Stack gap={4}>
|
||
<Text size="sm" fw={600} lineClamp={2} c="var(--mantine-color-text)">
|
||
{asI18n(`${booking.eventName}`)}
|
||
</Text>
|
||
<Group gap={4}>
|
||
{booking.isStammgruppe && (
|
||
<Text size="xs" c="yellow.6" title={m.common__pages__admin_booking__client__regular_group()} style={{ cursor: 'default' }}>{asI18n('★')}</Text>
|
||
)}
|
||
<Text size="xs" c="dimmed" lineClamp={1}>{asI18n(`${booking.clientName}`)}</Text>
|
||
</Group>
|
||
<Text size="xs" c="dimmed">
|
||
{asI18n(`${fmtDateShort(booking.startDate, locale)} – ${fmtDateShort(booking.endDate, locale)}`)}
|
||
</Text>
|
||
{booking.halfHouse && (
|
||
<Text size="xs" c="dimmed">{asI18n('½')}</Text>
|
||
)}
|
||
{booking.hasDeposit && (
|
||
<Text size="xs" c={booking.depositPaid ? 'green' : 'orange'} fw={500}>
|
||
{(booking.depositPaid ? m.common__pages__admin__bookings__deposit__paid : m.common__pages__admin__bookings__deposit__outstanding)()}
|
||
</Text>
|
||
)}
|
||
</Stack>
|
||
</Paper>
|
||
</Link>
|
||
)
|
||
}
|
||
|
||
function KanbanInner() {
|
||
useLocale()
|
||
usePageTitle(m.common__pages__kanban__title())
|
||
|
||
const { data, isLoading, error } = usePikkuQuery('getAdminBookings', { venueSlug: 'drawehn' })
|
||
|
||
if (isLoading) return <Container py="xl"><Text>{m.common__states__loading()}</Text></Container>
|
||
if (error) return <Container py="xl"><Text c="red">{m.common__states__error()}</Text></Container>
|
||
if (!data) return null
|
||
|
||
const byStatus = new Map<BookingStatus, Booking[]>()
|
||
for (const col of COLUMNS) byStatus.set(col, [])
|
||
for (const b of data.bookings) {
|
||
const col = b.status as BookingStatus
|
||
if (byStatus.has(col)) byStatus.get(col)!.push({ ...b, halfHouse: !!b.halfHouse } as unknown as Booking)
|
||
}
|
||
|
||
return (
|
||
<Box
|
||
style={{
|
||
overflowX: 'auto',
|
||
minHeight: 0,
|
||
paddingBottom: 24,
|
||
}}
|
||
px="xl"
|
||
pt="md"
|
||
>
|
||
<Group align="flex-start" wrap="nowrap" gap="md" style={{ minWidth: 'max-content' }}>
|
||
{COLUMNS.map((col) => {
|
||
const palette = BOOKING_STATUS[col]
|
||
const cards = byStatus.get(col) ?? []
|
||
return (
|
||
<Stack
|
||
key={col}
|
||
gap="sm"
|
||
style={{ width: 260, flexShrink: 0 }}
|
||
>
|
||
<Group gap="xs" align="center">
|
||
<Box
|
||
style={{
|
||
width: 8,
|
||
height: 8,
|
||
borderRadius: 4,
|
||
background: palette.dot,
|
||
flexShrink: 0,
|
||
}}
|
||
/>
|
||
<Text size="xs" fw={700} tt="uppercase" style={{ letterSpacing: '0.06em', color: palette.fg }}>
|
||
{bookingStatus[col]()}
|
||
</Text>
|
||
<Text size="xs" c="dimmed" ml="auto">
|
||
{cards.length}
|
||
</Text>
|
||
</Group>
|
||
<Stack gap="xs">
|
||
{cards.length === 0 ? (
|
||
<Paper withBorder radius="md" p="sm" style={{ borderStyle: 'dashed' }}>
|
||
<Text size="xs" c="dimmed" ta="center">{asI18n('—')}</Text>
|
||
</Paper>
|
||
) : (
|
||
cards.map((b) => (
|
||
<BookingCard key={b.bookingId} booking={b} locale={getLocale()} />
|
||
))
|
||
)}
|
||
</Stack>
|
||
</Stack>
|
||
)
|
||
})}
|
||
</Group>
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
function KanbanPage() {
|
||
return (
|
||
<PublicShell>
|
||
<KanbanInner />
|
||
</PublicShell>
|
||
)
|
||
}
|
||
|
||
export const Route = createFileRoute('/kanban')({
|
||
component: KanbanPage,
|
||
})
|