chore: seminarhof customer project
This commit is contained in:
260
apps/app/src/pages/admin.bookings.index.tsx
Normal file
260
apps/app/src/pages/admin.bookings.index.tsx
Normal file
@@ -0,0 +1,260 @@
|
||||
import { Link, createFileRoute, useNavigate } from '@tanstack/react-router'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { m } from '@/i18n/messages'
|
||||
import { bookingStatus } from '@/i18n/i18n-enum.gen'
|
||||
import { useLocale, getLocale } from '@/i18n/config'
|
||||
import { asI18n } from '@pikku/react'
|
||||
import { keepPreviousData } from '@tanstack/react-query'
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Container,
|
||||
Group,
|
||||
Loader,
|
||||
MultiSelect,
|
||||
Paper,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
TextInput,
|
||||
} from '@pikku/mantine/core'
|
||||
import { Search } from 'lucide-react'
|
||||
import { usePikkuQuery } from '@project/functions-sdk/pikku/api.gen'
|
||||
import { StatusPill } from '../components/StatusPill'
|
||||
import { usePageTitle } from '../components/AppLayout'
|
||||
import { BOOKING_STATUS, type BookingStatus } from '../lib/status'
|
||||
import { fmtDateShort } from '../lib/dates'
|
||||
|
||||
const STATUS_KEYS: BookingStatus[] = [
|
||||
'enquiry',
|
||||
'reserved',
|
||||
'confirmed',
|
||||
'ended',
|
||||
'cancelled',
|
||||
]
|
||||
|
||||
function StatusDot({ color }: { color: string }) {
|
||||
return (
|
||||
<Box
|
||||
component="span"
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
background: color,
|
||||
flex: '0 0 auto',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AdminBookingsInner() {
|
||||
useLocale()
|
||||
const navigate = useNavigate()
|
||||
|
||||
usePageTitle(m.common__pages__admin__bookings__title())
|
||||
|
||||
const [years, setYears] = useState<string[]>([])
|
||||
const [statusFilter, setStatusFilter] = useState<string[]>([])
|
||||
const [search, setSearch] = useState('')
|
||||
const [halfHouseOnly, setHalfHouseOnly] = useState(false)
|
||||
|
||||
const queryArgs = useMemo(
|
||||
() => ({
|
||||
venueSlug: 'drawehn',
|
||||
years: years.length ? years.map(Number) : undefined,
|
||||
status: statusFilter.length ? statusFilter : undefined,
|
||||
search: search.trim() || undefined,
|
||||
halfHouseOnly: halfHouseOnly || undefined,
|
||||
}),
|
||||
[years, statusFilter, search, halfHouseOnly],
|
||||
)
|
||||
|
||||
const normalizedSearch = search.trim().toLocaleLowerCase(getLocale())
|
||||
|
||||
const { data, isLoading, isFetching, error } = usePikkuQuery(
|
||||
'getAdminBookings',
|
||||
queryArgs,
|
||||
{ placeholderData: keepPreviousData },
|
||||
)
|
||||
|
||||
const statusOptions = useMemo(
|
||||
() =>
|
||||
STATUS_KEYS.map((s) => ({
|
||||
value: s,
|
||||
label: bookingStatus[s](),
|
||||
})),
|
||||
[getLocale()],
|
||||
)
|
||||
|
||||
if (isLoading && !data) {
|
||||
return <Container py="xl"><Text>{m.common__states__loading()}</Text></Container>
|
||||
}
|
||||
if (error && !data) {
|
||||
return <Container py="xl"><Text c="red">{m.common__states__error()}</Text></Container>
|
||||
}
|
||||
if (!data) return null
|
||||
|
||||
const bookings = [...data.bookings].sort((a, b) => {
|
||||
if (!normalizedSearch) return 0
|
||||
|
||||
const rank = (value: string | null | undefined) => {
|
||||
const text = value?.toLocaleLowerCase(getLocale()) ?? ''
|
||||
if (text === normalizedSearch) return 0
|
||||
if (text.startsWith(normalizedSearch)) return 1
|
||||
if (text.includes(normalizedSearch)) return 2
|
||||
return 3
|
||||
}
|
||||
|
||||
const aRank = Math.min(rank(a.eventName), rank(a.clientName))
|
||||
const bRank = Math.min(rank(b.eventName), rank(b.clientName))
|
||||
|
||||
if (aRank !== bRank) return aRank - bRank
|
||||
return a.eventName.localeCompare(b.eventName, getLocale())
|
||||
})
|
||||
|
||||
return (
|
||||
<Container size="xl" py="md">
|
||||
<Stack gap="sm">
|
||||
<Group gap="sm" align="flex-end" wrap="wrap">
|
||||
<TextInput
|
||||
flex="1 1 220px"
|
||||
leftSection={<Search size={16} />}
|
||||
placeholder={m.common__pages__admin__bookings__search_placeholder()}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<MultiSelect
|
||||
flex="0 1 200px"
|
||||
clearable
|
||||
placeholder={
|
||||
years.length ? undefined : m.common__pages__admin__bookings__filters__year()
|
||||
}
|
||||
data={data.yearOptions.map((y) => String(y))}
|
||||
value={years}
|
||||
onChange={setYears}
|
||||
/>
|
||||
<MultiSelect
|
||||
flex="1 1 240px"
|
||||
clearable
|
||||
placeholder={
|
||||
statusFilter.length
|
||||
? undefined
|
||||
: m.common__pages__admin__bookings__filters__status()
|
||||
}
|
||||
data={statusOptions}
|
||||
value={statusFilter}
|
||||
onChange={setStatusFilter}
|
||||
renderOption={({ option }) => (
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<StatusDot color={BOOKING_STATUS[option.value as BookingStatus].dot} />
|
||||
<span>{option.label}</span>
|
||||
</Group>
|
||||
)}
|
||||
/>
|
||||
<Checkbox
|
||||
color="plum"
|
||||
label={m.common__pages__admin__bookings__filters__half_house_only()}
|
||||
checked={halfHouseOnly}
|
||||
onChange={(e) => setHalfHouseOnly(e.currentTarget.checked)}
|
||||
styles={{ root: { alignSelf: 'center' } }}
|
||||
/>
|
||||
<Button
|
||||
component={Link}
|
||||
to="/enquiry"
|
||||
color="plum"
|
||||
variant="filled"
|
||||
ml="auto"
|
||||
>
|
||||
{m.common__pages__admin__bookings__new_booking()}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Group gap="xs" align="center">
|
||||
<Text size="sm" c="dimmed">
|
||||
{m.common__pages__admin__bookings__stats__showing({ count: data.totalCount })}
|
||||
</Text>
|
||||
{isFetching && <Loader size="xs" color="plum" />}
|
||||
</Group>
|
||||
|
||||
<Paper withBorder radius="md" p={0}>
|
||||
{bookings.length === 0 ? (
|
||||
<Box p="xl">
|
||||
<Text size="sm" c="dimmed" ta="center">
|
||||
{m.common__pages__admin__bookings__empty()}
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<Table highlightOnHover stickyHeader>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>{m.common__pages__admin__bookings__cols__dates()}</Table.Th>
|
||||
<Table.Th>{m.common__pages__admin__bookings__cols__course()}</Table.Th>
|
||||
<Table.Th>{m.common__pages__admin__bookings__cols__client()}</Table.Th>
|
||||
<Table.Th>{m.common__pages__admin__bookings__cols__status()}</Table.Th>
|
||||
<Table.Th>{m.common__pages__admin__bookings__cols__persons()}</Table.Th>
|
||||
<Table.Th ta="center">{m.common__pages__admin__bookings__cols__half_house()}</Table.Th>
|
||||
<Table.Th>{m.common__pages__admin__bookings__cols__deposit()}</Table.Th>
|
||||
<Table.Th>{m.common__pages__admin__bookings__cols__final()}</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{bookings.map((b) => (
|
||||
<Table.Tr
|
||||
key={b.bookingId}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() =>
|
||||
navigate({ to: '/admin/bookings/$bookingId', params: { bookingId: b.bookingId } })
|
||||
}
|
||||
>
|
||||
<Table.Td>
|
||||
<Text size="sm">{asI18n(`${fmtDateShort(b.startDate, getLocale())}`)}</Text>
|
||||
<Text size="xs" c="dimmed">{asI18n(`→ ${fmtDateShort(b.endDate, getLocale())}`)}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{asI18n(`${b.eventName}`)}</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap={4}>
|
||||
{b.isStammgruppe && (
|
||||
<Text size="xs" c="yellow" title={m.common__pages__admin__bookings__cols__regular_group()}>{asI18n('★')}</Text>
|
||||
)}
|
||||
{asI18n(`${b.clientName}`)}
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<StatusPill kind="booking" status={b.status as BookingStatus} />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{b.participantCount} / {b.expectedPersons ?? '?'}
|
||||
</Table.Td>
|
||||
<Table.Td ta="center">{b.halfHouse ? '½' : ''}</Table.Td>
|
||||
<Table.Td>
|
||||
{!b.hasDeposit
|
||||
? <Text size="xs" c="dimmed">{m.common__pages__admin__bookings__deposit__missing()}</Text>
|
||||
: <Badge size="xs" color={b.depositPaid ? 'green' : 'orange'} variant="light">
|
||||
{(b.depositPaid ? m.common__pages__admin__bookings__deposit__paid : m.common__pages__admin__bookings__deposit__outstanding)()}
|
||||
</Badge>}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{!b.hasFinal
|
||||
? <Text size="xs" c="dimmed">{m.common__pages__admin__bookings__deposit__missing()}</Text>
|
||||
: <Badge size="xs" color={b.finalPaid ? 'green' : 'orange'} variant="light">
|
||||
{(b.finalPaid ? m.common__pages__admin__bookings__deposit__paid : m.common__pages__admin__bookings__deposit__outstanding)()}
|
||||
</Badge>}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const Route = createFileRoute('/admin/bookings/')({
|
||||
component: AdminBookingsInner,
|
||||
})
|
||||
Reference in New Issue
Block a user