Merge branch 'WorkflowChange' of github.com:Tria-plc/emaui into WorkflowChange

This commit is contained in:
Nati
2026-08-21 18:20:02 +00:00
33 changed files with 2684 additions and 309 deletions

View File

@@ -1,5 +1,5 @@
import { Button, Group } from '@mantine/core';
import { IconEye, IconTrash } from '@tabler/icons-react';
import { ActionIcon, Menu } from '@mantine/core';
import { IconDotsVertical, IconEye, IconSend, IconTrash } from '@tabler/icons-react';
import type { TFunction } from 'i18next';
import type { AdvancedColumn } from '@ema-platform/ui';
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
@@ -13,6 +13,7 @@ export function resultActionsColumn(
onQc: (result: Result, action: QcAction) => void;
onViewDetail: (result: Result) => void;
onDelete: (result: Result) => void;
onPublish: (result: Result) => void;
},
): AdvancedColumn<Result> {
return {
@@ -21,49 +22,66 @@ export function resultActionsColumn(
cell: ({ row }) => {
const r = row.original;
return (
<Group gap="xs">
{(r.reviewStatus === 'MARKED' || r.reviewStatus === 'MODERATED') && (
<>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MODERATE_EXAM_RESULT]} hideOnly>
<Button size="compact-xs" variant="light" color="yellow" onClick={() => handlers.onQc(r, 'moderate')}>
{t('result.review.moderate')}
</Button>
<Menu shadow="md" width={180} position="bottom-end">
<Menu.Target>
<ActionIcon variant="subtle" color="gray" size="sm">
<IconDotsVertical size={16} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item leftSection={<IconEye size={14} />} onClick={() => handlers.onViewDetail(r)}>
{t('result.action.viewEdit')}
</Menu.Item>
{(r.reviewStatus === 'MARKED' || r.reviewStatus === 'MODERATED') && (
<>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MODERATE_EXAM_RESULT]} hideOnly>
<Menu.Item color="yellow" onClick={() => handlers.onQc(r, 'moderate')}>
{t('result.review.moderate')}
</Menu.Item>
</RequirePermission>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.APPROVE_EXAM_RESULT]} hideOnly>
<Menu.Item color="blue" onClick={() => handlers.onQc(r, 'approve')}>
{t('result.review.approve')}
</Menu.Item>
</RequirePermission>
</>
)}
{(r.reviewStatus === 'APPROVED' || r.reviewStatus === 'MODERATED') && (
<RequirePermission
anyOf={[
LICENSE_PERMISSIONS.MODERATE_EXAM_RESULT,
LICENSE_PERMISSIONS.APPROVE_EXAM_RESULT,
]}
hideOnly
>
<Menu.Item color="orange" onClick={() => handlers.onQc(r, 'return')}>
{t('result.review.return')}
</Menu.Item>
</RequirePermission>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.APPROVE_EXAM_RESULT]} hideOnly>
<Button size="compact-xs" variant="light" color="blue" onClick={() => handlers.onQc(r, 'approve')}>
{t('result.review.approve')}
</Button>
)}
{r.reviewStatus === 'APPROVED' && (
<RequirePermission anyOf={[LICENSE_PERMISSIONS.PUBLISH_EXAM_RESULT]} hideOnly>
<Menu.Item
color="teal"
leftSection={<IconSend size={14} />}
onClick={() => handlers.onPublish(r)}
>
{t('result.review.publish')}
</Menu.Item>
</RequirePermission>
</>
)}
{(r.reviewStatus === 'APPROVED' || r.reviewStatus === 'MODERATED') && (
<RequirePermission
anyOf={[
LICENSE_PERMISSIONS.MODERATE_EXAM_RESULT,
LICENSE_PERMISSIONS.APPROVE_EXAM_RESULT,
]}
hideOnly
>
<Button size="compact-xs" variant="subtle" color="orange" onClick={() => handlers.onQc(r, 'return')}>
{t('result.review.return')}
</Button>
)}
<RequirePermission anyOf={[LICENSE_PERMISSIONS.APPROVE_EXAM_RESULT]} hideOnly>
<Menu.Divider />
<Menu.Item
color="red"
leftSection={<IconTrash size={14} />}
onClick={() => handlers.onDelete(r)}
>
{t('result.action.delete')}
</Menu.Item>
</RequirePermission>
)}
<Button size="xs" variant="subtle" leftSection={<IconEye size={13} />} onClick={() => handlers.onViewDetail(r)}>
{t('result.action.viewEdit')}
</Button>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.APPROVE_EXAM_RESULT]} hideOnly>
<Button
size="xs"
variant="subtle"
color="red"
leftSection={<IconTrash size={13} />}
onClick={() => handlers.onDelete(r)}
>
{t('result.action.delete')}
</Button>
</RequirePermission>
</Group>
</Menu.Dropdown>
</Menu>
);
},
};

