mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-30 12:18:12 +00:00
feat: add i18n support and localized labels to certification and profession features commit
This commit is contained in:
@@ -10,7 +10,6 @@ import {
|
||||
Modal,
|
||||
Text,
|
||||
TextInput,
|
||||
Textarea,
|
||||
Paper,
|
||||
Loader,
|
||||
Center,
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
NumberInput,
|
||||
} from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IconEdit, IconTrash, IconPlus, IconInfoCircle } from '@tabler/icons-react';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
import { useGetCertificationsQuery } from '../../certification/api/certification-api';
|
||||
@@ -44,8 +44,6 @@ function QuestionForm({
|
||||
certificationId: string;
|
||||
titleEn: string;
|
||||
titleAm: string;
|
||||
descriptionEn: string;
|
||||
descriptionAm: string;
|
||||
form: string;
|
||||
points: number;
|
||||
days: number;
|
||||
@@ -54,11 +52,11 @@ function QuestionForm({
|
||||
}, isEdit: boolean) => void;
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [certificationId, setCertificationId] = useState<string | null>(editing?.certificationId ?? null);
|
||||
const [titleEn, setTitleEn] = useState(editing?.title?.en ?? '');
|
||||
const [titleAm, setTitleAm] = useState(editing?.title?.am ?? '');
|
||||
const [descriptionEn, setDescriptionEn] = useState(editing?.description?.en ?? '');
|
||||
const [descriptionAm, setDescriptionAm] = useState(editing?.description?.am ?? '');
|
||||
|
||||
const [form, setForm] = useState<string | null>(editing?.form ?? null);
|
||||
const [points, setPoints] = useState<number>(editing?.points ?? 0);
|
||||
const [days, setDays] = useState(editing?.time?.days ?? 0);
|
||||
@@ -71,29 +69,29 @@ function QuestionForm({
|
||||
notify.error('Please fill all required fields');
|
||||
return;
|
||||
}
|
||||
onSubmit({ certificationId, titleEn, titleAm, descriptionEn, descriptionAm, form, points, days, hours, minutes }, !!editing);
|
||||
onSubmit({
|
||||
certificationId, titleEn, titleAm, form, points, days, hours, minutes
|
||||
}, !!editing);
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper p="md" withBorder mb="md" radius="md">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack gap="sm">
|
||||
<Select label="Certification" placeholder="Select certification" data={certOptions} value={certificationId} onChange={setCertificationId} size="sm" searchable required />
|
||||
<TextInput label="Title (English)" placeholder="Question in English" value={titleEn} onChange={(e) => setTitleEn(e.currentTarget.value)} size="sm" required />
|
||||
<TextInput label="Title (Amharic)" placeholder="ጥያቄ በአማርኛ" value={titleAm} onChange={(e) => setTitleAm(e.currentTarget.value)} size="sm" required />
|
||||
<Textarea label="Description (English)" placeholder="Question description in English" value={descriptionEn} onChange={(e) => setDescriptionEn(e.currentTarget.value)} size="sm" />
|
||||
<Textarea label="Description (Amharic)" placeholder="ስለ ዝርዝር መረጃ" value={descriptionAm} onChange={(e) => setDescriptionAm(e.currentTarget.value)} size="sm" />
|
||||
<Select label="Form" placeholder="Select form" data={[{ value: 'ESSAY', label: 'Essay' }, { value: 'CHOICE', label: 'Choice' }]} value={form} onChange={setForm} size="sm" required />
|
||||
<NumberInput label="Points" placeholder="Points" value={points} onChange={(v) => setPoints(Number(v))} min={0} size="sm" required />
|
||||
<Text fz="sm" fw={500}>Time Allowed</Text>
|
||||
<Select label={t('question.form.certification')} placeholder={t('question.form.selectCertification')} data={certOptions} value={certificationId} onChange={setCertificationId} size="sm" searchable required />
|
||||
<TextInput label={t('question.form.titleEn')} placeholder={t('question.form.titleEnPlaceholder')} value={titleEn} onChange={(e) => setTitleEn(e.currentTarget.value)} size="sm" required />
|
||||
<TextInput label={t('question.form.titleAm')} placeholder={t('question.form.titleAmPlaceholder')} value={titleAm} onChange={(e) => setTitleAm(e.currentTarget.value)} size="sm" required />
|
||||
<Select label={t('question.form.form')} placeholder={t('question.form.selectForm')} data={[{ value: 'ESSAY', label: t('question.form.essay') }, { value: 'CHOICE', label: t('question.form.choice') }]} value={form} onChange={setForm} size="sm" required />
|
||||
<NumberInput label={t('question.form.points')} placeholder={t('question.form.pointsPlaceholder')} value={points} onChange={(v) => setPoints(Number(v))} min={0} size="sm" required />
|
||||
<Text fz="sm" fw={500}>{t('question.form.timeAllowed')}</Text>
|
||||
<Group gap="sm" grow>
|
||||
<NumberInput label="Days" value={days} onChange={(v) => setDays(Number(v))} min={0} size="sm" />
|
||||
<NumberInput label="Hours" value={hours} onChange={(v) => setHours(Number(v))} min={0} size="sm" />
|
||||
<NumberInput label="Minutes" value={minutes} onChange={(v) => setMinutes(Number(v))} min={0} size="sm" />
|
||||
<NumberInput label={t('question.form.days')} value={days} onChange={(v) => setDays(Number(v))} min={0} size="sm" />
|
||||
<NumberInput label={t('question.form.hours')} value={hours} onChange={(v) => setHours(Number(v))} min={0} size="sm" />
|
||||
<NumberInput label={t('question.form.minutes')} value={minutes} onChange={(v) => setMinutes(Number(v))} min={0} size="sm" />
|
||||
</Group>
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={onCancel} size="sm">Cancel</Button>
|
||||
<Button type="submit" size="sm" loading={isSubmitting}>{editing ? 'Update' : 'Create'}</Button>
|
||||
<Button variant="default" onClick={onCancel} size="sm">{t('question.cancel')}</Button>
|
||||
<Button type="submit" size="sm" loading={isSubmitting}>{editing ? t('question.update') : t('question.create')}</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
@@ -102,6 +100,8 @@ function QuestionForm({
|
||||
}
|
||||
|
||||
export function QuestionPage() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const locale = i18n.language as 'en' | 'am';
|
||||
const { data: certRes } = useGetCertificationsQuery();
|
||||
const { data, isLoading, isError } = useGetQuestionsQuery();
|
||||
const [createQ, { isLoading: isCreating }] = useCreateQuestionMutation();
|
||||
@@ -117,33 +117,31 @@ export function QuestionPage() {
|
||||
const [deleteOpened, { open: openDelete, close: closeDelete }] = useDisclosure(false);
|
||||
const [certFilter, setCertFilter] = useState<string | null>(null);
|
||||
|
||||
const certOptions = certifications.filter((c) => c.isActive).map((c) => ({ value: c.id, label: c.name.en }));
|
||||
const certOptions = certifications.filter((c) => c.isActive).map((c) => ({ value: c.id, label: c.name[locale] }));
|
||||
|
||||
const filtered = questions.filter((q) => !certFilter || q.certificationId === certFilter);
|
||||
|
||||
const getCertName = (id: string) => certifications.find((c) => c.id === id)?.name?.en ?? '-';
|
||||
const getCertName = (id: string) => certifications.find((c) => c.id === id)?.name?.[locale] ?? '-';
|
||||
|
||||
const resetForm = () => { setEditing(null); setShowForm(false); };
|
||||
|
||||
const handleSubmit = async (values: {
|
||||
certificationId: string; titleEn: string; titleAm: string;
|
||||
descriptionEn: string; descriptionAm: string; form: string;
|
||||
points: number; days: number; hours: number; minutes: number;
|
||||
form: string; points: number; days: number; hours: number; minutes: number;
|
||||
}, isEdit: boolean) => {
|
||||
const title = { en: values.titleEn, am: values.titleAm };
|
||||
const description = { en: values.descriptionEn, am: values.descriptionAm };
|
||||
const time = { days: values.days, hours: values.hours, minutes: values.minutes };
|
||||
try {
|
||||
if (isEdit && editing) {
|
||||
await updateQ({ id: editing.id, certificationId: values.certificationId, title, /* description, */ form: values.form as QuestionForm, points: values.points, time }).unwrap();
|
||||
notify.success('Question updated');
|
||||
await updateQ({ id: editing.id, certificationId: values.certificationId, title, form: values.form as QuestionForm, points: values.points, time }).unwrap();
|
||||
notify.success(t('question.updated'));
|
||||
} else {
|
||||
await createQ({ certificationId: values.certificationId, title, description, form: values.form as QuestionForm, points: values.points, time }).unwrap();
|
||||
notify.success('Question created');
|
||||
await createQ({ certificationId: values.certificationId, title, description: { en: '', am: '' }, form: values.form as QuestionForm, points: values.points, time }).unwrap();
|
||||
notify.success(t('question.created'));
|
||||
}
|
||||
resetForm();
|
||||
} catch {
|
||||
notify.error('Operation failed');
|
||||
notify.error(t('question.error'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -151,27 +149,24 @@ export function QuestionPage() {
|
||||
if (!deleteTarget) return;
|
||||
try {
|
||||
await deleteQ(deleteTarget.id).unwrap();
|
||||
notify.success('Question deleted');
|
||||
notify.success(t('question.deleted'));
|
||||
closeDelete();
|
||||
setDeleteTarget(null);
|
||||
} catch {
|
||||
notify.error('Failed to delete');
|
||||
notify.error(t('question.error'));
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) return <Center py="xl"><Loader /></Center>;
|
||||
if (isError) return <Alert icon={<IconInfoCircle size={16} />} color="red" title="Error loading questions" />;
|
||||
if (isError) return <Alert icon={<IconInfoCircle size={16} />} color="red" title={t('question.loadError')} />;
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Group justify="space-between" align="flex-end">
|
||||
<div>
|
||||
<Title order={2}>Questions</Title>
|
||||
<Text fz="sm" c="dimmed">Manage the question pool for examinations</Text>
|
||||
</div>
|
||||
<Title order={2}>{t('question.title')}</Title>
|
||||
{!showForm && (
|
||||
<Button variant="light" leftSection={<IconPlus size={16} />} onClick={() => setShowForm(true)} size="sm">
|
||||
Add Question
|
||||
{t('question.addQuestion')}
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
@@ -188,29 +183,29 @@ export function QuestionPage() {
|
||||
|
||||
<Paper withBorder radius="md">
|
||||
<Group p="md" justify="space-between" wrap="wrap" gap="sm">
|
||||
<Text fw={600}>Question Pool</Text>
|
||||
<Select placeholder="Filter by certification" data={[{ value: '', label: 'All' }, ...certOptions]} value={certFilter} onChange={(v) => setCertFilter(v ?? null)} size="sm" style={{ width: 280 }} clearable />
|
||||
<Text fw={600}>{t('question.pool')}</Text>
|
||||
<Select placeholder={t('question.filterByCertification')} data={[{ value: '', label: 'All' }, ...certOptions]} value={certFilter} onChange={(v) => setCertFilter(v ?? null)} size="sm" style={{ width: 280 }} clearable />
|
||||
</Group>
|
||||
<Table striped highlightOnHover>
|
||||
<Table.Thead bg="var(--mantine-color-default-hover)">
|
||||
<Table.Tr>
|
||||
<Table.Th>Title (EN)</Table.Th>
|
||||
<Table.Th>Certification</Table.Th>
|
||||
<Table.Th>Form</Table.Th>
|
||||
<Table.Th>Points</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>{t('question.columns.title')}</Table.Th>
|
||||
<Table.Th>{t('question.columns.certification')}</Table.Th>
|
||||
<Table.Th>{t('question.columns.form')}</Table.Th>
|
||||
<Table.Th>{t('question.columns.points')}</Table.Th>
|
||||
<Table.Th>{t('question.columns.status')}</Table.Th>
|
||||
<Table.Th />
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{filtered.map((q) => (
|
||||
<Table.Tr key={q.id}>
|
||||
<Table.Td><Text fz="sm" maw={300} lineClamp={2}>{q.title.en}</Text></Table.Td>
|
||||
<Table.Td><Text fz="sm" maw={300} lineClamp={2}>{q.title[locale]}</Text></Table.Td>
|
||||
<Table.Td><Text fz="sm">{getCertName(q.certificationId)}</Text></Table.Td>
|
||||
<Table.Td><Badge size="sm" variant="light" color={q.form === 'ESSAY' ? 'blue' : 'violet'}>{q.form}</Badge></Table.Td>
|
||||
<Table.Td><Badge size="sm" variant="light" color={q.form === 'ESSAY' ? 'blue' : 'violet'}>{t(`question.form.${q.form === 'ESSAY' ? 'essay' : 'choice'}`)}</Badge></Table.Td>
|
||||
<Table.Td><Text fz="sm" fw={600}>{q.points}</Text></Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="sm" variant="light" color={q.isActive ? 'teal' : 'gray'}>{q.isActive ? 'Active' : 'Inactive'}</Badge>
|
||||
<Badge size="sm" variant="light" color={q.isActive ? 'teal' : 'gray'}>{q.isActive ? t('question.status.active') : t('question.status.inactive')}</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap="xs">
|
||||
@@ -227,7 +222,7 @@ export function QuestionPage() {
|
||||
{filtered.length === 0 && (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={6}>
|
||||
<Text c="dimmed" ta="center" py="xl">No questions found</Text>
|
||||
<Text c="dimmed" ta="center" py="xl">{t('question.noQuestions')}</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
@@ -235,11 +230,11 @@ export function QuestionPage() {
|
||||
</Table>
|
||||
</Paper>
|
||||
|
||||
<Modal opened={deleteOpened} onClose={closeDelete} title="Delete Question" size="sm">
|
||||
<Text mb="md">Are you sure you want to delete this question?</Text>
|
||||
<Modal opened={deleteOpened} onClose={closeDelete} title={t('question.confirmDelete')} size="sm">
|
||||
<Text mb="md">{t('question.deleteConfirmText')}</Text>
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={closeDelete} size="sm">Cancel</Button>
|
||||
<Button color="red" onClick={handleDelete} size="sm">Delete</Button>
|
||||
<Button variant="default" onClick={closeDelete} size="sm">{t('question.cancel')}</Button>
|
||||
<Button color="red" onClick={handleDelete} size="sm">{t('question.delete')}</Button>
|
||||
</Group>
|
||||
</Modal>
|
||||
</Stack>
|
||||
|
||||
Reference in New Issue
Block a user