Files
emaui/apps/backoffice/src/app/features/exam/components/ExamCandidatesPanel/columns.tsx
mihretue ae3af7777e fix(exam,result,question): kebab-icon action menus, status change as a picker
Two follow-ups on the dropdown-actions pass:
- All four action menus (exams, results, questions, candidates panel) now
  trigger off a plain three-dot icon button instead of a text 'Actions'
  button with a chevron.
- Exam status change no longer lists all six statuses flat in the dropdown
  (bad UX, looked like six always-visible options). 'Status' is now one
  menu item that opens a small picker modal (current status + a Select +
  Update) rather than the full edit form.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 11:46:16 +00:00

133 lines
4.0 KiB
TypeScript

import { ActionIcon, Badge, Menu, Text } from '@mantine/core';
import { IconDotsVertical, IconRefresh, IconUserCheck } from '@tabler/icons-react';
import type { TFunction } from 'i18next';
import type { AdvancedColumn } from '@ema-platform/ui';
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
import type { AttendanceStatus, ExamRegistration } from '../../types/exam';
const ATTENDANCE_COLOR: Record<AttendanceStatus, string> = {
REGISTERED: 'gray',
PRESENT: 'teal',
LATE: 'yellow',
ABSENT: 'red',
WITHDRAWN: 'orange',
DISQUALIFIED: 'red',
};
export const candidateName = (registration: ExamRegistration) =>
[
registration.profile?.firstName,
registration.profile?.middleName,
registration.profile?.lastName,
]
.filter(Boolean)
.join(' ') || registration.profileId.slice(0, 8);
export function examCandidateColumns(
t: TFunction,
handlers: {
onRecord: (registration: ExamRegistration) => void;
onRegrade: (registration: ExamRegistration) => void;
regrading?: string | null;
},
): AdvancedColumn<ExamRegistration>[] {
return [
{
header: t('exam.candidates.admission'),
cell: ({ row }) => (
<Text fz="sm" ff="monospace" fw={600}>
{row.original.admissionNumber}
</Text>
),
},
{
header: t('exam.candidates.name'),
cell: ({ row }) => <Text fz="sm">{candidateName(row.original)}</Text>,
},
{
header: t('exam.candidates.attempt'),
cell: ({ row }) => (
<Badge
size="sm"
variant="light"
color={row.original.kind === 'RETAKE' ? 'orange' : 'blue'}
>
{row.original.kind === 'RETAKE'
? t('exam.candidates.retake', { n: row.original.attemptNumber })
: t('exam.candidates.firstSitting')}
</Badge>
),
},
{
header: t('exam.candidates.attendance'),
cell: ({ row }) => (
<Badge
size="sm"
variant="light"
color={ATTENDANCE_COLOR[row.original.attendanceStatus] ?? 'gray'}
>
{t(`exam.attendance.${row.original.attendanceStatus}`)}
</Badge>
),
},
{
header: t('exam.candidates.remark'),
cell: ({ row }) => (
<Text fz="xs" c="dimmed" maw={220} lineClamp={2}>
{row.original.attendanceRemark ?? '—'}
</Text>
),
},
{
header: '',
label: t('exam.candidates.record'),
align: 'right',
cell: ({ row }) => {
const attemptStatus = row.original.attempt?.status;
const canRegrade = attemptStatus === 'SUBMITTED' || attemptStatus === 'EXPIRED';
return (
<Menu shadow="md" width={180} position="bottom-end">
<Menu.Target>
<ActionIcon
variant="subtle"
color="gray"
size="sm"
loading={handlers.regrading === row.original.attempt?.id}
>
<IconDotsVertical size={16} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<RequirePermission
anyOf={[LICENSE_PERMISSIONS.RECORD_EXAM_ATTENDANCE]}
hideOnly
>
<Menu.Item
leftSection={<IconUserCheck size={14} />}
onClick={() => handlers.onRecord(row.original)}
>
{t('exam.candidates.record')}
</Menu.Item>
</RequirePermission>
{canRegrade && (
<RequirePermission
anyOf={[LICENSE_PERMISSIONS.RECORD_EXAM_RESULT]}
hideOnly
>
<Menu.Item
color="grape"
leftSection={<IconRefresh size={14} />}
onClick={() => handlers.onRegrade(row.original)}
>
{t('exam.candidates.regrade')}
</Menu.Item>
</RequirePermission>
)}
</Menu.Dropdown>
</Menu>
);
},
},
];
}