feat(i18n): add new translations for refresh, view, toggle columns, and no results

refactor(payment): remove payment API client and related tests, consolidating payment logic

refactor(payment): delete payment modal and associated constants, hooks, and types

fix(ui): enhance AdvancedTable with refresh button and column toggle functionality

fix(ui): implement pagination in useServerTable for better data handling
This commit is contained in:
estifanos
2026-08-03 09:22:02 +00:00
parent 52e2f4c009
commit 599dc4aa83
19 changed files with 452 additions and 1395 deletions

View File

@@ -9,6 +9,7 @@ import {
Modal,
Text,
Paper,
Card,
Loader,
Center,
Alert,
@@ -35,7 +36,7 @@ import {
IconChartBar,
IconSearch,
} from '@tabler/icons-react';
import { notify, BilingualInput, useErrorHandler } from '@ema-platform/ui';
import { notify, BilingualInput, useErrorHandler, AdvancedTable, useServerTable, type AdvancedColumn } from '@ema-platform/ui';
import type { BilingualValue } from '@ema-platform/ui';
import { useGetResultsQuery, useLazyGetResultQuery, useDeleteResultMutation, useUpdateResultMutation } from '../api/result-api';
import { useGetExamsQuery } from '../../exam/api/exam-api';
@@ -97,7 +98,8 @@ export function ResultPage() {
const locale = i18n.language as 'en' | 'am';
const { handleError } = useErrorHandler();
const { data: examRes } = useGetExamsQuery();
const { data, isLoading, isError } = useGetResultsQuery();
const { data, isFetching, isError, refetch } = useGetResultsQuery();
const { setPageIndex, pageSize, paginate } = useServerTable({ pageSize: 25 });
const [fetchDetail, { data: detailResult, isFetching: isDetailLoading }] = useLazyGetResultQuery();
const exams = examRes?.items ?? [];
@@ -207,9 +209,76 @@ export function ResultPage() {
}
};
if (isLoading) return <Center py="xl"><Loader /></Center>;
if (isError) return <Alert icon={<IconInfoCircle size={16} />} color="red" title={t('result.loadError')} />;
const columns: AdvancedColumn<Result>[] = [
{
header: t('result.columns.seafarer'),
cell: ({ row }) => (
<Text fz="sm" fw={500}>
{row.original.seafarer
? `${row.original.seafarer.firstName} ${row.original.seafarer.lastName}`
: row.original.seafarerId.slice(0, 8)}
</Text>
),
},
{
header: t('result.columns.exam'),
cell: ({ row }) => (
<Text fz="sm">{row.original.exam ? row.original.exam.title[locale] : getExamTitle(row.original.examId)}</Text>
),
},
{
header: t('result.columns.totalScore'),
cell: ({ row }) => <Text fz="sm" fw={600}>{row.original.totalScore}</Text>,
},
{
header: t('result.columns.status'),
cell: ({ row }) => (
<Badge
size="sm"
variant="light"
color={STATUS_COLOR[row.original.status]}
leftSection={
<Box
w={6}
h={6}
style={{ borderRadius: 999, background: `var(--mantine-color-${STATUS_COLOR[row.original.status]}-6)` }}
/>
}
>
{t(`result.status.${row.original.status}`)}
</Badge>
),
},
{
header: t('result.columns.date'),
cell: ({ row }) => <Text fz="sm">{new Date(row.original.createdAt).toLocaleDateString()}</Text>,
},
{
header: '',
label: t('result.columns.actions', 'Actions'),
cell: ({ row }) => (
<Group gap="xs">
<Button size="xs" variant="subtle" leftSection={<IconEye size={13} />} onClick={() => viewDetail(row.original)}>
{t('result.action.viewEdit')}
</Button>
<Button
size="xs"
variant="subtle"
color="red"
leftSection={<IconTrash size={13} />}
onClick={() => { setDeleteTarget(row.original); openDelete(); }}
>
{t('result.action.delete')}
</Button>
</Group>
),
},
];
const page = paginate(filtered);
return (
<Stack gap="lg">
<Group justify="space-between" align="flex-end">
@@ -229,7 +298,7 @@ export function ResultPage() {
<ResultStat label={t('result.stats.avgScore')} value={avgScore} icon={IconChartBar} color="indigo" />
</SimpleGrid>
<Paper withBorder radius="md">
<Card withBorder padding={0}>
<Group p="md" justify="space-between" wrap="wrap" gap="sm">
<Text fw={600}>{t('result.section')}</Text>
<Group gap="sm" wrap="wrap">
@@ -237,7 +306,7 @@ export function ResultPage() {
placeholder={t('result.search.seafarer')}
leftSection={<IconSearch size={15} />}
value={searchQuery}
onChange={(e) => setSearchQuery(e.currentTarget.value)}
onChange={(e) => { setSearchQuery(e.currentTarget.value); setPageIndex(0); }}
size="sm"
style={{ width: 240 }}
/>
@@ -245,7 +314,7 @@ export function ResultPage() {
placeholder={t('result.search.filterByExam')}
data={[{ value: '', label: t('result.search.allExams') }, ...examOptions]}
value={examFilter}
onChange={(v) => setExamFilter(v ?? null)}
onChange={(v) => { setExamFilter(v ?? null); setPageIndex(0); }}
size="sm"
style={{ width: 280 }}
clearable
@@ -253,77 +322,19 @@ export function ResultPage() {
</Group>
</Group>
<Table striped highlightOnHover>
<Table.Thead bg="var(--mantine-color-default-hover)">
<Table.Tr>
<Table.Th>{t('result.columns.seafarer')}</Table.Th>
<Table.Th>{t('result.columns.exam')}</Table.Th>
<Table.Th>{t('result.columns.totalScore')}</Table.Th>
<Table.Th>{t('result.columns.status')}</Table.Th>
<Table.Th>{t('result.columns.date')}</Table.Th>
<Table.Th />
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{filtered.map((r) => (
<Table.Tr key={r.id}>
<Table.Td>
<Text fz="sm" fw={500}>
{r.seafarer ? `${r.seafarer.firstName} ${r.seafarer.lastName}` : r.seafarerId.slice(0, 8)}
</Text>
</Table.Td>
<Table.Td><Text fz="sm">{r.exam ? r.exam.title[locale] : getExamTitle(r.examId)}</Text></Table.Td>
<Table.Td><Text fz="sm" fw={600}>{r.totalScore}</Text></Table.Td>
<Table.Td>
<Badge
size="sm"
variant="light"
color={STATUS_COLOR[r.status]}
leftSection={
<Box
w={6}
h={6}
style={{ borderRadius: 999, background: `var(--mantine-color-${STATUS_COLOR[r.status]}-6)` }}
/>
}
>
{t(`result.status.${r.status}`)}
</Badge>
</Table.Td>
<Table.Td><Text fz="sm">{new Date(r.createdAt).toLocaleDateString()}</Text></Table.Td>
<Table.Td>
<Group gap="xs">
<Button
size="xs"
variant="subtle"
leftSection={<IconEye size={13} />}
onClick={() => viewDetail(r)}
>
{t('result.action.viewEdit')}
</Button>
<Button
size="xs"
variant="subtle"
color="red"
leftSection={<IconTrash size={13} />}
onClick={() => { setDeleteTarget(r); openDelete(); }}
>
{t('result.action.delete')}
</Button>
</Group>
</Table.Td>
</Table.Tr>
))}
{filtered.length === 0 && (
<Table.Tr>
<Table.Td colSpan={6}>
<Text c="dimmed" ta="center" py="xl">{t('result.noItems')}</Text>
</Table.Td>
</Table.Tr>
)}
</Table.Tbody>
</Table>
</Paper>
<AdvancedTable
columns={columns}
data={page.rows}
tableName={t('result.title')}
itemCount={page.itemCount}
pageIndex={page.pageIndex}
onPageChange={setPageIndex}
pageSize={pageSize}
refresh={refetch}
isLoading={isFetching}
emptyText={t('result.noItems')}
/>
</Card>
<Modal
opened={detailOpened}