Files
emaui/apps/backoffice/src/app/features/configuration/pages/ConfigurationPage/index.tsx
2026-08-21 13:09:40 +00:00

425 lines
11 KiB
TypeScript

import { useState, useEffect, useCallback } from "react";
import {
Stack,
Title,
Tabs,
Group,
Button,
TextInput,
Textarea,
Modal,
Text,
Select,
Loader,
Center,
Alert,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { useDisclosure } from "@mantine/hooks";
import {
IconPlus,
IconBriefcase,
IconMap,
IconCertificate,
IconHash,
IconInfoCircle,
} from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import {
notify,
useErrorHandler,
AdvancedTable,
useServerTable,
ModalFooter,
PageLoader,
} from "@ema-platform/ui";
import { LocationPage } from "../../../location/pages/LocationPage";
import { CertificationPage } from "../../../certification/pages/CertificationPage";
import { NumberFormatTab } from "../../components/NumberFormatTab";
import {
useGetOrganizationsQuery,
useGetProfessionsQuery,
useCreateProfessionMutation,
useUpdateProfessionMutation,
useDeleteProfessionMutation,
} from "../../api/configuration-api";
import type { Profession } from "../../types/configuration";
import { professionColumns } from "./columns";
import { professionActionsColumn } from "./actions";
import { PageHeader } from '@ema-platform/ui';
interface ProfFormValues {
nameEn: string;
nameAm: string;
descEn: string;
descAm: string;
departmentId: string;
}
interface ProfFormProps {
editingProf: Profession | null;
deptOptions: { value: string; label: string }[];
isSubmitting: boolean;
onSubmit: (values: ProfFormValues, isEdit: boolean) => void;
onCancel: () => void;
}
function ProfessionForm({
editingProf,
deptOptions,
isSubmitting,
onSubmit,
onCancel,
}: ProfFormProps) {
const { t } = useTranslation();
const form = useForm<ProfFormValues>({
initialValues: {
nameEn: "",
nameAm: "",
descEn: "",
descAm: "",
departmentId: "",
},
validate: {
nameEn: (v) => (!v ? t("configuration.validation.nameEnRequired") : null),
nameAm: (v) => (!v ? t("configuration.validation.nameAmRequired") : null),
departmentId: (v) =>
!v ? t("configuration.validation.departmentRequired") : null,
},
});
useEffect(() => {
if (editingProf) {
form.setValues({
nameEn: editingProf.name.en,
nameAm: editingProf.name.am,
descEn: editingProf.description.en ?? "",
descAm: editingProf.description.am ?? "",
departmentId: editingProf.departmentId,
});
}
}, [editingProf]);
const handleSubmit = form.onSubmit((values) =>
onSubmit(values, !!editingProf),
);
return (
<Modal
opened
onClose={onCancel}
title={
editingProf
? t("configuration.update")
: t("configuration.addProfession")
}
size="md"
>
<form onSubmit={handleSubmit}>
<Stack gap="sm">
<TextInput
label={t("configuration.nameEn")}
placeholder="English name"
{...form.getInputProps("nameEn")}
size="sm"
/>
<TextInput
label={t("configuration.nameAm")}
placeholder="የአማርኛ ስም"
{...form.getInputProps("nameAm")}
size="sm"
/>
<Textarea
label={t("configuration.descEn")}
placeholder="English description"
{...form.getInputProps("descEn")}
size="sm"
autosize
minRows={2}
/>
<Textarea
label={t("configuration.descAm")}
placeholder="የአማርኛ መግለጫ"
{...form.getInputProps("descAm")}
size="sm"
autosize
minRows={2}
/>
<Select
label={t("configuration.department")}
placeholder={t("configuration.selectDepartment")}
data={deptOptions}
{...form.getInputProps("departmentId")}
size="sm"
searchable
/>
<ModalFooter>
<Button variant="default" onClick={onCancel} size="sm">
{t("configuration.cancel")}
</Button>
<Button type="submit" size="sm" loading={isSubmitting}>
{editingProf
? t("configuration.update")
: t("configuration.create")}
</Button>
</ModalFooter>
</Stack>
</form>
</Modal>
);
}
function ProfessionTab() {
const { t, i18n } = useTranslation();
const locale = i18n.language as "en" | "am";
const { handleError } = useErrorHandler();
const { data: deptRes } = useGetOrganizationsQuery();
const { pageIndex, setPageIndex, setQ, pageSize, setPageSize, skip, take } =
useServerTable({
pageSize: 10,
});
const {
data: profRes,
isLoading,
isFetching,
isError,
refetch,
} = useGetProfessionsQuery(
`skip:${skip},take:${take},orderBy:createdAt:DESC`,
);
const [createProfession, { isLoading: isCreating }] =
useCreateProfessionMutation();
const [updateProfession, { isLoading: isUpdating }] =
useUpdateProfessionMutation();
const [deleteProfession] = useDeleteProfessionMutation();
const departments = Array.isArray(deptRes) ? deptRes : (deptRes?.items ?? []);
// Server now paginates, so the previous client-side `isActive` filter is
// dropped (it would hide rows outside just this page). Restore it via a
// server-side `q` filter once the backend field name is confirmed.
const professions = profRes?.items ?? [];
const totalCount = profRes?.total ?? profRes?.count ?? professions.length;
const [editingProf, setEditingProf] = useState<Profession | null>(null);
const [showProfForm, setShowProfForm] = useState(false);
const [deleteTarget, setDeleteTarget] = useState<Profession | null>(null);
const [deleteOpened, { open: openDelete, close: closeDelete }] =
useDisclosure(false);
const deptOptions = departments
.filter((d) => d?.status?.toLowerCase() === "active")
.map((d) => ({
value: d.id,
label: d.name?.[locale] ?? d.name ?? "",
}));
const resetProfForm = useCallback(() => {
setEditingProf(null);
setShowProfForm(false);
}, []);
const handleEditProf = useCallback((prof: Profession) => {
setEditingProf(prof);
setShowProfForm(true);
}, []);
const handleDeleteProf = useCallback(
(prof: Profession) => {
setDeleteTarget(prof);
openDelete();
},
[openDelete],
);
const confirmDeleteProf = useCallback(async () => {
if (!deleteTarget) return;
try {
await deleteProfession(deleteTarget.id).unwrap();
notify.success(t("configuration.deleted"));
closeDelete();
setDeleteTarget(null);
} catch (e) {
handleError(e);
}
}, [deleteTarget, deleteProfession, closeDelete, handleError]);
const handleProfSubmit = useCallback(
async (values: ProfFormValues) => {
const name = { en: values.nameEn, am: values.nameAm };
const description = { en: values.descEn, am: values.descAm };
try {
if (editingProf) {
await updateProfession({
id: editingProf.id,
name,
description,
departmentId: values.departmentId,
}).unwrap();
notify.success(t("configuration.updated"));
} else {
await createProfession({
departmentId: values.departmentId,
name,
description,
}).unwrap();
notify.success(t("configuration.created"));
}
resetProfForm();
} catch (e) {
handleError(e);
}
},
[
editingProf,
createProfession,
updateProfession,
resetProfForm,
handleError,
],
);
const getDeptName = useCallback(
(deptId: string) => {
const dept = departments.find((d) => d.id === deptId);
return dept ? (dept.name?.[locale] ?? dept.name?.en ?? "-") : "-";
},
[departments, locale],
);
const columns = [
...professionColumns(t, locale, getDeptName),
professionActionsColumn({
onEdit: handleEditProf,
onDelete: handleDeleteProf,
}),
];
if (isLoading) {
return <PageLoader label="Loading Configuration…" height={400} />;
}
if (isError) {
return (
<Alert
icon={<IconInfoCircle size={16} />}
color="red"
title={t("configuration.error")}
/>
);
}
return (
<>
<Group justify="space-between" align="flex-end" mb="md">
<Title order={2}>{t("configuration.professionsList")}</Title>
{!showProfForm && (
<Button
variant="light"
leftSection={<IconPlus size={16} />}
onClick={() => setShowProfForm(true)}
size="sm"
>
{t("configuration.addProfession")}
</Button>
)}
</Group>
{showProfForm && (
<ProfessionForm
editingProf={editingProf}
deptOptions={deptOptions}
isSubmitting={isCreating || isUpdating}
onSubmit={handleProfSubmit}
onCancel={resetProfForm}
/>
)}
<AdvancedTable
columns={columns}
data={professions}
tableName={t("configuration.professionsList")}
itemCount={totalCount}
pageIndex={pageIndex}
onPageChange={setPageIndex}
pageSize={pageSize}
onPageSizeChange={setPageSize}
refresh={refetch}
onSearchChange={setQ}
isLoading={isFetching}
emptyText={t("configuration.noProfessions")}
/>
<Modal
opened={deleteOpened}
onClose={closeDelete}
title={t("configuration.confirmDelete")}
size="sm"
>
<Text mb="md">
{t("configuration.deleteConfirmText", {
name: deleteTarget?.name?.[locale] ?? "",
})}
</Text>
<ModalFooter>
<Button variant="default" onClick={closeDelete} size="sm">
{t("configuration.cancel")}
</Button>
<Button color="red" onClick={confirmDeleteProf} size="sm">
{t("configuration.delete")}
</Button>
</ModalFooter>
</Modal>
</>
);
}
export function ConfigurationPage() {
const { t } = useTranslation();
return (
<Stack gap="lg">
<PageHeader title={t("configuration.title")} noMargin />
<Tabs defaultValue="professions">
<Tabs.List>
<Tabs.Tab
value="professions"
leftSection={<IconBriefcase size={16} />}
>
{t("configuration.professions")}
</Tabs.Tab>
<Tabs.Tab value="locations" leftSection={<IconMap size={16} />}>
{t("location.title")}
</Tabs.Tab>
<Tabs.Tab
value="certifications"
leftSection={<IconCertificate size={16} />}
>
{t("certification.title")}
</Tabs.Tab>
<Tabs.Tab value="numberFormats" leftSection={<IconHash size={16} />}>
{t("numberFormat.title", "Number Formats")}
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="professions" pt="md">
<ProfessionTab />
</Tabs.Panel>
<Tabs.Panel value="locations" pt="md">
<LocationPage />
</Tabs.Panel>
<Tabs.Panel value="certifications" pt="md">
<CertificationPage />
</Tabs.Panel>
<Tabs.Panel value="numberFormats" pt="md">
<NumberFormatTab />
</Tabs.Panel>
</Tabs>
</Stack>
);
}