mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
173 lines
5.2 KiB
TypeScript
173 lines
5.2 KiB
TypeScript
import { useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import {
|
|
Badge,
|
|
Button,
|
|
Card,
|
|
Center,
|
|
Container,
|
|
Group,
|
|
Loader,
|
|
SegmentedControl,
|
|
Table,
|
|
Text,
|
|
TextInput,
|
|
Title,
|
|
} from '@mantine/core';
|
|
import { IconSearch } from '@tabler/icons-react';
|
|
import { notifications } from '@mantine/notifications';
|
|
import {
|
|
STATUS_COLORS,
|
|
STATUS_LABELS,
|
|
extractErrorMessage,
|
|
useClaimApplicationMutation,
|
|
useGetAssignedToMeQuery,
|
|
useGetQueueQuery,
|
|
} from '@ema-platform/api';
|
|
|
|
/**
|
|
* The officer work pool.
|
|
*
|
|
* "Unclaimed" is the shared queue; claiming moves an application into "Mine"
|
|
* and it stays there through every adjustment round.
|
|
*/
|
|
export function LicenseQueuePage() {
|
|
const navigate = useNavigate();
|
|
const [tab, setTab] = useState<'unclaimed' | 'mine'>('unclaimed');
|
|
const [search, setSearch] = useState('');
|
|
|
|
const queue = useGetQueueQuery({ search: search || undefined });
|
|
const mine = useGetAssignedToMeQuery({ search: search || undefined });
|
|
const [claim, { isLoading: claiming }] = useClaimApplicationMutation();
|
|
|
|
const active = tab === 'unclaimed' ? queue : mine;
|
|
const items = active.data?.items ?? [];
|
|
|
|
async function handleClaim(id: string) {
|
|
try {
|
|
await claim(id).unwrap();
|
|
notifications.show({
|
|
color: 'teal',
|
|
title: 'Claimed',
|
|
message: 'The application is now assigned to you.',
|
|
});
|
|
setTab('mine');
|
|
} catch (err) {
|
|
// A 409 means another officer got there first — refresh so the queue
|
|
// stops showing work that is no longer available.
|
|
notifications.show({
|
|
color: 'red',
|
|
title: 'Could not claim',
|
|
message: extractErrorMessage(err, 'Another officer already claimed it.'),
|
|
});
|
|
queue.refetch();
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Container size="xl" py="md">
|
|
<Group justify="space-between" mb="md">
|
|
<Title order={3}>Licence applications</Title>
|
|
<Group>
|
|
<TextInput
|
|
placeholder="Company, TIN or number"
|
|
leftSection={<IconSearch size={14} />}
|
|
value={search}
|
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
|
w={260}
|
|
/>
|
|
<SegmentedControl
|
|
value={tab}
|
|
onChange={(v) => setTab(v as 'unclaimed' | 'mine')}
|
|
data={[
|
|
{ label: `Unclaimed (${queue.data?.total ?? 0})`, value: 'unclaimed' },
|
|
{ label: `Mine (${mine.data?.total ?? 0})`, value: 'mine' },
|
|
]}
|
|
/>
|
|
</Group>
|
|
</Group>
|
|
|
|
<Card withBorder padding={0}>
|
|
{active.isLoading ? (
|
|
<Center h={200}>
|
|
<Loader />
|
|
</Center>
|
|
) : items.length === 0 ? (
|
|
<Center h={160}>
|
|
<Text c="dimmed" size="sm">
|
|
{tab === 'unclaimed'
|
|
? 'No applications waiting to be claimed.'
|
|
: 'You have no applications in progress.'}
|
|
</Text>
|
|
</Center>
|
|
) : (
|
|
<Table highlightOnHover>
|
|
<Table.Thead>
|
|
<Table.Tr>
|
|
<Table.Th>Number</Table.Th>
|
|
<Table.Th>Company</Table.Th>
|
|
<Table.Th>TIN</Table.Th>
|
|
<Table.Th>Status</Table.Th>
|
|
<Table.Th>Submitted</Table.Th>
|
|
<Table.Th />
|
|
</Table.Tr>
|
|
</Table.Thead>
|
|
<Table.Tbody>
|
|
{items.map((app) => (
|
|
<Table.Tr key={app.id}>
|
|
<Table.Td>
|
|
<Text size="sm" fw={500}>
|
|
{app.applicationNumber}
|
|
</Text>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Text size="sm">{app.companyName ?? '—'}</Text>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Text size="sm" c="dimmed">
|
|
{app.tinNumber ?? '—'}
|
|
</Text>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Badge color={STATUS_COLORS[app.status]} variant="light">
|
|
{STATUS_LABELS[app.status]}
|
|
</Badge>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Text size="sm" c="dimmed">
|
|
{app.submittedAt
|
|
? new Date(app.submittedAt).toLocaleDateString()
|
|
: '—'}
|
|
</Text>
|
|
</Table.Td>
|
|
<Table.Td align="right">
|
|
{tab === 'unclaimed' ? (
|
|
<Button
|
|
size="xs"
|
|
loading={claiming}
|
|
onClick={() => handleClaim(app.id)}
|
|
>
|
|
Claim
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
size="xs"
|
|
variant="light"
|
|
onClick={() => navigate(`/licence-review/${app.id}`)}
|
|
>
|
|
Review
|
|
</Button>
|
|
)}
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
))}
|
|
</Table.Tbody>
|
|
</Table>
|
|
)}
|
|
</Card>
|
|
</Container>
|
|
);
|
|
}
|
|
|
|
export default LicenseQueuePage;
|