mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-29 14:08:12 +00:00
added location registeration
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import {
|
||||
Paper,
|
||||
Title,
|
||||
Text,
|
||||
Group,
|
||||
Button,
|
||||
Stack,
|
||||
Badge,
|
||||
Divider,
|
||||
ActionIcon,
|
||||
Menu,
|
||||
rem,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconEdit,
|
||||
IconTrash,
|
||||
IconPlus,
|
||||
IconDots,
|
||||
} from '@tabler/icons-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { Location, LocationType } from '../types/location';
|
||||
|
||||
interface LocationDetailProps {
|
||||
location: Location;
|
||||
locationTypes: LocationType[];
|
||||
onAddChild: () => void;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export function LocationDetail({
|
||||
location,
|
||||
locationTypes,
|
||||
onAddChild,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: LocationDetailProps) {
|
||||
const { t } = useTranslation();
|
||||
const typeInfo = locationTypes.find(
|
||||
(lt) => lt.id === location.locationTypeId,
|
||||
);
|
||||
const isLeaf =
|
||||
!locationTypes.some(
|
||||
(lt) => lt.level === (typeInfo?.level ?? 0) + 1,
|
||||
);
|
||||
|
||||
return (
|
||||
<Paper p="lg" radius="md" withBorder>
|
||||
<Group justify="space-between" mb="xs">
|
||||
<Title order={4}>{location.names.en}</Title>
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
variant="light"
|
||||
size="sm"
|
||||
leftSection={<IconPlus size={16} />}
|
||||
onClick={onAddChild}
|
||||
disabled={isLeaf}
|
||||
>
|
||||
{t('location.addSubLocation')}
|
||||
</Button>
|
||||
<Menu shadow="md" width={180}>
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="subtle" color="gray" size="sm">
|
||||
<IconDots size={16} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
leftSection={<IconEdit size={16} />}
|
||||
onClick={onEdit}
|
||||
>
|
||||
{t('location.edit')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<IconTrash size={16} />}
|
||||
color="red"
|
||||
onClick={onDelete}
|
||||
>
|
||||
{t('location.delete')}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Group gap="xs" mb="md">
|
||||
<Badge size="lg" variant="light" color="blue">
|
||||
{location.code}
|
||||
</Badge>
|
||||
{typeInfo && (
|
||||
<Badge size="lg" variant="light" color="teal">
|
||||
{typeInfo.names.en}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Divider mb="md" />
|
||||
|
||||
<Stack gap="sm">
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={600}>
|
||||
{t('location.nameEn')}
|
||||
</Text>
|
||||
<Text size="sm">{location.names.en}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={600}>
|
||||
{t('location.nameAm')}
|
||||
</Text>
|
||||
<Text size="sm">{location.names.am}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={600}>
|
||||
{t('location.code')}
|
||||
</Text>
|
||||
<Text size="sm">{location.code}</Text>
|
||||
</div>
|
||||
{typeInfo && (
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={600}>
|
||||
{t('location.type')}
|
||||
</Text>
|
||||
<Text size="sm">{typeInfo.names.en} (Level {typeInfo.level})</Text>
|
||||
</div>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import {
|
||||
TextInput,
|
||||
Button,
|
||||
Group,
|
||||
Stack,
|
||||
Paper,
|
||||
Title,
|
||||
Text,
|
||||
Badge,
|
||||
} 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;
|
||||
}
|
||||
|
||||
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 } = useTranslation();
|
||||
|
||||
const isEditing = !!editingLocation;
|
||||
|
||||
const { type, typesAvailable } = useMemo(() => {
|
||||
if (isEditing) {
|
||||
const lt = locationTypes.find((t) => t.id === editingLocation.locationTypeId);
|
||||
return { type: lt ?? null, 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, 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, typesAvailable: roots.length > 1 };
|
||||
}, [locationTypes, parentLocation, editingLocation, isEditing]);
|
||||
|
||||
const form = useForm<LocationFormValues>({
|
||||
initialValues: {
|
||||
code: '',
|
||||
namesEn: '',
|
||||
namesAm: '',
|
||||
},
|
||||
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),
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (editingLocation) {
|
||||
form.setValues({
|
||||
code: editingLocation.code,
|
||||
namesEn: editingLocation.names.en,
|
||||
namesAm: editingLocation.names.am,
|
||||
});
|
||||
}
|
||||
}, [editingLocation]);
|
||||
|
||||
const handleSubmit = form.onSubmit((values) => {
|
||||
if (!type) {
|
||||
notify.error(t('location.noTypesAvailable'));
|
||||
return;
|
||||
}
|
||||
onSubmit({
|
||||
code: values.code,
|
||||
names: { en: values.namesEn, am: values.namesAm },
|
||||
locationTypeId: type.id,
|
||||
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.en}
|
||||
disabled
|
||||
mb="sm"
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack gap="sm">
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" fw={500} mb={4}>
|
||||
{t('location.type')}
|
||||
</Text>
|
||||
{type ? (
|
||||
<Badge size="lg" variant="light" color="blue">
|
||||
{type.names.en}
|
||||
</Badge>
|
||||
) : (
|
||||
<Text size="sm" c="red">
|
||||
{t('location.noTypesAvailable')}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
{typesAvailable && (
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('location.multipleTypesHint')}
|
||||
</Text>
|
||||
)}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Text,
|
||||
Loader,
|
||||
Group,
|
||||
Badge,
|
||||
TextInput,
|
||||
UnstyledButton,
|
||||
Collapse,
|
||||
Stack,
|
||||
Box,
|
||||
rem,
|
||||
Center,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconChevronRight,
|
||||
IconChevronDown,
|
||||
IconSearch,
|
||||
IconMapPin,
|
||||
} from '@tabler/icons-react';
|
||||
import { useGetLocationsQuery } from '../api/location-api';
|
||||
import type { Location } from '../types/location';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface LocationTreeProps {
|
||||
selectedId: string | null;
|
||||
onSelect: (location: Location) => void;
|
||||
}
|
||||
|
||||
function flatListToTree(
|
||||
items: Location[],
|
||||
parentId: string | null,
|
||||
): Location[] {
|
||||
return items
|
||||
.filter((item) => item.parentId === parentId)
|
||||
.map((item) => ({
|
||||
...item,
|
||||
children: flatListToTree(items, item.id),
|
||||
}));
|
||||
}
|
||||
|
||||
function TreeNode({
|
||||
location,
|
||||
selectedId,
|
||||
onSelect,
|
||||
depth,
|
||||
}: {
|
||||
location: Location;
|
||||
selectedId: string | null;
|
||||
onSelect: (location: Location) => void;
|
||||
depth: number;
|
||||
}) {
|
||||
const [opened, setOpened] = useState(depth < 1);
|
||||
const isSelected = selectedId === location.id;
|
||||
const hasChildren =
|
||||
Array.isArray(location.children) && location.children.length > 0;
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setOpened((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const select = useCallback(() => {
|
||||
onSelect(location);
|
||||
}, [onSelect, location]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<UnstyledButton
|
||||
onClick={select}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: rem(6),
|
||||
width: '100%',
|
||||
padding: `${rem(6)} ${rem(8)}`,
|
||||
paddingLeft: rem(12 + depth * 20),
|
||||
borderRadius: rem(8),
|
||||
backgroundColor: isSelected
|
||||
? 'var(--mantine-color-blue-light)'
|
||||
: 'transparent',
|
||||
color: isSelected
|
||||
? 'var(--mantine-color-blue-filled)'
|
||||
: 'var(--mantine-color-text)',
|
||||
cursor: 'pointer',
|
||||
border: 'none',
|
||||
fontSize: rem(14),
|
||||
transition: 'background-color 100ms ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isSelected)
|
||||
e.currentTarget.style.backgroundColor =
|
||||
'var(--mantine-color-gray-0)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isSelected)
|
||||
e.currentTarget.style.backgroundColor = 'transparent';
|
||||
}}
|
||||
>
|
||||
{hasChildren && (
|
||||
<Box
|
||||
component="button"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
toggle();
|
||||
}}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: rem(18),
|
||||
height: rem(18),
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: 0,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{opened ? (
|
||||
<IconChevronDown size={14} stroke={1.5} />
|
||||
) : (
|
||||
<IconChevronRight size={14} stroke={1.5} />
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{!hasChildren && <Box w={rem(18)} flexShrink={0} />}
|
||||
<IconMapPin
|
||||
size={14}
|
||||
stroke={1.5}
|
||||
style={{ flexShrink: 0, opacity: 0.6 }}
|
||||
/>
|
||||
<Text size="sm" truncate style={{ flex: 1 }}>
|
||||
{location.names.en}
|
||||
</Text>
|
||||
</UnstyledButton>
|
||||
{hasChildren && (
|
||||
<Collapse in={opened}>
|
||||
<Stack gap={2} mt={2}>
|
||||
{location.children!.map((child) => (
|
||||
<TreeNode
|
||||
key={child.id}
|
||||
location={child}
|
||||
selectedId={selectedId}
|
||||
onSelect={onSelect}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Collapse>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function LocationTree({
|
||||
selectedId,
|
||||
onSelect,
|
||||
}: LocationTreeProps) {
|
||||
const { t } = useTranslation();
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const { data: allLocationsRes, isLoading } = useGetLocationsQuery({ take: 10000 });
|
||||
|
||||
const tree = useMemo(() => {
|
||||
if (!allLocationsRes?.items) return [];
|
||||
const roots = flatListToTree(allLocationsRes.items, null);
|
||||
return roots;
|
||||
}, [allLocationsRes]);
|
||||
|
||||
const filteredTree = useMemo(() => {
|
||||
if (!search) return tree;
|
||||
|
||||
const matches = (loc: Location): boolean => {
|
||||
const nameMatch = loc.names.en
|
||||
.toLowerCase()
|
||||
.includes(search.toLowerCase());
|
||||
const childMatch =
|
||||
Array.isArray(loc.children) &&
|
||||
loc.children.some(matches);
|
||||
return nameMatch || childMatch;
|
||||
};
|
||||
|
||||
const filterNodes = (nodes: Location[]): Location[] =>
|
||||
nodes
|
||||
.filter(matches)
|
||||
.map((node) => ({
|
||||
...node,
|
||||
children: Array.isArray(node.children)
|
||||
? filterNodes(node.children)
|
||||
: [],
|
||||
}));
|
||||
|
||||
return filterNodes(tree);
|
||||
}, [tree, search]);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(loc: Location) => {
|
||||
onSelect(loc);
|
||||
},
|
||||
[onSelect],
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Center py="xl">
|
||||
<Loader />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextInput
|
||||
placeholder={t('location.search')}
|
||||
leftSection={<IconSearch size={16} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
mb="sm"
|
||||
size="sm"
|
||||
/>
|
||||
{tree.length === 0 && (
|
||||
<Text c="dimmed" size="sm" ta="center" py="xl">
|
||||
{t('location.noLocations')}
|
||||
</Text>
|
||||
)}
|
||||
<Stack gap={2}>
|
||||
{filteredTree.map((root) => (
|
||||
<TreeNode
|
||||
key={root.id}
|
||||
location={root}
|
||||
selectedId={selectedId}
|
||||
onSelect={handleSelect}
|
||||
depth={0}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
TextInput,
|
||||
Button,
|
||||
Group,
|
||||
Stack,
|
||||
NumberInput,
|
||||
Table,
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Text,
|
||||
Tooltip,
|
||||
Divider,
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconEdit, IconTrash, IconPlus } from '@tabler/icons-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
useGetLocationTypesQuery,
|
||||
useCreateLocationTypeMutation,
|
||||
useUpdateLocationTypeMutation,
|
||||
useDeleteLocationTypeMutation,
|
||||
} from '../api/location-api';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
|
||||
interface LocationTypeFormValues {
|
||||
code: string;
|
||||
namesEn: string;
|
||||
namesAm: string;
|
||||
level: number;
|
||||
}
|
||||
|
||||
export function LocationTypeModal({ opened, onClose }: { opened: boolean; onClose: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const { data: locationTypes, isLoading } = useGetLocationTypesQuery();
|
||||
const [createType] = useCreateLocationTypeMutation();
|
||||
const [updateType] = useUpdateLocationTypeMutation();
|
||||
const [deleteType] = useDeleteLocationTypeMutation();
|
||||
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
|
||||
const form = useForm<LocationTypeFormValues>({
|
||||
initialValues: {
|
||||
code: '',
|
||||
namesEn: '',
|
||||
namesAm: '',
|
||||
level: 1,
|
||||
},
|
||||
validate: {
|
||||
code: (v) => (!v ? 'Code is required' : null),
|
||||
namesEn: (v) => (!v ? 'English name is required' : null),
|
||||
namesAm: (v) => (!v ? 'Amharic name is required' : null),
|
||||
level: (v) => (v < 1 ? 'Level must be at least 1' : null),
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!opened) {
|
||||
resetForm();
|
||||
}
|
||||
}, [opened]);
|
||||
|
||||
const resetForm = () => {
|
||||
form.reset();
|
||||
setEditingId(null);
|
||||
setShowForm(false);
|
||||
};
|
||||
|
||||
const handleEdit = (type: { id: string; code: string; names: { en: string; am: string }; level: number }) => {
|
||||
setEditingId(type.id);
|
||||
form.setValues({
|
||||
code: type.code,
|
||||
namesEn: type.names.en,
|
||||
namesAm: type.names.am,
|
||||
level: type.level,
|
||||
});
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await deleteType(id).unwrap();
|
||||
notify.success(t('location.typeDeleted'));
|
||||
} catch {
|
||||
notify.error(t('location.deleteError'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = form.onSubmit(async (values) => {
|
||||
try {
|
||||
if (editingId) {
|
||||
await updateType({ id: editingId, ...values, names: { en: values.namesEn, am: values.namesAm } }).unwrap();
|
||||
notify.success(t('location.typeUpdated'));
|
||||
} else {
|
||||
await createType({ code: values.code, names: { en: values.namesEn, am: values.namesAm }, level: values.level }).unwrap();
|
||||
notify.success(t('location.typeCreated'));
|
||||
}
|
||||
resetForm();
|
||||
} catch {
|
||||
notify.error(t('location.typeError'));
|
||||
}
|
||||
});
|
||||
|
||||
const sortedTypes = locationTypes?.items
|
||||
? [...locationTypes.items].sort((a, b) => a.level - b.level)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={t('location.manageTypes')}
|
||||
size="lg"
|
||||
>
|
||||
{!showForm && (
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={16} />}
|
||||
onClick={() => setShowForm(true)}
|
||||
mb="md"
|
||||
size="sm"
|
||||
>
|
||||
{t('location.addType')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showForm && (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack gap="sm" mb="md">
|
||||
<TextInput
|
||||
label="Code"
|
||||
placeholder="e.g., COUNTRY, REGION, CITY"
|
||||
{...form.getInputProps('code')}
|
||||
size="sm"
|
||||
/>
|
||||
<TextInput
|
||||
label="Name (English)"
|
||||
placeholder="English name"
|
||||
{...form.getInputProps('namesEn')}
|
||||
size="sm"
|
||||
/>
|
||||
<TextInput
|
||||
label="Name (Amharic)"
|
||||
placeholder="የአማርኛ ስም"
|
||||
{...form.getInputProps('namesAm')}
|
||||
size="sm"
|
||||
/>
|
||||
<NumberInput
|
||||
label="Level"
|
||||
placeholder="1"
|
||||
min={1}
|
||||
max={10}
|
||||
{...form.getInputProps('level')}
|
||||
size="sm"
|
||||
/>
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={resetForm} size="sm">
|
||||
{t('location.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" size="sm">
|
||||
{editingId ? t('location.update') : t('location.create')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<Divider mb="md" />
|
||||
|
||||
{isLoading && <Text c="dimmed">Loading...</Text>}
|
||||
{!isLoading && sortedTypes.length === 0 && (
|
||||
<Text c="dimmed" ta="center" py="xl">
|
||||
{t('location.noTypes')}
|
||||
</Text>
|
||||
)}
|
||||
{sortedTypes.length > 0 && (
|
||||
<Table striped highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Level</Table.Th>
|
||||
<Table.Th>Code</Table.Th>
|
||||
<Table.Th>Name (EN)</Table.Th>
|
||||
<Table.Th>Name (AM)</Table.Th>
|
||||
<Table.Th />
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{sortedTypes.map((type) => (
|
||||
<Table.Tr key={type.id}>
|
||||
<Table.Td>
|
||||
<Badge size="sm" variant="light" color="gray">
|
||||
{type.level}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="sm" variant="light" color="blue">
|
||||
{type.code}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>{type.names.en}</Table.Td>
|
||||
<Table.Td>{type.names.am}</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap="xs">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(type)}
|
||||
>
|
||||
<IconEdit size={14} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="red"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(type.id)}
|
||||
>
|
||||
<IconTrash size={14} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user