mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-30 05:18:13 +00:00
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:
@@ -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 =
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user