import { useCallback, useState } from "react"; import { Alert, Button, Group, Modal, NumberInput, Select, Stack, Text, TextInput, Title, } from "@mantine/core"; import { useForm } from "@mantine/form"; import { useDisclosure } from "@mantine/hooks"; import { IconInfoCircle, IconPlus } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; import { AdvancedTable, ModalFooter, notify, PageLoader, useErrorHandler, type AdvancedColumn, } from "@ema-platform/ui"; import { useLocalized, useGetDepartmentsQuery, useCreateDepartmentMutation, useUpdateDepartmentMutation, useDeleteDepartmentMutation, useGetRanksQuery, useCreateRankMutation, useUpdateRankMutation, useDeleteRankMutation, type Department, type Rank, type RankCertificateCategory, } from "@ema-platform/api"; const CATEGORY_OPTIONS: { value: RankCertificateCategory; label: string }[] = [ { value: "COC", label: "CoC" }, { value: "COP", label: "CoP" }, ]; /** * Departments and their CoC/CoP rank ladders, as backoffice-editable config. * * Both used to be hardcoded (`ESeafarerDepartment` and the `COC_LADDERS`/ * `COP_LADDERS` arrays) — this is the write side that config never had. A * rank's `ladderOrder` is the rung position `resolveNextRank` climbs, so * reordering here changes what an applicant is auto-advanced to next. */ export function RankDepartmentTab() { const { t } = useTranslation(); const localized = useLocalized(); const { data: deptRes, isLoading: deptLoading, isFetching: deptFetching, refetch: refetchDepts } = useGetDepartmentsQuery(); const { data: rankRes, isLoading: rankLoading, isFetching: rankFetching, refetch: refetchRanks } = useGetRanksQuery(); const departments = deptRes?.items ?? []; const ranks = rankRes?.items ?? []; const deptOptions = departments.map((d) => ({ value: d.id, label: localized(d.name) })); const deptName = useCallback( (id: string) => departments.find((d) => d.id === id)?.code ?? "-", [departments], ); if (deptLoading || rankLoading) { return ; } return ( }> {t( "configuration.rankLadderNotice", "A rank's position is its rung on the ladder — an applicant is auto-advanced to the next position up from what they already hold.", )} ); } // ------------------------------------------------------------- departments function DepartmentSection({ departments, isFetching, refetch, localized, }: { departments: Department[]; isFetching: boolean; refetch: () => void; localized: (v: Department["name"]) => string; }) { const { t } = useTranslation(); const { handleError } = useErrorHandler(); const [createDepartment, { isLoading: isCreating }] = useCreateDepartmentMutation(); const [updateDepartment, { isLoading: isUpdating }] = useUpdateDepartmentMutation(); const [deleteDepartment] = useDeleteDepartmentMutation(); const [editing, setEditing] = useState(null); const [showForm, setShowForm] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const [deleteOpened, { open: openDelete, close: closeDelete }] = useDisclosure(false); const resetForm = useCallback(() => { setEditing(null); setShowForm(false); }, []); const form = useForm({ initialValues: { code: "", nameEn: "", nameAm: "", sortOrder: 0 }, validate: { code: (v) => (!v ? t("configuration.validation.codeRequired", "Code is required") : null), nameEn: (v) => (!v ? t("configuration.validation.nameEnRequired") : null), nameAm: (v) => (!v ? t("configuration.validation.nameAmRequired") : null), }, }); const openEdit = useCallback( (dept: Department) => { setEditing(dept); form.setValues({ code: dept.code, nameEn: dept.name.en ?? "", nameAm: dept.name.am ?? "", sortOrder: dept.sortOrder, }); setShowForm(true); }, // eslint-disable-next-line react-hooks/exhaustive-deps [], ); const handleSubmit = form.onSubmit(async (values) => { const name = { en: values.nameEn, am: values.nameAm }; try { if (editing) { await updateDepartment({ id: editing.id, code: values.code, name, sortOrder: values.sortOrder, }).unwrap(); notify.success(t("configuration.updated")); } else { await createDepartment({ code: values.code, name, sortOrder: values.sortOrder }).unwrap(); notify.success(t("configuration.created")); } resetForm(); form.reset(); } catch (e) { handleError(e); } }); const confirmDelete = useCallback(async () => { if (!deleteTarget) return; try { await deleteDepartment(deleteTarget.id).unwrap(); notify.success(t("configuration.deleted")); closeDelete(); setDeleteTarget(null); } catch (e) { handleError(e); } }, [deleteTarget, deleteDepartment, closeDelete, handleError]); const columns: AdvancedColumn[] = [ { header: t("configuration.code", "Code"), cell: ({ row }) => row.original.code }, { header: t("configuration.name"), cell: ({ row }) => localized(row.original.name) }, { header: t("configuration.sortOrder", "Order"), cell: ({ row }) => row.original.sortOrder }, { header: "actions", size: 90, cell: ({ row }) => ( ), }, ]; return ( {t("configuration.departments", "Departments")} {!showForm && ( )} a.sortOrder - b.sortOrder)} tableName={t("configuration.departments", "Departments")} itemCount={departments.length} pageIndex={0} onPageChange={() => {}} pageSize={departments.length || 10} onPageSizeChange={() => {}} refresh={refetch} isLoading={isFetching} />
{t("configuration.deleteConfirmText", { name: deleteTarget ? localized(deleteTarget.name) : "" })}
); } // ------------------------------------------------------------------ ranks function RankSection({ ranks, deptOptions, deptName, isFetching, refetch, localized, }: { ranks: Rank[]; deptOptions: { value: string; label: string }[]; deptName: (id: string) => string; isFetching: boolean; refetch: () => void; localized: (v: Rank["name"]) => string; }) { const { t } = useTranslation(); const { handleError } = useErrorHandler(); const [createRank, { isLoading: isCreating }] = useCreateRankMutation(); const [updateRank, { isLoading: isUpdating }] = useUpdateRankMutation(); const [deleteRank] = useDeleteRankMutation(); const [editing, setEditing] = useState(null); const [showForm, setShowForm] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const [deleteOpened, { open: openDelete, close: closeDelete }] = useDisclosure(false); const resetForm = useCallback(() => { setEditing(null); setShowForm(false); }, []); const form = useForm({ initialValues: { departmentId: "", certificateCategory: "COC" as RankCertificateCategory, key: "", nameEn: "", nameAm: "", ladderOrder: 0, }, validate: { departmentId: (v) => (!v ? t("configuration.validation.departmentRequired") : null), key: (v) => (!v ? t("configuration.validation.keyRequired", "Key is required") : null), nameEn: (v) => (!v ? t("configuration.validation.nameEnRequired") : null), nameAm: (v) => (!v ? t("configuration.validation.nameAmRequired") : null), }, }); const openEdit = useCallback( (rank: Rank) => { setEditing(rank); form.setValues({ departmentId: rank.departmentId, certificateCategory: rank.certificateCategory, key: rank.key, nameEn: rank.name.en ?? "", nameAm: rank.name.am ?? "", ladderOrder: rank.ladderOrder, }); setShowForm(true); }, // eslint-disable-next-line react-hooks/exhaustive-deps [], ); const handleSubmit = form.onSubmit(async (values) => { const name = { en: values.nameEn, am: values.nameAm }; try { if (editing) { await updateRank({ id: editing.id, departmentId: values.departmentId, certificateCategory: values.certificateCategory, key: values.key, name, ladderOrder: values.ladderOrder, }).unwrap(); notify.success(t("configuration.updated")); } else { await createRank({ departmentId: values.departmentId, certificateCategory: values.certificateCategory, key: values.key, name, ladderOrder: values.ladderOrder, }).unwrap(); notify.success(t("configuration.created")); } resetForm(); form.reset(); } catch (e) { handleError(e); } }); const confirmDelete = useCallback(async () => { if (!deleteTarget) return; try { await deleteRank(deleteTarget.id).unwrap(); notify.success(t("configuration.deleted")); closeDelete(); setDeleteTarget(null); } catch (e) { handleError(e); } }, [deleteTarget, deleteRank, closeDelete, handleError]); const sortedRanks = [...ranks].sort( (a, b) => a.departmentId.localeCompare(b.departmentId) || a.certificateCategory.localeCompare(b.certificateCategory) || a.ladderOrder - b.ladderOrder, ); const columns: AdvancedColumn[] = [ { header: t("configuration.department"), cell: ({ row }) => deptName(row.original.departmentId) }, { header: t("configuration.category", "Ladder"), cell: ({ row }) => row.original.certificateCategory }, { header: t("configuration.rankOrder", "Rung"), cell: ({ row }) => row.original.ladderOrder }, { header: t("configuration.key", "Key"), cell: ({ row }) => row.original.key }, { header: t("configuration.name"), cell: ({ row }) => localized(row.original.name) }, { header: "actions", size: 90, cell: ({ row }) => ( ), }, ]; return ( {t("configuration.ranks", "Ranks")} {!showForm && ( )} {}} pageSize={sortedRanks.length || 10} onPageSizeChange={() => {}} refresh={refetch} isLoading={isFetching} />
{t("configuration.deleteConfirmText", { name: deleteTarget ? localized(deleteTarget.name) : "" })}
); }