mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
fixes
This commit is contained in:
@@ -279,7 +279,7 @@ function ProfessionTab() {
|
||||
{
|
||||
header: t("configuration.name"),
|
||||
cell: ({ row }) => row.original.name[locale],
|
||||
enabled:true
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
header: t("configuration.description"),
|
||||
@@ -294,7 +294,7 @@ function ProfessionTab() {
|
||||
cell: ({ row }) => getDeptName(row.original.departmentId),
|
||||
},
|
||||
{
|
||||
header: "",
|
||||
header: "actions",
|
||||
size: 90,
|
||||
cell: ({ row }) => (
|
||||
<Group gap="xs">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Text,
|
||||
Title,
|
||||
rem,
|
||||
} from '@mantine/core';
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconArrowLeft,
|
||||
IconArrowRight,
|
||||
@@ -18,31 +18,42 @@ import {
|
||||
IconLogout2,
|
||||
IconMapPin,
|
||||
IconUser,
|
||||
} from '@tabler/icons-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useApiMutation } from '@ema-platform/api';
|
||||
import { notify, useErrorHandler } from '@ema-platform/ui';
|
||||
import { authStorage, setUser, logout, type AuthUser } from '@ema-platform/auth';
|
||||
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
||||
} from "@tabler/icons-react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useApiMutation } from "@ema-platform/api";
|
||||
import { notify, useErrorHandler } from "@ema-platform/ui";
|
||||
import {
|
||||
authStorage,
|
||||
setUser,
|
||||
logout,
|
||||
type AuthUser,
|
||||
} from "@ema-platform/auth";
|
||||
import { useAppDispatch, useAppSelector } from "../../../store/hooks";
|
||||
import {
|
||||
ProfileFormContent,
|
||||
profileSchema,
|
||||
type ProfileValues,
|
||||
} from '../../profile/components/ProfileFormContent';
|
||||
} from "../../profile/components/ProfileFormContent";
|
||||
import {
|
||||
AddressFormContent,
|
||||
addressSchema,
|
||||
type AddressValues,
|
||||
} from '../../profile/components/AddressFormContent';
|
||||
} from "../../profile/components/AddressFormContent";
|
||||
|
||||
const STEPS = [
|
||||
{ label: 'Profile', icon: IconUser },
|
||||
{ label: 'Address', icon: IconMapPin },
|
||||
{ label: "Profile", icon: IconUser },
|
||||
{ label: "Address", icon: IconMapPin },
|
||||
];
|
||||
|
||||
function StepIndicator({ active, completed }: { active: number; completed: number[] }) {
|
||||
function StepIndicator({
|
||||
active,
|
||||
completed,
|
||||
}: {
|
||||
active: number;
|
||||
completed: number[];
|
||||
}) {
|
||||
return (
|
||||
<Box mb={32}>
|
||||
<Group gap={0} align="center" wrap="nowrap">
|
||||
@@ -50,31 +61,41 @@ function StepIndicator({ active, completed }: { active: number; completed: numbe
|
||||
const isDone = completed.includes(i);
|
||||
const isCurrent = active === i;
|
||||
return (
|
||||
<Group key={i} gap={0} align="center" style={{ flex: i < STEPS.length - 1 ? 1 : 'none' }}>
|
||||
<Group
|
||||
key={i}
|
||||
gap={0}
|
||||
align="center"
|
||||
style={{ flex: i < STEPS.length - 1 ? 1 : "none" }}
|
||||
>
|
||||
<Stack gap={4} align="center" style={{ minWidth: rem(40) }}>
|
||||
<Box
|
||||
style={{
|
||||
width: rem(40),
|
||||
height: rem(40),
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: "50%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: isDone
|
||||
? 'var(--mantine-color-blue-8)'
|
||||
? "var(--mantine-color-blue-8)"
|
||||
: isCurrent
|
||||
? 'var(--mantine-color-blue-7)'
|
||||
: 'var(--mantine-color-gray-1)',
|
||||
border: isCurrent ? '2.5px solid var(--mantine-color-blue-5)' : '2px solid transparent',
|
||||
boxShadow: isCurrent || isDone ? '0 2px 8px rgba(34, 139, 230, 0.2)' : 'none',
|
||||
? "var(--mantine-color-blue-7)"
|
||||
: "var(--mantine-color-gray-1)",
|
||||
border: isCurrent
|
||||
? "2.5px solid var(--mantine-color-blue-5)"
|
||||
: "2px solid transparent",
|
||||
boxShadow:
|
||||
isCurrent || isDone
|
||||
? "0 2px 8px rgba(34, 139, 230, 0.2)"
|
||||
: "none",
|
||||
flexShrink: 0,
|
||||
transition: 'all 0.2s ease',
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{isDone ? (
|
||||
<IconCheck size={18} color="white" stroke={2.5} />
|
||||
) : (
|
||||
<Text fw={700} fz="sm" c={isCurrent ? 'white' : 'gray.5'}>
|
||||
<Text fw={700} fz="sm" c={isCurrent ? "white" : "gray.5"}>
|
||||
{i + 1}
|
||||
</Text>
|
||||
)}
|
||||
@@ -82,8 +103,8 @@ function StepIndicator({ active, completed }: { active: number; completed: numbe
|
||||
<Text
|
||||
fz="xs"
|
||||
fw={isCurrent ? 700 : 400}
|
||||
c={isCurrent ? 'blue.7' : 'dimmed'}
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
c={isCurrent ? "blue.7" : "dimmed"}
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
>
|
||||
{step.label}
|
||||
</Text>
|
||||
@@ -95,8 +116,8 @@ function StepIndicator({ active, completed }: { active: number; completed: numbe
|
||||
flex: 1,
|
||||
height: rem(2),
|
||||
backgroundColor: isDone
|
||||
? 'var(--mantine-color-blue-8)'
|
||||
: 'var(--mantine-color-gray-2)',
|
||||
? "var(--mantine-color-blue-8)"
|
||||
: "var(--mantine-color-gray-2)",
|
||||
marginBottom: rem(22),
|
||||
}}
|
||||
/>
|
||||
@@ -116,19 +137,24 @@ export function ProfileSetupPage() {
|
||||
const [active, setActive] = useState(0);
|
||||
const [completed, setCompleted] = useState<number[]>([]);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [professions, setProfessions] = useState<Array<{ id: string; name: { en: string } }>>([]);
|
||||
const [professions, setProfessions] = useState<
|
||||
Array<{ id: string; name: { en: string } }>
|
||||
>([]);
|
||||
const [professionsLoading, setProfessionsLoading] = useState(true);
|
||||
const [profileTrigger] = useApiMutation<{ id: string }>();
|
||||
const [addressTrigger] = useApiMutation<unknown>();
|
||||
const [addressTrigger] = useApiMutation<{ id: string }>();
|
||||
const [meTrigger] = useApiMutation<AuthUser>();
|
||||
const [fetchProfessions] = useApiMutation<{ count: number; items: Array<{ id: string; name: { en: string } }> }>();
|
||||
const [fetchProfessions] = useApiMutation<{
|
||||
count: number;
|
||||
items: Array<{ id: string; name: { en: string } }>;
|
||||
}>();
|
||||
const { handleError } = useErrorHandler();
|
||||
const fetched = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (fetched.current) return;
|
||||
fetched.current = true;
|
||||
fetchProfessions({ url: '/professions?take=100', method: 'GET' })
|
||||
fetchProfessions({ url: "/professions?take=100", method: "GET" })
|
||||
.unwrap()
|
||||
.then((data) => setProfessions(data.items ?? []))
|
||||
.catch(() => setProfessions([]))
|
||||
@@ -148,36 +174,45 @@ export function ProfileSetupPage() {
|
||||
return map;
|
||||
}, [professions]);
|
||||
|
||||
const nameParts = useMemo(() => (user?.name?.en || '').trim().split(/\s+/), [user]);
|
||||
const profileDefaults: ProfileValues = useMemo(() => ({
|
||||
professionId: '',
|
||||
firstName: nameParts[0] || '',
|
||||
middleName: nameParts.length > 2 ? nameParts.slice(1, -1).join(' ') : '',
|
||||
lastName: nameParts.length > 1 ? nameParts[nameParts.length - 1] : '',
|
||||
gender: '',
|
||||
dob: '',
|
||||
pob: '',
|
||||
maritalStatus: '',
|
||||
}), [nameParts]);
|
||||
const nameParts = useMemo(
|
||||
() => (user?.name?.en || "").trim().split(/\s+/),
|
||||
[user],
|
||||
);
|
||||
const profileDefaults: ProfileValues = useMemo(
|
||||
() => ({
|
||||
professionId: "",
|
||||
firstName: nameParts[0] || "",
|
||||
middleName: nameParts.length > 2 ? nameParts.slice(1, -1).join(" ") : "",
|
||||
lastName: nameParts.length > 1 ? nameParts[nameParts.length - 1] : "",
|
||||
gender: "",
|
||||
dob: "",
|
||||
pob: "",
|
||||
maritalStatus: "",
|
||||
}),
|
||||
[nameParts],
|
||||
);
|
||||
|
||||
const addressDefaults: AddressValues = useMemo(() => ({
|
||||
idType: '',
|
||||
idNumber: '',
|
||||
nationality: '',
|
||||
primaryPhoneNumber: user?.phoneNumber || '',
|
||||
secondaryPhoneNumber: '',
|
||||
email: user?.email || '',
|
||||
regionId: '',
|
||||
cityId: '',
|
||||
subcityId: '',
|
||||
woredaId: '',
|
||||
kebeleId: '',
|
||||
streetAddress: '',
|
||||
postalAddress: '',
|
||||
emergencyContactName: '',
|
||||
emergencyContactPhone: '',
|
||||
emergencyContactRelation: '',
|
||||
}), [user]);
|
||||
const addressDefaults: AddressValues = useMemo(
|
||||
() => ({
|
||||
idType: "",
|
||||
idNumber: "",
|
||||
nationality: "",
|
||||
primaryPhoneNumber: user?.phoneNumber || "",
|
||||
secondaryPhoneNumber: "",
|
||||
email: user?.email || "",
|
||||
regionId: "",
|
||||
cityId: "",
|
||||
subcityId: "",
|
||||
woredaId: "",
|
||||
kebeleId: "",
|
||||
streetAddress: "",
|
||||
postalAddress: "",
|
||||
emergencyContactName: "",
|
||||
emergencyContactPhone: "",
|
||||
emergencyContactRelation: "",
|
||||
}),
|
||||
[user],
|
||||
);
|
||||
|
||||
const {
|
||||
register: profileRegister,
|
||||
@@ -218,29 +253,11 @@ export function ProfileSetupPage() {
|
||||
try {
|
||||
const pv = profileWatch();
|
||||
const av = addressWatch();
|
||||
const selectedProfessionName = professionNameMap[pv.professionId] ?? '';
|
||||
const selectedProfessionName = professionNameMap[pv.professionId] ?? "";
|
||||
|
||||
const profileResult = await profileTrigger({
|
||||
url: '/profiles',
|
||||
method: 'POST',
|
||||
body: {
|
||||
userId: user?.id,
|
||||
type: 'SEAFARER',
|
||||
professionId: pv.professionId,
|
||||
firstName: pv.firstName,
|
||||
middleName: pv.middleName,
|
||||
lastName: pv.lastName,
|
||||
gender: pv.gender,
|
||||
dob: pv.dob,
|
||||
pob: pv.pob || undefined,
|
||||
maritalStatus: pv.maritalStatus,
|
||||
},
|
||||
}).unwrap();
|
||||
authStorage.setProfileId(profileResult.id);
|
||||
|
||||
await addressTrigger({
|
||||
url: `/addresss/profile/${profileResult.id}`,
|
||||
method: 'POST',
|
||||
const addressData = await addressTrigger({
|
||||
url: `/addresss`,
|
||||
method: "POST",
|
||||
body: {
|
||||
idType: av.idType,
|
||||
idNumber: av.idNumber,
|
||||
@@ -261,11 +278,30 @@ export function ProfileSetupPage() {
|
||||
},
|
||||
}).unwrap();
|
||||
|
||||
const me = await meTrigger({ url: '/auth/me', method: 'GET' }).unwrap();
|
||||
const profileResult = await profileTrigger({
|
||||
url: "/profiles",
|
||||
method: "POST",
|
||||
body: {
|
||||
userId: user?.id,
|
||||
type: "SEAFARER",
|
||||
professionId: pv.professionId,
|
||||
firstName: pv.firstName,
|
||||
middleName: pv.middleName,
|
||||
lastName: pv.lastName,
|
||||
gender: pv.gender,
|
||||
dob: pv.dob,
|
||||
pob: pv.pob || undefined,
|
||||
maritalStatus: pv.maritalStatus,
|
||||
addressId: addressData.id,
|
||||
},
|
||||
}).unwrap();
|
||||
authStorage.setProfileId(profileResult.id);
|
||||
|
||||
const me = await meTrigger({ url: "/auth/me", method: "GET" }).unwrap();
|
||||
dispatch(setUser(me));
|
||||
|
||||
notify.success('Profile setup complete!');
|
||||
navigate('/dashboard');
|
||||
notify.success("Profile setup complete!");
|
||||
navigate("/dashboard");
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
} finally {
|
||||
@@ -333,7 +369,7 @@ export function ProfileSetupPage() {
|
||||
leftSection={<IconLogout2 size={16} />}
|
||||
onClick={() => {
|
||||
dispatch(logout());
|
||||
navigate('/login');
|
||||
navigate("/login");
|
||||
}}
|
||||
>
|
||||
Sign out
|
||||
@@ -349,7 +385,10 @@ export function ProfileSetupPage() {
|
||||
</Button>
|
||||
)}
|
||||
{active < STEPS.length - 1 ? (
|
||||
<Button rightSection={<IconArrowRight size={16} />} onClick={onNext}>
|
||||
<Button
|
||||
rightSection={<IconArrowRight size={16} />}
|
||||
onClick={onNext}
|
||||
>
|
||||
Next Step
|
||||
</Button>
|
||||
) : (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { ReactNode, useState } from "react";
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
@@ -11,9 +11,9 @@ import {
|
||||
Center,
|
||||
Paper,
|
||||
Badge,
|
||||
} from '@mantine/core';
|
||||
import { IconRefresh, IconEye, IconInbox } from '@tabler/icons-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
} from "@mantine/core";
|
||||
import { IconRefresh, IconEye, IconInbox } from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface AdvancedColumn<T> {
|
||||
header: ReactNode;
|
||||
@@ -21,7 +21,7 @@ export interface AdvancedColumn<T> {
|
||||
accessorKey?: string;
|
||||
cell?: (ctx: { row: { original: T }; value: unknown }) => ReactNode;
|
||||
size?: number;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
align?: "left" | "center" | "right";
|
||||
/** Whether column starts visible. Default true. */
|
||||
enabled?: boolean;
|
||||
}
|
||||
@@ -45,10 +45,15 @@ interface AdvancedTableProps<T> {
|
||||
|
||||
function getByPath(obj: unknown, path?: string): unknown {
|
||||
if (!path) return undefined;
|
||||
return path.split('.').reduce<unknown>(
|
||||
(acc, key) => (acc && typeof acc === 'object' ? (acc as Record<string, unknown>)[key] : undefined),
|
||||
obj,
|
||||
);
|
||||
return path
|
||||
.split(".")
|
||||
.reduce<unknown>(
|
||||
(acc, key) =>
|
||||
acc && typeof acc === "object"
|
||||
? (acc as Record<string, unknown>)[key]
|
||||
: undefined,
|
||||
obj,
|
||||
);
|
||||
}
|
||||
|
||||
export function AdvancedTable<T extends { id?: string | number }>({
|
||||
@@ -64,7 +69,9 @@ export function AdvancedTable<T extends { id?: string | number }>({
|
||||
emptyText,
|
||||
}: AdvancedTableProps<T>) {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState<boolean[]>(columns.map((c) => c.enabled ?? true));
|
||||
const [visible, setVisible] = useState<boolean[]>(
|
||||
columns.map((c) => c.enabled ?? true),
|
||||
);
|
||||
const toggleColumn = (i: number) =>
|
||||
setVisible((prev) => {
|
||||
if (prev[i] && prev.filter(Boolean).length === 1) return prev; // keep at least one column visible
|
||||
@@ -73,57 +80,80 @@ export function AdvancedTable<T extends { id?: string | number }>({
|
||||
const shownColumns = columns.filter((_, i) => visible[i] ?? true);
|
||||
|
||||
return (
|
||||
<Paper withBorder radius="md" p="md">
|
||||
<Paper radius="md" p="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Group gap="xs">
|
||||
<Text fw={600}>{tableName}</Text>
|
||||
<Badge variant="light" size="sm" radius="sm">
|
||||
{itemCount}
|
||||
</Badge>
|
||||
<Text fw={600}>{""}</Text>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Menu closeOnItemClick={false} shadow="md" position="bottom-end" width={220}>
|
||||
<Menu
|
||||
closeOnItemClick={false}
|
||||
shadow="md"
|
||||
position="bottom-end"
|
||||
width={220}
|
||||
>
|
||||
{refresh && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
leftSection={<IconRefresh size={16} />}
|
||||
onClick={refresh}
|
||||
loading={isLoading}
|
||||
>
|
||||
{t("common.refresh", "Refresh")}
|
||||
</Button>
|
||||
)}
|
||||
<Menu.Target>
|
||||
<Button variant="default" size="sm" leftSection={<IconEye size={16} />}>
|
||||
{t('common.view', 'View')}
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
leftSection={<IconEye size={16} />}
|
||||
>
|
||||
{t("common.view", "View")}
|
||||
</Button>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Label>{t('common.toggleColumns', 'Toggle columns')}</Menu.Label>
|
||||
<Menu.Label>
|
||||
{t("common.toggleColumns", "Toggle columns")}
|
||||
</Menu.Label>
|
||||
{columns.map((col, i) => (
|
||||
<Menu.Item key={i} onClick={() => toggleColumn(i)}>
|
||||
<Checkbox
|
||||
label={col.header}
|
||||
checked={visible[i] ?? true}
|
||||
disabled={visible.filter(Boolean).length === 1 && (visible[i] ?? true)}
|
||||
disabled={
|
||||
visible.filter(Boolean).length === 1 &&
|
||||
(visible[i] ?? true)
|
||||
}
|
||||
readOnly
|
||||
tabIndex={-1}
|
||||
styles={{ input: { cursor: 'pointer' }, label: { cursor: 'pointer' } }}
|
||||
styles={{
|
||||
input: { cursor: "pointer" },
|
||||
label: { cursor: "pointer" },
|
||||
}}
|
||||
/>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
{refresh && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
leftSection={<IconRefresh size={16} />}
|
||||
onClick={refresh}
|
||||
loading={isLoading}
|
||||
>
|
||||
{t('common.refresh', 'Refresh')}
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Table.ScrollContainer minWidth={480}>
|
||||
<Table striped highlightOnHover withTableBorder withColumnBorders verticalSpacing="sm">
|
||||
<Table
|
||||
striped
|
||||
highlightOnHover
|
||||
withTableBorder
|
||||
withColumnBorders
|
||||
verticalSpacing="sm"
|
||||
>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
{shownColumns.map((col, i) => (
|
||||
<Table.Th key={i} style={{ width: col.size, textAlign: col.align }}>
|
||||
<Table.Th
|
||||
key={i}
|
||||
style={{ width: col.size, textAlign: col.align }}
|
||||
>
|
||||
{col.header}
|
||||
</Table.Th>
|
||||
))}
|
||||
@@ -144,7 +174,9 @@ export function AdvancedTable<T extends { id?: string | number }>({
|
||||
<Center py="xl">
|
||||
<Group gap="xs" c="dimmed">
|
||||
<IconInbox size={18} />
|
||||
<Text c="dimmed">{emptyText ?? t('common.noResult', 'No results')}</Text>
|
||||
<Text c="dimmed">
|
||||
{emptyText ?? t("common.noResult", "No results")}
|
||||
</Text>
|
||||
</Group>
|
||||
</Center>
|
||||
</Table.Td>
|
||||
@@ -156,7 +188,9 @@ export function AdvancedTable<T extends { id?: string | number }>({
|
||||
const value = getByPath(row, col.accessorKey);
|
||||
return (
|
||||
<Table.Td key={i} style={{ textAlign: col.align }}>
|
||||
{col.cell ? col.cell({ row: { original: row }, value }) : (value as ReactNode) ?? '-'}
|
||||
{col.cell
|
||||
? col.cell({ row: { original: row }, value })
|
||||
: ((value as ReactNode) ?? "-")}
|
||||
</Table.Td>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user