diff --git a/apps/backoffice/src/app/features/configuration/pages/ConfigurationPage.tsx b/apps/backoffice/src/app/features/configuration/pages/ConfigurationPage.tsx index 48c4a3d38..6f01c73a5 100644 --- a/apps/backoffice/src/app/features/configuration/pages/ConfigurationPage.tsx +++ b/apps/backoffice/src/app/features/configuration/pages/ConfigurationPage.tsx @@ -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 }) => ( diff --git a/apps/portal/src/app/features/profile-setup/pages/ProfileSetupPage.tsx b/apps/portal/src/app/features/profile-setup/pages/ProfileSetupPage.tsx index 24e94ce70..ab6c32fde 100644 --- a/apps/portal/src/app/features/profile-setup/pages/ProfileSetupPage.tsx +++ b/apps/portal/src/app/features/profile-setup/pages/ProfileSetupPage.tsx @@ -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 ( @@ -50,31 +61,41 @@ function StepIndicator({ active, completed }: { active: number; completed: numbe const isDone = completed.includes(i); const isCurrent = active === i; return ( - + {isDone ? ( ) : ( - + {i + 1} )} @@ -82,8 +103,8 @@ function StepIndicator({ active, completed }: { active: number; completed: numbe {step.label} @@ -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([]); const [submitting, setSubmitting] = useState(false); - const [professions, setProfessions] = useState>([]); + const [professions, setProfessions] = useState< + Array<{ id: string; name: { en: string } }> + >([]); const [professionsLoading, setProfessionsLoading] = useState(true); const [profileTrigger] = useApiMutation<{ id: string }>(); - const [addressTrigger] = useApiMutation(); + const [addressTrigger] = useApiMutation<{ id: string }>(); const [meTrigger] = useApiMutation(); - 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={} onClick={() => { dispatch(logout()); - navigate('/login'); + navigate("/login"); }} > Sign out @@ -349,7 +385,10 @@ export function ProfileSetupPage() { )} {active < STEPS.length - 1 ? ( - ) : ( diff --git a/libs/ui/src/lib/data/AdvancedTable.tsx b/libs/ui/src/lib/data/AdvancedTable.tsx index 4b9a855c8..8f55811fd 100644 --- a/libs/ui/src/lib/data/AdvancedTable.tsx +++ b/libs/ui/src/lib/data/AdvancedTable.tsx @@ -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 { header: ReactNode; @@ -21,7 +21,7 @@ export interface AdvancedColumn { 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 { function getByPath(obj: unknown, path?: string): unknown { if (!path) return undefined; - return path.split('.').reduce( - (acc, key) => (acc && typeof acc === 'object' ? (acc as Record)[key] : undefined), - obj, - ); + return path + .split(".") + .reduce( + (acc, key) => + acc && typeof acc === "object" + ? (acc as Record)[key] + : undefined, + obj, + ); } export function AdvancedTable({ @@ -64,7 +69,9 @@ export function AdvancedTable({ emptyText, }: AdvancedTableProps) { const { t } = useTranslation(); - const [visible, setVisible] = useState(columns.map((c) => c.enabled ?? true)); + const [visible, setVisible] = useState( + 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({ const shownColumns = columns.filter((_, i) => visible[i] ?? true); return ( - + - {tableName} - - {itemCount} - + {""} - + + {refresh && ( + + )} - - {t('common.toggleColumns', 'Toggle columns')} + + {t("common.toggleColumns", "Toggle columns")} + {columns.map((col, i) => ( toggleColumn(i)}> ))} - {refresh && ( - - )} - +
{shownColumns.map((col, i) => ( - + {col.header} ))} @@ -144,7 +174,9 @@ export function AdvancedTable({
- {emptyText ?? t('common.noResult', 'No results')} + + {emptyText ?? t("common.noResult", "No results")} +
@@ -156,7 +188,9 @@ export function AdvancedTable({ const value = getByPath(row, col.accessorKey); return ( - {col.cell ? col.cell({ row: { original: row }, value }) : (value as ReactNode) ?? '-'} + {col.cell + ? col.cell({ row: { original: row }, value }) + : ((value as ReactNode) ?? "-")} ); })}