mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
425 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|