feat: implement permission-based access control across various pages and components

- Added RequirePermission component to manage access based on user permissions.
- Integrated permission checks in MySeaRecordsPage, SeafarerRegistrationPage, VesselRegistrationPage, WaiverPage, and PortalLayout.
- Updated router to enforce permissions for specific routes.
- Introduced PORTAL_PERMISSIONS and LICENSE_PERMISSIONS constants for consistent permission management.
- Enhanced usePermissions hook to fetch permissions from the server and determine access rights.
- Refactored UI components to conditionally render based on user permissions, improving security and user experience.
This commit is contained in:
Nati
2026-08-13 12:58:11 +00:00
parent 49fbd8fc53
commit 6852fc86c6
46 changed files with 1093 additions and 581 deletions

View File

@@ -47,7 +47,7 @@ import {
} from '@ema-platform/api';
import { EmptyState, ErrorState, ModalFooter, PageHeader } from '@ema-platform/ui';
import { authStorage, usePermissions } from '@ema-platform/auth';
import { PERMISSIONS } from '../../../layouts/nav-config';
import { LICENSE_PERMISSIONS as PERMISSIONS } from '@ema-platform/auth';
/** Same resolution the shared RTK Query baseQuery uses. */
const API_BASE_URL =

View File

@@ -2,6 +2,7 @@ import { Badge, Button, Text } from '@mantine/core';
import { 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> = {
@@ -78,14 +79,19 @@ export function examCandidateColumns(
label: t('exam.candidates.record'),
align: 'right',
cell: ({ row }) => (
<Button
size="compact-xs"
variant="light"
leftSection={<IconUserCheck size={12} />}
onClick={() => handlers.onRecord(row.original)}
<RequirePermission
anyOf={[LICENSE_PERMISSIONS.RECORD_EXAM_ATTENDANCE]}
hideOnly
>
{t('exam.candidates.record')}
</Button>
<Button
size="compact-xs"
variant="light"
leftSection={<IconUserCheck size={12} />}
onClick={() => handlers.onRecord(row.original)}
>
{t('exam.candidates.record')}
</Button>
</RequirePermission>
),
},
];

View File

@@ -2,6 +2,7 @@ import { Badge, Button, Text } from '@mantine/core';
import { IconAlertTriangle } 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 { ExamIncident, ExamIncidentStatus } from '../../types/exam';
const STATUS_COLOR: Record<ExamIncidentStatus, string> = {
@@ -70,14 +71,19 @@ export function examIncidentColumns(
align: 'right',
cell: ({ row }) =>
row.original.status === 'OPEN' || row.original.status === 'UNDER_REVIEW' ? (
<Button
size="compact-xs"
variant="light"
leftSection={<IconAlertTriangle size={12} />}
onClick={() => handlers.onResolve(row.original)}
<RequirePermission
anyOf={[LICENSE_PERMISSIONS.MANAGE_EXAM_INCIDENTS]}
hideOnly
>
{t('exam.incidents.resolve')}
</Button>
<Button
size="compact-xs"
variant="light"
leftSection={<IconAlertTriangle size={12} />}
onClick={() => handlers.onResolve(row.original)}
>
{t('exam.incidents.resolve')}
</Button>
</RequirePermission>
) : null,
},
];

View File

@@ -16,6 +16,7 @@ import { IconInfoCircle, IconPlus } from '@tabler/icons-react';
import { AdvancedTable, notify, useServerTable } from '@ema-platform/ui';
import { useDateDisplayer } from '@ema-platform/shared';
import { extractErrorMessage } from '@ema-platform/api';
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
import {
useGetExamIncidentsQuery,
useGetExamRegistrationsQuery,
@@ -122,15 +123,20 @@ export function ExamIncidentsPanel({ examId }: { examId: string }) {
<Paper withBorder radius="lg" p="lg">
<Group justify="space-between" mb="md">
<Title order={5}>{t('exam.incidents.section')}</Title>
<Button
size="xs"
variant="light"
color="orange"
leftSection={<IconPlus size={14} />}
onClick={() => setAddOpen(true)}
<RequirePermission
anyOf={[LICENSE_PERMISSIONS.MANAGE_EXAM_INCIDENTS]}
hideOnly
>
{t('exam.incidents.add')}
</Button>
<Button
size="xs"
variant="light"
color="orange"
leftSection={<IconPlus size={14} />}
onClick={() => setAddOpen(true)}
>
{t('exam.incidents.add')}
</Button>
</RequirePermission>
</Group>
{(incidents ?? []).length === 0 ? (

View File

@@ -41,6 +41,7 @@ import {
} from '@tabler/icons-react';
import { ModalFooter, notify, useErrorHandler } from '@ema-platform/ui';
import { extractErrorMessage } from '@ema-platform/api';
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
import {
useGetExamQuery,
useUpdateExamMutation,
@@ -393,14 +394,16 @@ export function ExamDetailPage() {
<Title order={5}>
{t("exam.detail.questionsSection", { pts: totalPoints })}
</Title>
<Button
variant="light"
size="xs"
leftSection={<IconPlus size={14} />}
onClick={openAssignModal}
>
{t("exam.manageQuestions")}
</Button>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MANAGE_EXAMS]} hideOnly>
<Button
variant="light"
size="xs"
leftSection={<IconPlus size={14} />}
onClick={openAssignModal}
>
{t("exam.manageQuestions")}
</Button>
</RequirePermission>
</Group>
{(exam.questions ?? []).length === 0 ? (
<Alert color="gray" icon={<IconInfoCircle size={16} />}>
@@ -461,9 +464,11 @@ export function ExamDetailPage() {
<Button variant="default" onClick={closeAssign} size="sm">
{t("exam.cancel")}
</Button>
<Button onClick={handleAssign} size="sm" loading={isAssigning}>
{t("exam.saveAssignments")}
</Button>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MANAGE_EXAMS]} hideOnly>
<Button onClick={handleAssign} size="sm" loading={isAssigning}>
{t("exam.saveAssignments")}
</Button>
</RequirePermission>
</ModalFooter>
</>
) : (
@@ -478,9 +483,11 @@ export function ExamDetailPage() {
size="xs"
style={{ width: 80 }}
/>
<Button size="xs" variant="light" loading={isDrawing} onClick={handleRandomSelect}>
{t('exam.randomSelect')}
</Button>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MANAGE_EXAMS]} hideOnly>
<Button size="xs" variant="light" loading={isDrawing} onClick={handleRandomSelect}>
{t('exam.randomSelect')}
</Button>
</RequirePermission>
</Group>
<QuestionAssigner
available={eligibleQuestions}
@@ -492,9 +499,11 @@ export function ExamDetailPage() {
<Button variant="default" onClick={closeAssign} size="sm">
{t("exam.cancel")}
</Button>
<Button onClick={handleAssign} size="sm" loading={isAssigning}>
{t("exam.saveAssignments")}
</Button>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MANAGE_EXAMS]} hideOnly>
<Button onClick={handleAssign} size="sm" loading={isAssigning}>
{t("exam.saveAssignments")}
</Button>
</RequirePermission>
</ModalFooter>
</>
)}

View File

@@ -2,6 +2,7 @@ import { ActionIcon, Group } from "@mantine/core";
import { IconEdit, IconTrash, IconDetails } 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 { Exam } from "../../types/exam";
export function examActionsColumn(
@@ -17,22 +18,24 @@ export function examActionsColumn(
align: "right",
cell: ({ row }) => (
<Group gap="xs">
<ActionIcon
variant="subtle"
color="blue"
size="sm"
onClick={() => handlers.onEdit(row.original)}
>
<IconEdit size={14} />
</ActionIcon>
<ActionIcon
variant="subtle"
color="red"
size="sm"
onClick={() => handlers.onDelete(row.original)}
>
<IconTrash size={14} />
</ActionIcon>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MANAGE_EXAMS]} hideOnly>
<ActionIcon
variant="subtle"
color="blue"
size="sm"
onClick={() => handlers.onEdit(row.original)}
>
<IconEdit size={14} />
</ActionIcon>
<ActionIcon
variant="subtle"
color="red"
size="sm"
onClick={() => handlers.onDelete(row.original)}
>
<IconTrash size={14} />
</ActionIcon>
</RequirePermission>
<ActionIcon
variant="subtle"
color="red"

View File

@@ -24,6 +24,7 @@ import {
IconClipboardList,
} from "@tabler/icons-react";
import { notify, useErrorHandler, AdvancedTable, useServerTable, ModalFooter, AmharicDatePicker } from "@ema-platform/ui";
import { LICENSE_PERMISSIONS, RequirePermission } from "@ema-platform/auth";
import { useGetCertificationsQuery } from "../../../certification/api/certification-api";
import {
useGetExamsQuery,
@@ -450,14 +451,16 @@ export function ExamPage() {
</Text>
</div>
{!showForm && (
<Button
variant="light"
leftSection={<IconPlus size={16} />}
onClick={() => setShowForm(true)}
size="sm"
>
{t("exam.add")}
</Button>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MANAGE_EXAMS]} hideOnly>
<Button
variant="light"
leftSection={<IconPlus size={16} />}
onClick={() => setShowForm(true)}
size="sm"
>
{t("exam.add")}
</Button>
</RequirePermission>
)}
</Group>

View File

@@ -2,6 +2,7 @@ import { Badge, Button, Text, Tooltip } from '@mantine/core';
import { IconShieldCog } from '@tabler/icons-react';
import type { AdvancedColumn } from '@ema-platform/ui';
import type { Bilingual, IssuedLicense } from '@ema-platform/api';
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
const LICENSE_STATUS_COLORS: Record<string, string> = {
ACTIVE: 'green',
@@ -84,16 +85,24 @@ export function licenseRegisterColumns(
align: 'right',
cell: ({ row }) =>
actionsFor(row.original).length > 0 ? (
<Tooltip label="Suspend / revoke / reinstate">
<Button
size="compact-xs"
variant="subtle"
leftSection={<IconShieldCog size={14} />}
onClick={() => handlers.onStatus(row.original)}
>
Status
</Button>
</Tooltip>
<RequirePermission
anyOf={[
LICENSE_PERMISSIONS.SUSPEND_LICENSE,
LICENSE_PERMISSIONS.CANCEL_LICENSE,
]}
hideOnly
>
<Tooltip label="Suspend / revoke / reinstate">
<Button
size="compact-xs"
variant="subtle"
leftSection={<IconShieldCog size={14} />}
onClick={() => handlers.onStatus(row.original)}
>
Status
</Button>
</Tooltip>
</RequirePermission>
) : null,
},
];

View File

@@ -1,5 +1,5 @@
import type { ApplicationDetail, LicenseStatus } from '@ema-platform/api';
import { PERMISSIONS } from '../../../layouts/nav-config';
import { LICENSE_PERMISSIONS as PERMISSIONS } from '@ema-platform/auth';
/**
* Where an action is rendered. One tier per action, decided here rather than

View File

@@ -2,6 +2,7 @@ import { Button } from "@mantine/core";
import type { TFunction } from "i18next";
import type { LicenseApplication } from "@ema-platform/api";
import type { AdvancedColumn } from "@ema-platform/ui";
import { LICENSE_PERMISSIONS, RequirePermission } from "@ema-platform/auth";
export function licenseQueueActionsColumn(
t: TFunction,
@@ -19,13 +20,18 @@ export function licenseQueueActionsColumn(
cell: ({ row }) =>
row.original.assignedOfficerId === null &&
row.original.status === "SUBMITTED" ? (
<Button
size="xs"
loading={handlers.claiming}
onClick={() => handlers.onClaim(row.original.id)}
<RequirePermission
anyOf={[LICENSE_PERMISSIONS.CLAIM_APPLICATION]}
hideOnly
>
{t("queue.claim", "Claim")}
</Button>
<Button
size="xs"
loading={handlers.claiming}
onClick={() => handlers.onClaim(row.original.id)}
>
{t("queue.claim", "Claim")}
</Button>
</RequirePermission>
) : (
<Button
size="xs"

View File

@@ -52,6 +52,7 @@ import {
AmharicDatePicker,
type AdvancedColumn,
} from "@ema-platform/ui";
import { LICENSE_PERMISSIONS, RequirePermission } from "@ema-platform/auth";
import {
DEFAULT_VIEW,
SAVED_VIEWS,
@@ -638,12 +639,17 @@ export function LicenseQueuePage() {
>
{t("queue.export", "Export CSV")}
</Button>
<Button loading={claiming} onClick={handleBulkClaim}>
{t("queue.bulkClaim", {
count: selected.length,
defaultValue: "Claim {{count}}",
})}
</Button>
<RequirePermission
anyOf={[LICENSE_PERMISSIONS.CLAIM_APPLICATION]}
hideOnly
>
<Button loading={claiming} onClick={handleBulkClaim}>
{t("queue.bulkClaim", {
count: selected.length,
defaultValue: "Claim {{count}}",
})}
</Button>
</RequirePermission>
</Group>
</Group>
</Paper>

View File

@@ -3,6 +3,7 @@ import { IconCheck, IconPaperclip, IconX } from '@tabler/icons-react';
import type { TFunction } from 'i18next';
import type { AdvancedColumn } from '@ema-platform/ui';
import type { MedicalCertificate, SeaServiceRecord } from '@ema-platform/api';
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
export function medicalActionsColumn(
t: TFunction,
@@ -28,24 +29,29 @@ export function medicalActionsColumn(
>
{t('recordVerification.evidence', 'Evidence')}
</Button>
<Button
size="compact-xs"
color="teal"
leftSection={<IconCheck size={14} />}
loading={handlers.ruling}
onClick={() => handlers.onVerify(row.original)}
<RequirePermission
anyOf={[LICENSE_PERMISSIONS.VERIFY_SEAFARER_RECORDS]}
hideOnly
>
{t('recordVerification.verify', 'Verify')}
</Button>
<Button
size="compact-xs"
color="red"
variant="light"
leftSection={<IconX size={14} />}
onClick={() => handlers.onReject(row.original)}
>
{t('recordVerification.reject', 'Reject')}
</Button>
<Button
size="compact-xs"
color="teal"
leftSection={<IconCheck size={14} />}
loading={handlers.ruling}
onClick={() => handlers.onVerify(row.original)}
>
{t('recordVerification.verify', 'Verify')}
</Button>
<Button
size="compact-xs"
color="red"
variant="light"
leftSection={<IconX size={14} />}
onClick={() => handlers.onReject(row.original)}
>
{t('recordVerification.reject', 'Reject')}
</Button>
</RequirePermission>
</Group>
),
};
@@ -75,24 +81,29 @@ export function seaServiceActionsColumn(
>
{t('recordVerification.evidence', 'Evidence')}
</Button>
<Button
size="compact-xs"
color="teal"
leftSection={<IconCheck size={14} />}
loading={handlers.ruling}
onClick={() => handlers.onVerify(row.original)}
<RequirePermission
anyOf={[LICENSE_PERMISSIONS.VERIFY_SEAFARER_RECORDS]}
hideOnly
>
{t('recordVerification.verify', 'Verify')}
</Button>
<Button
size="compact-xs"
color="red"
variant="light"
leftSection={<IconX size={14} />}
onClick={() => handlers.onReject(row.original)}
>
{t('recordVerification.reject', 'Reject')}
</Button>
<Button
size="compact-xs"
color="teal"
leftSection={<IconCheck size={14} />}
loading={handlers.ruling}
onClick={() => handlers.onVerify(row.original)}
>
{t('recordVerification.verify', 'Verify')}
</Button>
<Button
size="compact-xs"
color="red"
variant="light"
leftSection={<IconX size={14} />}
onClick={() => handlers.onReject(row.original)}
>
{t('recordVerification.reject', 'Reject')}
</Button>
</RequirePermission>
</Group>
),
};

View File

@@ -3,6 +3,7 @@ import { IconEdit } from '@tabler/icons-react';
import type { TFunction } from 'i18next';
import type { LicenseType } from '@ema-platform/api';
import type { AdvancedColumn } from '@ema-platform/ui';
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
export function paymentConfigActionsColumn(
t: TFunction,
@@ -13,14 +14,19 @@ export function paymentConfigActionsColumn(
size: 90,
align: 'right',
cell: ({ row }) => (
<Button
size="xs"
variant="light"
leftSection={<IconEdit size={14} />}
onClick={() => handlers.onEdit(row.original)}
<RequirePermission
anyOf={[LICENSE_PERMISSIONS.UPDATE_LICENSE_TYPE]}
hideOnly
>
{t('paymentConfig.edit', 'Edit')}
</Button>
<Button
size="xs"
variant="light"
leftSection={<IconEdit size={14} />}
onClick={() => handlers.onEdit(row.original)}
>
{t('paymentConfig.edit', 'Edit')}
</Button>
</RequirePermission>
),
};
}

View File

@@ -2,6 +2,7 @@ import { ActionIcon, Button, Group } from '@mantine/core';
import { IconEdit, IconGavel, 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';
import type { Question } from '../../types/question';
export function questionActionsColumn(
@@ -22,43 +23,49 @@ export function questionActionsColumn(
return (
<Group gap="xs">
{(q.status === 'DRAFT' || q.status === 'REJECTED') && (
<Button
size="compact-xs"
variant="light"
leftSection={<IconSend size={12} />}
loading={handlers.isSubmittingReview}
onClick={() => handlers.onSubmitForApproval(q)}
>
{t('question.qc.submit')}
</Button>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.AUTHOR_QUESTION]} hideOnly>
<Button
size="compact-xs"
variant="light"
leftSection={<IconSend size={12} />}
loading={handlers.isSubmittingReview}
onClick={() => handlers.onSubmitForApproval(q)}
>
{t('question.qc.submit')}
</Button>
</RequirePermission>
)}
{q.status === 'PENDING_APPROVAL' && (
<>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.APPROVE_QUESTION]} hideOnly>
<Button size="compact-xs" variant="light" color="teal" onClick={() => handlers.onReview(q, 'APPROVED')}>
{t('question.qc.approve')}
</Button>
<Button size="compact-xs" variant="light" color="red" onClick={() => handlers.onReview(q, 'REJECTED')}>
{t('question.qc.reject')}
</Button>
</>
</RequirePermission>
)}
{q.status === 'APPROVED' && (
<Button
size="compact-xs"
variant="subtle"
color="dark"
leftSection={<IconGavel size={12} />}
onClick={() => handlers.onReview(q, 'RETIRED')}
>
{t('question.qc.retire')}
</Button>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.APPROVE_QUESTION]} hideOnly>
<Button
size="compact-xs"
variant="subtle"
color="dark"
leftSection={<IconGavel size={12} />}
onClick={() => handlers.onReview(q, 'RETIRED')}
>
{t('question.qc.retire')}
</Button>
</RequirePermission>
)}
<ActionIcon variant="subtle" color="blue" size="sm" onClick={() => handlers.onEdit(q)}>
<IconEdit size={14} />
</ActionIcon>
<ActionIcon variant="subtle" color="red" size="sm" onClick={() => handlers.onDelete(q)}>
<IconTrash size={14} />
</ActionIcon>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.AUTHOR_QUESTION]} hideOnly>
<ActionIcon variant="subtle" color="blue" size="sm" onClick={() => handlers.onEdit(q)}>
<IconEdit size={14} />
</ActionIcon>
<ActionIcon variant="subtle" color="red" size="sm" onClick={() => handlers.onDelete(q)}>
<IconTrash size={14} />
</ActionIcon>
</RequirePermission>
</Group>
);
},

View File

@@ -19,6 +19,7 @@ import { useTranslation } from 'react-i18next';
import { IconPlus, IconInfoCircle } from '@tabler/icons-react';
import { AdvancedColumn, AdvancedTable, ModalFooter, notify, useErrorHandler, useServerTable } from '@ema-platform/ui';
import { extractErrorMessage } from '@ema-platform/api';
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
import { useGetCertificationsQuery } from '../../../certification/api/certification-api';
import {
useGetQuestionsQuery,
@@ -219,9 +220,11 @@ export function QuestionPage() {
<Group justify="space-between" align="flex-end">
<Title order={2}>{t('question.title')}</Title>
{!showForm && (
<Button variant="light" leftSection={<IconPlus size={16} />} onClick={() => setShowForm(true)} size="sm">
{t('question.addQuestion')}
</Button>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.AUTHOR_QUESTION]} hideOnly>
<Button variant="light" leftSection={<IconPlus size={16} />} onClick={() => setShowForm(true)} size="sm">
{t('question.addQuestion')}
</Button>
</RequirePermission>
)}
</Group>

View File

@@ -17,6 +17,7 @@ import {
import { IconInfoCircle, IconCheck, IconX } from '@tabler/icons-react';
import { AdvancedTable, ModalFooter, notify, useServerTable } from '@ema-platform/ui';
import { extractErrorMessage } from '@ema-platform/api';
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
import { recordResultColumns } from './columns';
import { useCreateResultMutation } from '../../api/result-api';
import { useGetExamRegistrationsQuery } from '../../../exam/api/exam-api';
@@ -207,9 +208,11 @@ export function RecordResultModal({
<ModalFooter>
<Button variant="default" onClick={onClose} size="sm">{t('result.cancel')}</Button>
<Button onClick={handleSave} size="sm" loading={isSaving}>
{t('result.saveResult')}
</Button>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.RECORD_EXAM_RESULT]} hideOnly>
<Button onClick={handleSave} size="sm" loading={isSaving}>
{t('result.saveResult')}
</Button>
</RequirePermission>
</ModalFooter>
</>
)}

View File

@@ -2,6 +2,7 @@ import { Badge, Button, Text } from '@mantine/core';
import { IconGavel } 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 { ExamAppeal } from '../../types/result';
export function examAppealsColumns(
@@ -59,15 +60,17 @@ export function examAppealsColumns(
label: t('result.appeals.decide'),
align: 'right',
cell: ({ row }) => (
<Button
size="compact-xs"
variant="light"
color="grape"
leftSection={<IconGavel size={12} />}
onClick={() => handlers.onDecide(row.original)}
>
{t('result.appeals.decide')}
</Button>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.DECIDE_EXAM_APPEAL]} hideOnly>
<Button
size="compact-xs"
variant="light"
color="grape"
leftSection={<IconGavel size={12} />}
onClick={() => handlers.onDecide(row.original)}
>
{t('result.appeals.decide')}
</Button>
</RequirePermission>
),
},
];

View File

@@ -2,6 +2,7 @@ import { Button, Group } from '@mantine/core';
import { IconEye, 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';
import type { Result } from '../../types/result';
export type QcAction = 'moderate' | 'approve' | 'return';
@@ -23,31 +24,45 @@ export function resultActionsColumn(
<Group gap="xs">
{(r.reviewStatus === 'MARKED' || r.reviewStatus === 'MODERATED') && (
<>
<Button size="compact-xs" variant="light" color="yellow" onClick={() => handlers.onQc(r, 'moderate')}>
{t('result.review.moderate')}
</Button>
<Button size="compact-xs" variant="light" color="blue" onClick={() => handlers.onQc(r, 'approve')}>
{t('result.review.approve')}
</Button>
<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>
</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>
</RequirePermission>
</>
)}
{(r.reviewStatus === 'APPROVED' || r.reviewStatus === 'MODERATED') && (
<Button size="compact-xs" variant="subtle" color="orange" onClick={() => handlers.onQc(r, 'return')}>
{t('result.review.return')}
</Button>
<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>
)}
<Button size="xs" variant="subtle" leftSection={<IconEye size={13} />} onClick={() => handlers.onViewDetail(r)}>
{t('result.action.viewEdit')}
</Button>
<Button
size="xs"
variant="subtle"
color="red"
leftSection={<IconTrash size={13} />}
onClick={() => handlers.onDelete(r)}
>
{t('result.action.delete')}
</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>
);
},

View File

@@ -38,6 +38,7 @@ import { notify, BilingualInput, useErrorHandler, AdvancedTable, useServerTable,
import { useDateDisplayer } from '@ema-platform/shared';
import type { BilingualValue } from '@ema-platform/ui';
import { extractErrorMessage, useLocalized } from '@ema-platform/api';
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
import {
useGetResultsQuery,
useLazyGetResultQuery,
@@ -300,17 +301,19 @@ export function ResultPage() {
<Text fz="sm" c="dimmed">{t('result.subtitle')}</Text>
</div>
<Group gap="sm">
<Button
variant="light"
color="teal"
size="sm"
loading={isPublishing}
disabled={!examFilter}
leftSection={<IconSend size={15} />}
onClick={handlePublish}
>
{t('result.review.publish')}
</Button>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.PUBLISH_EXAM_RESULT]} hideOnly>
<Button
variant="light"
color="teal"
size="sm"
loading={isPublishing}
disabled={!examFilter}
leftSection={<IconSend size={15} />}
onClick={handlePublish}
>
{t('result.review.publish')}
</Button>
</RequirePermission>
<Button leftSection={<IconPlus size={15} />} onClick={openPicker} size="sm">
{t('result.record')}
</Button>

View File

@@ -2,6 +2,7 @@ import { Button, Tooltip } from '@mantine/core';
import { IconShieldCog } 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 { ProfileRow } from './columns';
export function seafarerStatusActionColumn(
@@ -13,16 +14,21 @@ export function seafarerStatusActionColumn(
label: t('seafarerRegistry.columns.actions', 'Actions'),
cell: ({ row }) =>
row.original.seafarerNumber ? (
<Tooltip label={t('seafarerRegistry.statusActionTooltip', 'Suspend / reinstate / close')}>
<Button
size="compact-xs"
variant="subtle"
leftSection={<IconShieldCog size={14} />}
onClick={() => handlers.onStatus(row.original)}
>
{t('seafarerRegistry.statusAction', 'Status')}
</Button>
</Tooltip>
<RequirePermission
anyOf={[LICENSE_PERMISSIONS.MANAGE_SEAFARER_STATUS]}
hideOnly
>
<Tooltip label={t('seafarerRegistry.statusActionTooltip', 'Suspend / reinstate / close')}>
<Button
size="compact-xs"
variant="subtle"
leftSection={<IconShieldCog size={14} />}
onClick={() => handlers.onStatus(row.original)}
>
{t('seafarerRegistry.statusAction', 'Status')}
</Button>
</Tooltip>
</RequirePermission>
) : null,
};
}

View File

@@ -2,6 +2,7 @@ import { Badge, Button, Text, Tooltip } from '@mantine/core';
import { IconShieldCog } from '@tabler/icons-react';
import type { AdvancedColumn } from '@ema-platform/ui';
import type { Vessel } from '@ema-platform/api';
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
const VESSEL_STATUS_COLORS: Record<string, string> = {
REGISTERED: 'green',
@@ -76,19 +77,24 @@ export function vesselRegistrationQueueColumns(
label: 'Actions',
align: 'right',
cell: ({ row }) => (
<Tooltip label="Suspend / deregister / reinstate">
<Button
size="compact-xs"
variant="subtle"
leftSection={<IconShieldCog size={14} />}
onClick={(e) => {
e.stopPropagation();
handlers.onStatus(row.original);
}}
>
Status
</Button>
</Tooltip>
<RequirePermission
anyOf={[LICENSE_PERMISSIONS.MANAGE_VESSEL_STATUS]}
hideOnly
>
<Tooltip label="Suspend / deregister / reinstate">
<Button
size="compact-xs"
variant="subtle"
leftSection={<IconShieldCog size={14} />}
onClick={(e) => {
e.stopPropagation();
handlers.onStatus(row.original);
}}
>
Status
</Button>
</Tooltip>
</RequirePermission>
),
},
];