Files
emaui/apps/backoffice/src/app/features/location/components/LocationForm.tsx

203 lines
6.0 KiB
TypeScript

import { useEffect, useMemo } from 'react';
import {
TextInput,
Button,
Group,
Stack,
Paper,
Title,
Text,
Badge,
Select,
} from '@mantine/core';
import { useForm } from '@mantine/form';
import { useTranslation } from 'react-i18next';
import { notify } from '@ema-platform/ui';
import type { Location, LocationType } from '../types/location';
interface LocationFormValues {
code: string;
namesEn: string;
namesAm: string;
locationTypeId: string;
}
interface LocationFormProps {
locationTypes: LocationType[];
parentLocation?: Location | null;
editingLocation?: Location | null;
onSubmit: (values: {
code: string;
names: { en: string; am: string };
locationTypeId: string;
parentId?: string | null;
}) => void;
onCancel: () => void;
isSubmitting?: boolean;
}
export function LocationForm({
locationTypes,
parentLocation,
editingLocation,
onSubmit,
onCancel,
isSubmitting,
}: LocationFormProps) {
const { t, i18n } = useTranslation();
const locale = i18n.language as 'en' | 'am';
const isEditing = !!editingLocation;
const { type, allAtLevel, typesAvailable } = useMemo(() => {
if (isEditing) {
const lt = locationTypes.find((t) => t.id === editingLocation.locationTypeId);
return { type: lt ?? null, allAtLevel: [lt].filter(Boolean) as LocationType[], typesAvailable: false };
}
if (parentLocation) {
const parentLevel =
locationTypes.find((lt) => lt.id === parentLocation.locationTypeId)?.level ?? 0;
const nextLevel = locationTypes
.filter((lt) => lt.level === parentLevel + 1)
.sort((a, b) => a.level - b.level);
return { type: nextLevel[0] ?? null, allAtLevel: nextLevel, typesAvailable: nextLevel.length > 1 };
}
const minLevel = Math.min(...locationTypes.map((lt) => lt.level));
const roots = locationTypes
.filter((lt) => lt.level === minLevel)
.sort((a, b) => a.level - b.level);
return { type: roots[0] ?? null, allAtLevel: roots, typesAvailable: roots.length > 1 };
}, [locationTypes, parentLocation, editingLocation, isEditing]);
const form = useForm<LocationFormValues>({
initialValues: {
code: '',
namesEn: '',
namesAm: '',
locationTypeId: typesAvailable ? '' : (type?.id ?? ''),
},
validate: {
code: (v) => (!v ? t('location.validation.codeRequired') : null),
namesEn: (v) => (!v ? t('location.validation.nameEnRequired') : null),
namesAm: (v) => (!v ? t('location.validation.nameAmRequired') : null),
locationTypeId: (v) => {
if (typesAvailable && !v) return t('location.validation.typeRequired');
return null;
},
},
});
useEffect(() => {
if (editingLocation) {
form.setValues({
code: editingLocation.code,
namesEn: editingLocation.names.en,
namesAm: editingLocation.names.am,
locationTypeId: editingLocation.locationTypeId,
});
}
}, [editingLocation]);
const handleSubmit = form.onSubmit((values) => {
const resolvedTypeId = typesAvailable ? values.locationTypeId : type?.id;
if (!resolvedTypeId) {
notify.error(t('location.noTypesAvailable'));
return;
}
onSubmit({
code: values.code,
names: { en: values.namesEn, am: values.namesAm },
locationTypeId: resolvedTypeId,
parentId: editingLocation ? editingLocation.parentId : parentLocation?.id ?? null,
});
});
const title = isEditing
? t('location.editTitle')
: t('location.addTitle');
return (
<Paper p="lg" radius="md" withBorder>
<Title order={5} mb="md">
{title}
</Title>
{parentLocation && !isEditing && (
<TextInput
label={t('location.parent')}
value={parentLocation.names[locale]}
disabled
mb="sm"
size="sm"
/>
)}
<form onSubmit={handleSubmit}>
<Stack gap="sm">
{typesAvailable ? (
<Select
label={t('location.type')}
placeholder={t('location.selectType')}
data={allAtLevel.map((lt) => ({
value: lt.id,
// Not every locale is filled in on every row, and an option
// with no label is unpickable — fall back to English, then the
// code, which always exists.
label: lt.names[locale] || lt.names.en || lt.code,
}))}
{...form.getInputProps('locationTypeId')}
size="sm"
required
nothingFoundMessage={t('location.noTypesAvailable')}
/>
) : (
<div>
<Text size="xs" c="dimmed" fw={500} mb={4}>
{t('location.type')}
</Text>
{type ? (
<Badge size="lg" variant="light" color="blue">
{type.names[locale]}
</Badge>
) : (
<Text size="sm" c="red">
{t('location.noTypesAvailable')}
</Text>
)}
</div>
)}
<TextInput
label={t('location.code')}
placeholder="e.g., ETH, AA, KK"
{...form.getInputProps('code')}
size="sm"
required
/>
<TextInput
label={t('location.nameEn')}
placeholder="English name"
{...form.getInputProps('namesEn')}
size="sm"
required
/>
<TextInput
label={t('location.nameAm')}
placeholder="የአማርኛ ስም"
{...form.getInputProps('namesAm')}
size="sm"
required
/>
<Group justify="flex-end" mt="md">
<Button variant="default" onClick={onCancel} size="sm">
{t('location.cancel')}
</Button>
<Button type="submit" loading={isSubmitting} size="sm">
{isEditing ? t('location.update') : t('location.create')}
</Button>
</Group>
</Stack>
</form>
</Paper>
);
}