mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-29 00:10:58 +00:00
480 lines
17 KiB
TypeScript
480 lines
17 KiB
TypeScript
import { useState } from 'react';
|
|
import {
|
|
Stack,
|
|
Title,
|
|
Tabs,
|
|
Group,
|
|
Button,
|
|
TextInput,
|
|
Textarea,
|
|
Table,
|
|
ActionIcon,
|
|
Modal,
|
|
Text,
|
|
Select,
|
|
Badge,
|
|
Paper,
|
|
} from '@mantine/core';
|
|
import { useForm } from '@mantine/form';
|
|
import { useDisclosure } from '@mantine/hooks';
|
|
import { IconEdit, IconTrash, IconPlus, IconBuilding, IconBriefcase, IconMap } from '@tabler/icons-react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { notify } from '@ema-platform/ui';
|
|
import { LocationPage } from '../../location/pages/LocationPage';
|
|
import type { Department, Profession } from '../types/configuration';
|
|
|
|
let nextId = 1;
|
|
const uid = () => String(nextId++);
|
|
|
|
const MOCK_DEPARTMENTS: Department[] = [
|
|
{ id: uid(), code: 'IT', names: { en: 'Information Technology', am: 'ኢንፎርሜሽን ቴክኖሎጂ' }, description: 'Handles all IT infrastructure and systems.', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
|
|
{ id: uid(), code: 'HR', names: { en: 'Human Resources', am: 'የሰው ኃይል ሀብት' }, description: 'Manages personnel and recruitment.', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
|
|
{ id: uid(), code: 'FIN', names: { en: 'Finance', am: 'ፋይናንስ' }, description: 'Oversees budgeting and accounting.', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
|
|
];
|
|
|
|
const MOCK_PROFESSIONS: Profession[] = [
|
|
{ id: uid(), code: 'SWE', names: { en: 'Software Engineer', am: 'የሶፍትዌር መሐንዲስ' }, description: 'Develops and maintains software applications.', departmentId: '1', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
|
|
{ id: uid(), code: 'SRE', names: { en: 'Site Reliability Engineer', am: 'የጣቢያ አስተማማኝነት መሐንዲስ' }, description: 'Ensures system reliability and uptime.', departmentId: '1', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
|
|
{ id: uid(), code: 'HRM', names: { en: 'HR Manager', am: 'የሰው ኃይል አስተዳዳሪ' }, description: 'Leads the HR team and strategy.', departmentId: '2', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
|
|
];
|
|
|
|
function DepartmentTab() {
|
|
const { t } = useTranslation();
|
|
const [departments, setDepartments] = useState<Department[]>(MOCK_DEPARTMENTS);
|
|
const [editingDept, setEditingDept] = useState<Department | null>(null);
|
|
const [showDeptForm, setShowDeptForm] = useState(false);
|
|
const [deleteTarget, setDeleteTarget] = useState<Department | null>(null);
|
|
const [deleteOpened, { open: openDelete, close: closeDelete }] = useDisclosure(false);
|
|
|
|
const deptForm = useForm({
|
|
initialValues: { code: '', nameEn: '', nameAm: '', description: '' },
|
|
validate: {
|
|
code: (v) => (!v ? t('configuration.validation.codeRequired') : null),
|
|
nameEn: (v) => (!v ? t('configuration.validation.nameEnRequired') : null),
|
|
nameAm: (v) => (!v ? t('configuration.validation.nameAmRequired') : null),
|
|
},
|
|
});
|
|
|
|
const resetDeptForm = () => {
|
|
deptForm.reset();
|
|
setEditingDept(null);
|
|
setShowDeptForm(false);
|
|
};
|
|
|
|
const handleEditDept = (dept: Department) => {
|
|
setEditingDept(dept);
|
|
deptForm.setValues({
|
|
code: dept.code,
|
|
nameEn: dept.names.en,
|
|
nameAm: dept.names.am,
|
|
description: dept.description,
|
|
});
|
|
setShowDeptForm(true);
|
|
};
|
|
|
|
const handleDeleteDept = (dept: Department) => {
|
|
setDeleteTarget(dept);
|
|
openDelete();
|
|
};
|
|
|
|
const confirmDeleteDept = () => {
|
|
if (!deleteTarget) return;
|
|
setDepartments((prev) => prev.filter((d) => d.id !== deleteTarget.id));
|
|
notify.success(t('configuration.deleted'));
|
|
closeDelete();
|
|
setDeleteTarget(null);
|
|
};
|
|
|
|
const handleDeptSubmit = deptForm.onSubmit((values) => {
|
|
const now = new Date().toISOString();
|
|
if (editingDept) {
|
|
setDepartments((prev) =>
|
|
prev.map((d) =>
|
|
d.id === editingDept.id
|
|
? { ...d, code: values.code, names: { en: values.nameEn, am: values.nameAm }, description: values.description, updatedAt: now }
|
|
: d
|
|
)
|
|
);
|
|
notify.success(t('configuration.updated'));
|
|
} else {
|
|
const newDept: Department = {
|
|
id: uid(),
|
|
code: values.code,
|
|
names: { en: values.nameEn, am: values.nameAm },
|
|
description: values.description,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
};
|
|
setDepartments((prev) => [...prev, newDept]);
|
|
notify.success(t('configuration.created'));
|
|
}
|
|
resetDeptForm();
|
|
});
|
|
|
|
return (
|
|
<>
|
|
<Group justify="space-between" mb="md">
|
|
<Text fw={600} size="sm">{t('configuration.departmentsList')}</Text>
|
|
{!showDeptForm && (
|
|
<Button
|
|
variant="light"
|
|
leftSection={<IconPlus size={16} />}
|
|
onClick={() => { resetDeptForm(); setShowDeptForm(true); }}
|
|
size="sm"
|
|
>
|
|
{t('configuration.addDepartment')}
|
|
</Button>
|
|
)}
|
|
</Group>
|
|
|
|
{showDeptForm && (
|
|
<Paper p="md" withBorder mb="md" radius="md">
|
|
<form onSubmit={handleDeptSubmit}>
|
|
<Stack gap="sm">
|
|
<TextInput
|
|
label={t('configuration.code')}
|
|
placeholder="e.g., IT, HR"
|
|
{...deptForm.getInputProps('code')}
|
|
size="sm"
|
|
/>
|
|
<TextInput
|
|
label={t('configuration.nameEn')}
|
|
placeholder="English name"
|
|
{...deptForm.getInputProps('nameEn')}
|
|
size="sm"
|
|
/>
|
|
<TextInput
|
|
label={t('configuration.nameAm')}
|
|
placeholder="የአማርኛ ስም"
|
|
{...deptForm.getInputProps('nameAm')}
|
|
size="sm"
|
|
/>
|
|
<Textarea
|
|
label={t('configuration.description')}
|
|
placeholder="Optional description"
|
|
{...deptForm.getInputProps('description')}
|
|
size="sm"
|
|
autosize
|
|
minRows={2}
|
|
/>
|
|
<Group justify="flex-end">
|
|
<Button variant="default" onClick={resetDeptForm} size="sm">
|
|
{t('configuration.cancel')}
|
|
</Button>
|
|
<Button type="submit" size="sm">
|
|
{editingDept ? t('configuration.update') : t('configuration.create')}
|
|
</Button>
|
|
</Group>
|
|
</Stack>
|
|
</form>
|
|
</Paper>
|
|
)}
|
|
|
|
<Table striped highlightOnHover>
|
|
<Table.Thead>
|
|
<Table.Tr>
|
|
<Table.Th>{t('configuration.code')}</Table.Th>
|
|
<Table.Th>{t('configuration.nameEn')}</Table.Th>
|
|
<Table.Th>{t('configuration.nameAm')}</Table.Th>
|
|
<Table.Th>{t('configuration.description')}</Table.Th>
|
|
<Table.Th />
|
|
</Table.Tr>
|
|
</Table.Thead>
|
|
<Table.Tbody>
|
|
{departments.map((dept) => (
|
|
<Table.Tr key={dept.id}>
|
|
<Table.Td>
|
|
<Badge size="sm" variant="light" color="blue">{dept.code}</Badge>
|
|
</Table.Td>
|
|
<Table.Td>{dept.names.en}</Table.Td>
|
|
<Table.Td>{dept.names.am}</Table.Td>
|
|
<Table.Td>
|
|
<Text size="sm" lineClamp={2} maw={200}>{dept.description}</Text>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Group gap="xs">
|
|
<ActionIcon variant="subtle" color="blue" size="sm" onClick={() => handleEditDept(dept)}>
|
|
<IconEdit size={14} />
|
|
</ActionIcon>
|
|
<ActionIcon variant="subtle" color="red" size="sm" onClick={() => handleDeleteDept(dept)}>
|
|
<IconTrash size={14} />
|
|
</ActionIcon>
|
|
</Group>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
))}
|
|
{departments.length === 0 && (
|
|
<Table.Tr>
|
|
<Table.Td colSpan={5}>
|
|
<Text c="dimmed" ta="center" py="xl">
|
|
{t('configuration.noDepartments')}
|
|
</Text>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
)}
|
|
</Table.Tbody>
|
|
</Table>
|
|
|
|
<Modal opened={deleteOpened} onClose={closeDelete} title={t('configuration.confirmDelete')} size="sm">
|
|
<Text mb="md">
|
|
{t('configuration.deleteConfirmText', { name: deleteTarget?.names.en ?? '' })}
|
|
</Text>
|
|
<Group justify="flex-end">
|
|
<Button variant="default" onClick={closeDelete} size="sm">{t('configuration.cancel')}</Button>
|
|
<Button color="red" onClick={confirmDeleteDept} size="sm">{t('configuration.delete')}</Button>
|
|
</Group>
|
|
</Modal>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function ProfessionTab() {
|
|
const { t } = useTranslation();
|
|
const [departments] = useState<Department[]>(MOCK_DEPARTMENTS);
|
|
const [professions, setProfessions] = useState<Profession[]>(MOCK_PROFESSIONS);
|
|
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 profForm = useForm({
|
|
initialValues: { code: '', nameEn: '', nameAm: '', description: '', departmentId: '' },
|
|
validate: {
|
|
code: (v) => (!v ? t('configuration.validation.codeRequired') : null),
|
|
nameEn: (v) => (!v ? t('configuration.validation.nameEnRequired') : null),
|
|
nameAm: (v) => (!v ? t('configuration.validation.nameAmRequired') : null),
|
|
departmentId: (v) => (!v ? t('configuration.validation.departmentRequired') : null),
|
|
},
|
|
});
|
|
|
|
const resetProfForm = () => {
|
|
profForm.reset();
|
|
setEditingProf(null);
|
|
setShowProfForm(false);
|
|
};
|
|
|
|
const handleEditProf = (prof: Profession) => {
|
|
setEditingProf(prof);
|
|
profForm.setValues({
|
|
code: prof.code,
|
|
nameEn: prof.names.en,
|
|
nameAm: prof.names.am,
|
|
description: prof.description,
|
|
departmentId: prof.departmentId,
|
|
});
|
|
setShowProfForm(true);
|
|
};
|
|
|
|
const handleDeleteProf = (prof: Profession) => {
|
|
setDeleteTarget(prof);
|
|
openDelete();
|
|
};
|
|
|
|
const confirmDeleteProf = () => {
|
|
if (!deleteTarget) return;
|
|
setProfessions((prev) => prev.filter((p) => p.id !== deleteTarget.id));
|
|
notify.success(t('configuration.deleted'));
|
|
closeDelete();
|
|
setDeleteTarget(null);
|
|
};
|
|
|
|
const handleProfSubmit = profForm.onSubmit((values) => {
|
|
const now = new Date().toISOString();
|
|
if (editingProf) {
|
|
setProfessions((prev) =>
|
|
prev.map((p) =>
|
|
p.id === editingProf.id
|
|
? { ...p, code: values.code, names: { en: values.nameEn, am: values.nameAm }, description: values.description, departmentId: values.departmentId, updatedAt: now }
|
|
: p
|
|
)
|
|
);
|
|
notify.success(t('configuration.updated'));
|
|
} else {
|
|
const newProf: Profession = {
|
|
id: uid(),
|
|
code: values.code,
|
|
names: { en: values.nameEn, am: values.nameAm },
|
|
description: values.description,
|
|
departmentId: values.departmentId,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
};
|
|
setProfessions((prev) => [...prev, newProf]);
|
|
notify.success(t('configuration.created'));
|
|
}
|
|
resetProfForm();
|
|
});
|
|
|
|
const deptOptions = departments.map((d) => ({
|
|
value: d.id,
|
|
label: `${d.code} — ${d.names.en}`,
|
|
}));
|
|
|
|
const getDeptName = (deptId: string) => {
|
|
const dept = departments.find((d) => d.id === deptId);
|
|
return dept ? `${dept.code} — ${dept.names.en}` : '-';
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Group justify="space-between" mb="md">
|
|
<Text fw={600} size="sm">{t('configuration.professionsList')}</Text>
|
|
{!showProfForm && (
|
|
<Button
|
|
variant="light"
|
|
leftSection={<IconPlus size={16} />}
|
|
onClick={() => { resetProfForm(); setShowProfForm(true); }}
|
|
size="sm"
|
|
>
|
|
{t('configuration.addProfession')}
|
|
</Button>
|
|
)}
|
|
</Group>
|
|
|
|
{showProfForm && (
|
|
<Paper p="md" withBorder mb="md" radius="md">
|
|
<form onSubmit={handleProfSubmit}>
|
|
<Stack gap="sm">
|
|
<TextInput
|
|
label={t('configuration.code')}
|
|
placeholder="e.g., SWE, HRM"
|
|
{...profForm.getInputProps('code')}
|
|
size="sm"
|
|
/>
|
|
<TextInput
|
|
label={t('configuration.nameEn')}
|
|
placeholder="English name"
|
|
{...profForm.getInputProps('nameEn')}
|
|
size="sm"
|
|
/>
|
|
<TextInput
|
|
label={t('configuration.nameAm')}
|
|
placeholder="የአማርኛ ስም"
|
|
{...profForm.getInputProps('nameAm')}
|
|
size="sm"
|
|
/>
|
|
<Textarea
|
|
label={t('configuration.description')}
|
|
placeholder="Optional description"
|
|
{...profForm.getInputProps('description')}
|
|
size="sm"
|
|
autosize
|
|
minRows={2}
|
|
/>
|
|
<Select
|
|
label={t('configuration.department')}
|
|
placeholder={t('configuration.selectDepartment')}
|
|
data={deptOptions}
|
|
{...profForm.getInputProps('departmentId')}
|
|
size="sm"
|
|
searchable
|
|
/>
|
|
<Group justify="flex-end">
|
|
<Button variant="default" onClick={resetProfForm} size="sm">
|
|
{t('configuration.cancel')}
|
|
</Button>
|
|
<Button type="submit" size="sm">
|
|
{editingProf ? t('configuration.update') : t('configuration.create')}
|
|
</Button>
|
|
</Group>
|
|
</Stack>
|
|
</form>
|
|
</Paper>
|
|
)}
|
|
|
|
<Table striped highlightOnHover>
|
|
<Table.Thead>
|
|
<Table.Tr>
|
|
<Table.Th>{t('configuration.code')}</Table.Th>
|
|
<Table.Th>{t('configuration.nameEn')}</Table.Th>
|
|
<Table.Th>{t('configuration.nameAm')}</Table.Th>
|
|
<Table.Th>{t('configuration.description')}</Table.Th>
|
|
<Table.Th>{t('configuration.department')}</Table.Th>
|
|
<Table.Th />
|
|
</Table.Tr>
|
|
</Table.Thead>
|
|
<Table.Tbody>
|
|
{professions.map((prof) => (
|
|
<Table.Tr key={prof.id}>
|
|
<Table.Td>
|
|
<Badge size="sm" variant="light" color="teal">{prof.code}</Badge>
|
|
</Table.Td>
|
|
<Table.Td>{prof.names.en}</Table.Td>
|
|
<Table.Td>{prof.names.am}</Table.Td>
|
|
<Table.Td>
|
|
<Text size="sm" lineClamp={2} maw={200}>{prof.description}</Text>
|
|
</Table.Td>
|
|
<Table.Td>{getDeptName(prof.departmentId)}</Table.Td>
|
|
<Table.Td>
|
|
<Group gap="xs">
|
|
<ActionIcon variant="subtle" color="blue" size="sm" onClick={() => handleEditProf(prof)}>
|
|
<IconEdit size={14} />
|
|
</ActionIcon>
|
|
<ActionIcon variant="subtle" color="red" size="sm" onClick={() => handleDeleteProf(prof)}>
|
|
<IconTrash size={14} />
|
|
</ActionIcon>
|
|
</Group>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
))}
|
|
{professions.length === 0 && (
|
|
<Table.Tr>
|
|
<Table.Td colSpan={6}>
|
|
<Text c="dimmed" ta="center" py="xl">
|
|
{t('configuration.noProfessions')}
|
|
</Text>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
)}
|
|
</Table.Tbody>
|
|
</Table>
|
|
|
|
<Modal opened={deleteOpened} onClose={closeDelete} title={t('configuration.confirmDelete')} size="sm">
|
|
<Text mb="md">
|
|
{t('configuration.deleteConfirmText', { name: deleteTarget?.names.en ?? '' })}
|
|
</Text>
|
|
<Group justify="flex-end">
|
|
<Button variant="default" onClick={closeDelete} size="sm">{t('configuration.cancel')}</Button>
|
|
<Button color="red" onClick={confirmDeleteProf} size="sm">{t('configuration.delete')}</Button>
|
|
</Group>
|
|
</Modal>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function ConfigurationPage() {
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<Stack gap="lg">
|
|
<Title order={2}>{t('configuration.title')}</Title>
|
|
|
|
<Tabs defaultValue="departments">
|
|
<Tabs.List>
|
|
<Tabs.Tab value="departments" leftSection={<IconBuilding size={16} />}>
|
|
{t('configuration.departments')}
|
|
</Tabs.Tab>
|
|
<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.List>
|
|
|
|
<Tabs.Panel value="departments" pt="md">
|
|
<DepartmentTab />
|
|
</Tabs.Panel>
|
|
|
|
<Tabs.Panel value="professions" pt="md">
|
|
<ProfessionTab />
|
|
</Tabs.Panel>
|
|
|
|
<Tabs.Panel value="locations" pt="md">
|
|
<LocationPage />
|
|
</Tabs.Panel>
|
|
</Tabs>
|
|
</Stack>
|
|
);
|
|
}
|