View File

@@ -95,6 +95,8 @@ export function ResultPage() {
const [detailOpened, { open: openDetail, close: closeDetail }] = useDisclosure(false);
const [deleteTarget, setDeleteTarget] = useState<Result | null>(null);
const [deleteOpened, { open: openDelete, close: closeDelete }] = useDisclosure(false);
const [publishTarget, setPublishTarget] = useState<Result | null>(null);
const [publishOpened, { open: openPublish, close: closePublish }] = useDisclosure(false);
const [detailRemark, setDetailRemark] = useState<BilingualValue>({ en: '', am: '' });
const [detailBreakdowns, setDetailBreakdowns] = useState<ResultBreakdown[]>([]);
const [detailSaving, setDetailSaving] = useState(false);
@@ -231,6 +233,19 @@ export function ResultPage() {
}
};
/** Same publish call as handlePublish, but scoped to one row's exam — no page filter needed. */
const handleConfirmPublish = async () => {
if (!publishTarget) return;
try {
const outcome = await publishResults(publishTarget.examId).unwrap();
notify.success(t('result.review.publishedCount', outcome));
closePublish();
setPublishTarget(null);
} catch (error) {
notify.error(extractErrorMessage(error, t('result.review.error')));
}
};
const handleDetailClose = () => {
closeDetail();
setDetailBreakdowns([]);
@@ -257,6 +272,7 @@ export function ResultPage() {
onQc: openQc,
onViewDetail: viewDetail,
onDelete: (r) => { setDeleteTarget(r); openDelete(); },
onPublish: (r) => { setPublishTarget(r); openPublish(); },
}),
];
@@ -283,9 +299,11 @@ export function ResultPage() {
{t('result.review.publish')}
</Button>
</RequirePermission>
<Button leftSection={<IconPlus size={15} />} onClick={openPicker} size="sm">
{t('result.record')}
</Button>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.RECORD_EXAM_RESULT]} hideOnly>
<Button leftSection={<IconPlus size={15} />} onClick={openPicker} size="sm">
{t('result.record')}
</Button>
</RequirePermission>
</Group>
}
/>
@@ -472,14 +490,22 @@ export function ResultPage() {
<ModalFooter>
<Button variant="default" onClick={handleDetailClose} size="sm">{t('result.close')}</Button>
<Button
onClick={handleDetailSave}
size="sm"
loading={detailSaving}
leftSection={<IconDeviceFloppy size={15} />}
<RequirePermission
anyOf={[
LICENSE_PERMISSIONS.RECORD_EXAM_RESULT,
LICENSE_PERMISSIONS.MODERATE_EXAM_RESULT,
]}
hideOnly
>
{t('result.save')}
</Button>
<Button
onClick={handleDetailSave}
size="sm"
loading={detailSaving}
leftSection={<IconDeviceFloppy size={15} />}
>
{t('result.save')}
</Button>
</RequirePermission>
</ModalFooter>
</Stack>
) : (
@@ -542,6 +568,20 @@ export function ResultPage() {
</ModalFooter>
</Modal>
<Modal opened={publishOpened} onClose={closePublish} title={t('result.review.publish')} size="sm">
<Text mb="md">
{t('result.review.publishConfirmText', {
exam: publishTarget ? getExamTitle(publishTarget.examId) : '',
})}
</Text>
<ModalFooter>
<Button variant="default" onClick={closePublish} size="sm">{t('result.cancel')}</Button>
<Button color="teal" loading={isPublishing} onClick={handleConfirmPublish} size="sm">
{t('result.review.publish')}
</Button>
</ModalFooter>
</Modal>
{/* Choose exam, then record */}
<Modal opened={pickerOpened} onClose={closePicker} title={t('result.record')} size="md" radius="lg">
<Stack gap="md">