Files
emaui/apps/backoffice/src/app/features/license-review/pages/LicenseQueuePage.tsx
2026-07-29 16:53:50 +03:00

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;