feat: integrate AmharicDatePicker component across multiple pages and update translations

This commit is contained in:
estifanos
2026-07-31 10:27:50 +00:00
parent b5858c37be
commit 48bc31d321
17 changed files with 205 additions and 76 deletions

View File

@@ -1,6 +1,13 @@
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ActionIcon, Button, Group, Popover, TextInput } from '@mantine/core'; import {
ActionIcon,
Button,
Group,
MantineSize,
Popover,
TextInput,
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks'; import { useDisclosure } from '@mantine/hooks';
import { DayPicker as EthiopicDayPicker } from '@daypicker/ethiopic'; import { DayPicker as EthiopicDayPicker } from '@daypicker/ethiopic';
import { DayPicker as GregorianDayPicker } from '@daypicker/react'; import { DayPicker as GregorianDayPicker } from '@daypicker/react';
@@ -52,23 +59,48 @@ const ETH_FORMATTERS = {
formatMonthDropdown: (month: Date) => ethMonthName(month), formatMonthDropdown: (month: Date) => ethMonthName(month),
}; };
export function toEthiopicDateLabel(date: Date): string { // Local-date parse/format for the "YYYY-MM-DD" wire format (matches native
// <input type="date"> semantics). Deliberately NOT `new Date(iso)` (parses as
// UTC midnight, off-by-one in negative-offset zones) and NOT
// `.toISOString()` (shifts by the local offset when formatting) — same class
// of bug the UTC-noon workaround above already had to fix once in this file.
function parseISO(value?: string | null): Date | null {
if (!value) return null;
const [y, m, d] = value.split('-').map(Number);
return y && m && d ? new Date(y, m - 1, d) : null;
}
function formatISO(date: Date): string {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
export function toEthiopicDateLabel(date: Date | string): string {
const d = typeof date === 'string' ? parseISO(date) : date;
if (!d) return '';
try { try {
const eth = toEthDateTime(date); const eth = toEthDateTime(d);
return `EC ${eth.year}-${String(eth.month).padStart(2, '0')}-${String(eth.date).padStart(2, '0')}`; return `EC ${eth.year}-${String(eth.month).padStart(2, '0')}-${String(eth.date).padStart(2, '0')}`;
} catch { } catch {
return date.toLocaleDateString('en-US'); return d.toLocaleDateString('en-US');
} }
} }
const YEAR_DROPDOWN_END = new Date(new Date().getFullYear() + 50, 11, 31); const YEAR_DROPDOWN_END = new Date(new Date().getFullYear() + 50, 11, 31);
export interface AmharicDatePickerProps { export interface AmharicDatePickerProps {
label?: string; label?: React.ReactNode;
value?: Date | null; value?: string | null;
onChange?: (date: Date | null) => void; onChange?: (value: string) => void;
required?: boolean; required?: boolean;
placeholder?: string; placeholder?: string;
error?: React.ReactNode;
disabled?: boolean;
size?: MantineSize;
name?: string;
onBlur?: () => void;
} }
export function AmharicDatePicker({ export function AmharicDatePicker({
@@ -77,21 +109,28 @@ export function AmharicDatePicker({
onChange, onChange,
required, required,
placeholder, placeholder,
error,
disabled,
size,
name,
onBlur,
}: AmharicDatePickerProps) { }: AmharicDatePickerProps) {
const { i18n } = useTranslation(); const { t, i18n } = useTranslation();
const [calendarType, setCalendarType] = useState<'EN' | 'AMH'>(() => const [calendarType, setCalendarType] = useState<'EN' | 'AMH'>(() =>
i18n.language?.startsWith('am') ? 'AMH' : 'EN', i18n.language?.startsWith('am') ? 'AMH' : 'EN',
); );
const [opened, { close, toggle }] = useDisclosure(false); const [opened, { close, toggle }] = useDisclosure(false);
const displayValue = value const selected = parseISO(value);
const displayValue = selected
? calendarType === 'EN' ? calendarType === 'EN'
? value.toLocaleDateString('en-US', { ? selected.toLocaleDateString('en-US', {
year: 'numeric', year: 'numeric',
month: 'long', month: 'long',
day: 'numeric', day: 'numeric',
}) })
: toAmharicDisplay(value) : toAmharicDisplay(selected)
: ''; : '';
return ( return (
@@ -109,24 +148,32 @@ export function AmharicDatePicker({
required={required} required={required}
value={displayValue} value={displayValue}
readOnly readOnly
disabled={disabled}
size={size}
name={name}
error={error}
placeholder={placeholder} placeholder={placeholder}
onClick={toggle} onClick={() => !disabled && toggle()}
onBlur={onBlur}
leftSection={ leftSection={
<Button <Button
type="button"
variant="light" variant="light"
size="compact-xs" size="compact-xs"
tabIndex={0}
disabled={disabled}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setCalendarType((prev) => (prev === 'EN' ? 'AMH' : 'EN')); setCalendarType((prev) => (prev === 'EN' ? 'AMH' : 'EN'));
}} }}
aria-label="Switch calendar type" aria-label={t('common.switchCalendar')}
> >
{calendarType} {calendarType}
</Button> </Button>
} }
leftSectionWidth="calc(4.375rem * var(--mantine-scale))" leftSectionWidth="calc(4.375rem * var(--mantine-scale))"
rightSection={ rightSection={
<ActionIcon size="md" variant="transparent" onClick={toggle}> <ActionIcon size="md" variant="transparent" disabled={disabled} onClick={() => toggle()}>
<IconCalendarEvent size={20} /> <IconCalendarEvent size={20} />
</ActionIcon> </ActionIcon>
} }
@@ -138,14 +185,14 @@ export function AmharicDatePicker({
<EthiopicDayPicker <EthiopicDayPicker
className="amharic-daypicker" className="amharic-daypicker"
mode="single" mode="single"
selected={value ?? undefined} selected={selected ?? undefined}
defaultMonth={value ?? undefined} defaultMonth={selected ?? undefined}
endMonth={YEAR_DROPDOWN_END} endMonth={YEAR_DROPDOWN_END}
numerals="latn" numerals="latn"
captionLayout="dropdown" captionLayout="dropdown"
formatters={ETH_FORMATTERS} formatters={ETH_FORMATTERS}
onSelect={(date: Date | undefined) => { onSelect={(date: Date | undefined) => {
onChange?.(date ?? null); onChange?.(date ? formatISO(date) : '');
close(); close();
}} }}
/> />
@@ -153,12 +200,12 @@ export function AmharicDatePicker({
<GregorianDayPicker <GregorianDayPicker
className="amharic-daypicker" className="amharic-daypicker"
mode="single" mode="single"
selected={value ?? undefined} selected={selected ?? undefined}
defaultMonth={value ?? undefined} defaultMonth={selected ?? undefined}
endMonth={YEAR_DROPDOWN_END} endMonth={YEAR_DROPDOWN_END}
captionLayout="dropdown" captionLayout="dropdown"
onSelect={(date: Date | undefined) => { onSelect={(date: Date | undefined) => {
onChange?.(date ?? null); onChange?.(date ? formatISO(date) : '');
close(); close();
}} }}
/> />
@@ -169,7 +216,7 @@ export function AmharicDatePicker({
variant="subtle" variant="subtle"
size="xs" size="xs"
onClick={() => { onClick={() => {
onChange?.(null); onChange?.('');
close(); close();
}} }}
> >
@@ -179,7 +226,7 @@ export function AmharicDatePicker({
variant="light" variant="light"
size="xs" size="xs"
onClick={() => { onClick={() => {
onChange?.(new Date()); onChange?.(formatISO(new Date()));
close(); close();
}} }}
> >

View File

@@ -1,4 +1,5 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { AmharicDatePicker } from '../../../components/AmharicDatePicker';
import { import {
Alert, Alert,
Badge, Badge,
@@ -202,9 +203,9 @@ function UploadModal({
<TextInput label="Issuing Institution" placeholder="e.g. Bahirdar Maritime School" required value={issuer} onChange={(e) => setIssuer(e.currentTarget.value)} size="sm" /> <TextInput label="Issuing Institution" placeholder="e.g. Bahirdar Maritime School" required value={issuer} onChange={(e) => setIssuer(e.currentTarget.value)} size="sm" />
<TextInput label="Certificate Number" placeholder="e.g. PST-2024-001" required value={certNumber} onChange={(e) => setCertNumber(e.currentTarget.value)} size="sm" /> <TextInput label="Certificate Number" placeholder="e.g. PST-2024-001" required value={certNumber} onChange={(e) => setCertNumber(e.currentTarget.value)} size="sm" />
<SimpleGrid cols={2} spacing="sm"> <SimpleGrid cols={2} spacing="sm">
<TextInput label="Issue Date" type="date" required value={issueDate} onChange={(e) => setIssueDate(e.currentTarget.value)} size="sm" /> <AmharicDatePicker label="Issue Date" required value={issueDate} onChange={setIssueDate} size="sm" />
{item.refreshYears > 0 && ( {item.refreshYears > 0 && (
<TextInput label={`Expiry Date (${item.refreshYears}-yr refresh)`} type="date" value={expiryDate} onChange={(e) => setExpiryDate(e.currentTarget.value)} size="sm" /> <AmharicDatePicker label={`Expiry Date (${item.refreshYears}-yr refresh)`} value={expiryDate} onChange={setExpiryDate} size="sm" />
)} )}
</SimpleGrid> </SimpleGrid>
<div> <div>

View File

@@ -1,5 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { AmharicDatePicker } from '../../../components/AmharicDatePicker';
import { import {
Alert, Alert,
Badge, Badge,
@@ -999,11 +1000,10 @@ export function CoCApplicationPage() {
size="sm" size="sm"
required required
/> />
<TextInput <AmharicDatePicker
label="Payment Date" label="Payment Date"
type="date"
value={paymentDate} value={paymentDate}
onChange={(e) => setPaymentDate(e.currentTarget.value)} onChange={setPaymentDate}
size="sm" size="sm"
required required
/> />

View File

@@ -1,6 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { CountrySelect, getCountryName } from '@ema-platform/ui'; import { CountrySelect, getCountryName } from '@ema-platform/ui';
import { AmharicDatePicker } from '../../../components/AmharicDatePicker';
import { import {
Alert, Alert,
Badge, Badge,
@@ -133,8 +134,8 @@ function ApplicationWizard({ onDone }: { onDone: () => void }) {
<CountrySelect label="Issuing Country" value={country} onChange={setCountry} required /> <CountrySelect label="Issuing Country" value={country} onChange={setCountry} required />
<TextInput label="Issuing Authority / Administration" placeholder="e.g. Maritime Industry Authority (MARINA)" value={issuer} onChange={(e) => setIssuer(e.currentTarget.value)} required /> <TextInput label="Issuing Authority / Administration" placeholder="e.g. Maritime Industry Authority (MARINA)" value={issuer} onChange={(e) => setIssuer(e.currentTarget.value)} required />
<TextInput label="Certificate Type" placeholder="e.g. Officer in Charge of a Navigational Watch" value={cocType} onChange={(e) => setCocType(e.currentTarget.value)} required /> <TextInput label="Certificate Type" placeholder="e.g. Officer in Charge of a Navigational Watch" value={cocType} onChange={(e) => setCocType(e.currentTarget.value)} required />
<TextInput label="Issue Date" type="date" value={issueDate} onChange={(e) => setIssueDate(e.currentTarget.value)} required /> <AmharicDatePicker label="Issue Date" value={issueDate} onChange={setIssueDate} required />
<TextInput label="Expiry Date" type="date" value={expiryDate} onChange={(e) => setExpiryDate(e.currentTarget.value)} required /> <AmharicDatePicker label="Expiry Date" value={expiryDate} onChange={setExpiryDate} required />
</SimpleGrid> </SimpleGrid>
</Paper> </Paper>
)} )}

View File

@@ -1,6 +1,7 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useApiMutation } from '@ema-platform/api'; import { useApiMutation } from '@ema-platform/api';
import { AmharicDatePicker } from '../../../components/AmharicDatePicker';
import { import {
Alert, Alert,
Badge, Badge,
@@ -447,7 +448,7 @@ export function JointInvestmentLicenseApplicationPage() {
<SectionHead title="Business License and Registration Information" /> <SectionHead title="Business License and Registration Information" />
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md"> <SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
<TextInput label="Budget Year" required value={budgetYear} onChange={(e) => setBudgetYear(e.currentTarget.value)} /> <TextInput label="Budget Year" required value={budgetYear} onChange={(e) => setBudgetYear(e.currentTarget.value)} />
<TextInput label="License Validity Date" required placeholder="YYYY-MM-DD" value={licenseValidityDate} onChange={(e) => setLicenseValidityDate(e.currentTarget.value)} /> <AmharicDatePicker label="License Validity Date" required value={licenseValidityDate} onChange={setLicenseValidityDate} />
</SimpleGrid> </SimpleGrid>
</Stack> </Stack>
)} )}

View File

@@ -1,4 +1,5 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { AmharicDatePicker } from '../../../components/AmharicDatePicker';
import { import {
Alert, Alert,
Badge, Badge,
@@ -224,18 +225,16 @@ export function MedicalCertificatePage() {
size="sm" size="sm"
/> />
<SimpleGrid cols={2} spacing="sm"> <SimpleGrid cols={2} spacing="sm">
<TextInput <AmharicDatePicker
label="Issue Date" label="Issue Date"
type="date"
value={issuedDate} value={issuedDate}
onChange={(e) => setIssuedDate(e.currentTarget.value)} onChange={setIssuedDate}
size="sm" size="sm"
/> />
<TextInput <AmharicDatePicker
label="Expiry Date" label="Expiry Date"
type="date"
value={expiryDate} value={expiryDate}
onChange={(e) => setExpiryDate(e.currentTarget.value)} onChange={setExpiryDate}
size="sm" size="sm"
/> />
</SimpleGrid> </SimpleGrid>

View File

@@ -1,6 +1,7 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useApiMutation } from '@ema-platform/api'; import { useApiMutation } from '@ema-platform/api';
import { AmharicDatePicker } from '../../../components/AmharicDatePicker';
import { import {
Alert, Alert,
Badge, Badge,
@@ -572,8 +573,8 @@ export function MtoLicenseApplicationPage() {
<DocCard slot={{ key: 'customsBondDoc', label: 'Customs Bond Document', description: 'Customs bond documentation', required: true, icon: IconShieldCheck }} file={customsBondDoc} onFile={setCustomsBondDoc} /> <DocCard slot={{ key: 'customsBondDoc', label: 'Customs Bond Document', description: 'Customs bond documentation', required: true, icon: IconShieldCheck }} file={customsBondDoc} onFile={setCustomsBondDoc} />
</SimpleGrid> </SimpleGrid>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md" mt="xs"> <SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md" mt="xs">
<TextInput label="Insurance Validity Date" required placeholder="YYYY-MM-DD" value={insuranceValidityDate} onChange={(e) => setInsuranceValidityDate(e.currentTarget.value)} /> <AmharicDatePicker label="Insurance Validity Date" required value={insuranceValidityDate} onChange={setInsuranceValidityDate} />
<TextInput label="Bond Validity Date" required placeholder="YYYY-MM-DD" value={bondValidityDate} onChange={(e) => setBondValidityDate(e.currentTarget.value)} /> <AmharicDatePicker label="Bond Validity Date" required value={bondValidityDate} onChange={setBondValidityDate} />
</SimpleGrid> </SimpleGrid>
</Stack> </Stack>
)} )}

View File

@@ -1,6 +1,7 @@
import { Loader, Select, SimpleGrid, TextInput } from '@mantine/core'; import { Loader, Select, SimpleGrid, TextInput } from '@mantine/core';
import type { FieldErrors, UseFormRegister, UseFormSetValue, UseFormWatch, UseFormTrigger } from 'react-hook-form'; import type { FieldErrors, UseFormRegister, UseFormSetValue, UseFormWatch, UseFormTrigger } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import { AmharicDatePicker } from '../../../components/AmharicDatePicker';
export const profileSchema = z.object({ export const profileSchema = z.object({
professionId: z.string().min(1, 'Select your profession'), professionId: z.string().min(1, 'Select your profession'),
@@ -86,11 +87,13 @@ export function ProfileFormContent({
onBlur={() => trigger('gender')} onBlur={() => trigger('gender')}
name="gender" name="gender"
/> />
<TextInput <AmharicDatePicker
label="Date of Birth" label="Date of Birth"
type="date"
required required
{...register('dob')} value={watch('dob')}
onChange={(val) => setValue('dob', val, { shouldValidate: true })}
onBlur={() => trigger('dob')}
name="dob"
error={errors.dob?.message} error={errors.dob?.message}
/> />
<TextInput <TextInput

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { AmharicDatePicker } from '../../../components/AmharicDatePicker';
import { import {
ActionIcon, ActionIcon,
Alert, Alert,
@@ -462,6 +463,9 @@ function HistoryTab({ entries }: { entries: HistoryEntry[] }) {
// Add Record Modal (generic) // Add Record Modal (generic)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function AddTrainingModal({ opened, onClose }: { opened: boolean; onClose: () => void }) { function AddTrainingModal({ opened, onClose }: { opened: boolean; onClose: () => void }) {
const [issueDate, setIssueDate] = useState('');
const [expiryDate, setExpiryDate] = useState('');
return ( return (
<Modal opened={opened} onClose={onClose} title="Add Training Record" size="lg"> <Modal opened={opened} onClose={onClose} title="Add Training Record" size="lg">
<Stack gap="sm"> <Stack gap="sm">
@@ -471,8 +475,8 @@ function AddTrainingModal({ opened, onClose }: { opened: boolean; onClose: () =>
<TextInput label="Certificate No." placeholder="CERT-0000" /> <TextInput label="Certificate No." placeholder="CERT-0000" />
</SimpleGrid> </SimpleGrid>
<SimpleGrid cols={2} spacing="sm"> <SimpleGrid cols={2} spacing="sm">
<TextInput label="Issue Date" type="date" /> <AmharicDatePicker label="Issue Date" value={issueDate} onChange={setIssueDate} />
<TextInput label="Expiry Date" type="date" /> <AmharicDatePicker label="Expiry Date" value={expiryDate} onChange={setExpiryDate} />
</SimpleGrid> </SimpleGrid>
<Select label="Status" data={['Approved', 'Pending', 'Expired']} defaultValue="Pending" /> <Select label="Status" data={['Approved', 'Pending', 'Expired']} defaultValue="Pending" />
<Group justify="flex-end" mt="sm"> <Group justify="flex-end" mt="sm">
@@ -485,14 +489,17 @@ function AddTrainingModal({ opened, onClose }: { opened: boolean; onClose: () =>
} }
function AddMedicalModal({ opened, onClose }: { opened: boolean; onClose: () => void }) { function AddMedicalModal({ opened, onClose }: { opened: boolean; onClose: () => void }) {
const [issueDate, setIssueDate] = useState('');
const [expiryDate, setExpiryDate] = useState('');
return ( return (
<Modal opened={opened} onClose={onClose} title="Add Medical Record" size="lg"> <Modal opened={opened} onClose={onClose} title="Add Medical Record" size="lg">
<Stack gap="sm"> <Stack gap="sm">
<TextInput label="Exam Type" placeholder="e.g. STCW Medical Certificate" required /> <TextInput label="Exam Type" placeholder="e.g. STCW Medical Certificate" required />
<TextInput label="Issued By" placeholder="Issuing authority" /> <TextInput label="Issued By" placeholder="Issuing authority" />
<SimpleGrid cols={2} spacing="sm"> <SimpleGrid cols={2} spacing="sm">
<TextInput label="Issue Date" type="date" /> <AmharicDatePicker label="Issue Date" value={issueDate} onChange={setIssueDate} />
<TextInput label="Expiry Date" type="date" /> <AmharicDatePicker label="Expiry Date" value={expiryDate} onChange={setExpiryDate} />
</SimpleGrid> </SimpleGrid>
<Select label="Result" data={['Fit', 'Unfit', 'Conditional']} defaultValue="Fit" /> <Select label="Result" data={['Fit', 'Unfit', 'Conditional']} defaultValue="Fit" />
<Textarea label="Remarks" placeholder="Any notes" autosize minRows={2} /> <Textarea label="Remarks" placeholder="Any notes" autosize minRows={2} />
@@ -507,6 +514,8 @@ function AddMedicalModal({ opened, onClose }: { opened: boolean; onClose: () =>
function AddSeaServiceModal({ opened, onClose }: { opened: boolean; onClose: () => void }) { function AddSeaServiceModal({ opened, onClose }: { opened: boolean; onClose: () => void }) {
const [flag, setFlag] = useState<string | null>(null); const [flag, setFlag] = useState<string | null>(null);
const [fromDate, setFromDate] = useState('');
const [toDate, setToDate] = useState('');
return ( return (
<Modal opened={opened} onClose={onClose} title="Add Sea Service Record" size="lg"> <Modal opened={opened} onClose={onClose} title="Add Sea Service Record" size="lg">
@@ -520,8 +529,8 @@ function AddSeaServiceModal({ opened, onClose }: { opened: boolean; onClose: ()
<CountrySelect label="Flag" value={flag} onChange={setFlag} /> <CountrySelect label="Flag" value={flag} onChange={setFlag} />
</SimpleGrid> </SimpleGrid>
<SimpleGrid cols={2} spacing="sm"> <SimpleGrid cols={2} spacing="sm">
<TextInput label="From" type="date" /> <AmharicDatePicker label="From" value={fromDate} onChange={setFromDate} />
<TextInput label="To" type="date" /> <AmharicDatePicker label="To" value={toDate} onChange={setToDate} />
</SimpleGrid> </SimpleGrid>
<TextInput label="Engagement Port" placeholder="Port name" /> <TextInput label="Engagement Port" placeholder="Port name" />
<Group justify="flex-end" mt="sm"> <Group justify="flex-end" mt="sm">
@@ -534,6 +543,9 @@ function AddSeaServiceModal({ opened, onClose }: { opened: boolean; onClose: ()
} }
function AddCertModal({ opened, onClose }: { opened: boolean; onClose: () => void }) { function AddCertModal({ opened, onClose }: { opened: boolean; onClose: () => void }) {
const [issueDate, setIssueDate] = useState('');
const [expiryDate, setExpiryDate] = useState('');
return ( return (
<Modal opened={opened} onClose={onClose} title="Add Certification" size="lg"> <Modal opened={opened} onClose={onClose} title="Add Certification" size="lg">
<Stack gap="sm"> <Stack gap="sm">
@@ -544,8 +556,8 @@ function AddCertModal({ opened, onClose }: { opened: boolean; onClose: () => voi
</SimpleGrid> </SimpleGrid>
<TextInput label="Issued By" placeholder="Issuing authority" /> <TextInput label="Issued By" placeholder="Issuing authority" />
<SimpleGrid cols={2} spacing="sm"> <SimpleGrid cols={2} spacing="sm">
<TextInput label="Issue Date" type="date" /> <AmharicDatePicker label="Issue Date" value={issueDate} onChange={setIssueDate} />
<TextInput label="Expiry Date" type="date" /> <AmharicDatePicker label="Expiry Date" value={expiryDate} onChange={setExpiryDate} />
</SimpleGrid> </SimpleGrid>
<Select label="Status" data={['Valid', 'Expired', 'Pending']} defaultValue="Valid" /> <Select label="Status" data={['Valid', 'Expired', 'Pending']} defaultValue="Valid" />
<Group justify="flex-end" mt="sm"> <Group justify="flex-end" mt="sm">

View File

@@ -264,7 +264,7 @@ export function SeafarerRegistrationPage() {
const [middleName, setMiddleName] = useState<BilingualValue>({ en: '', am: '' }); const [middleName, setMiddleName] = useState<BilingualValue>({ en: '', am: '' });
const [lastName, setLastName] = useState<BilingualValue>({ en: '', am: '' }); const [lastName, setLastName] = useState<BilingualValue>({ en: '', am: '' });
const [gender, setGender] = useState<string | null>(null); const [gender, setGender] = useState<string | null>(null);
const [dob, setDob] = useState<Date | null>(null); const [dob, setDob] = useState('');
const [placeOfBirth, setPlaceOfBirth] = useState(''); const [placeOfBirth, setPlaceOfBirth] = useState('');
const [nationality, setNationality] = useState<string | null>('Ethiopian'); const [nationality, setNationality] = useState<string | null>('Ethiopian');
const [maritalStatus, setMaritalStatus] = useState<string | null>(null); const [maritalStatus, setMaritalStatus] = useState<string | null>(null);
@@ -307,7 +307,7 @@ export function SeafarerRegistrationPage() {
setSubmitting(true); setSubmitting(true);
try { try {
const result = await submitSeafarerRegistration({ const result = await submitSeafarerRegistration({
personalInfo: { firstName, middleName, lastName, gender, dob: dob?.toISOString().split('T')[0] ?? '', placeOfBirth, nationality, maritalStatus, nationalIdNumber, passportNumber, passportExpiry }, personalInfo: { firstName, middleName, lastName, gender, dob, placeOfBirth, nationality, maritalStatus, nationalIdNumber, passportNumber, passportExpiry },
contactDetails: { mobile, email, locationId, permanentAddress, currentAddress, emergency: { name: emergencyName, relationship: emergencyRel, phone: emergencyPhone } }, contactDetails: { mobile, email, locationId, permanentAddress, currentAddress, emergency: { name: emergencyName, relationship: emergencyRel, phone: emergencyPhone } },
documents: Object.fromEntries(Object.entries(files).map(([k, v]) => [k, v?.name ?? null])), documents: Object.fromEntries(Object.entries(files).map(([k, v]) => [k, v?.name ?? null])),
}); });
@@ -365,7 +365,7 @@ export function SeafarerRegistrationPage() {
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md"> <SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
<TextInput label="National ID Number" placeholder="ET-000000" required value={nationalIdNumber} onChange={(e) => setNationalIdNumber(e.currentTarget.value)} /> <TextInput label="National ID Number" placeholder="ET-000000" required value={nationalIdNumber} onChange={(e) => setNationalIdNumber(e.currentTarget.value)} />
<TextInput label="Passport Number" placeholder="EP000000" value={passportNumber} onChange={(e) => setPassportNumber(e.currentTarget.value)} /> <TextInput label="Passport Number" placeholder="EP000000" value={passportNumber} onChange={(e) => setPassportNumber(e.currentTarget.value)} />
<TextInput label="Passport Expiry Date" type="date" value={passportExpiry} onChange={(e) => setPassportExpiry(e.currentTarget.value)} /> <AmharicDatePicker label="Passport Expiry Date" value={passportExpiry} onChange={setPassportExpiry} />
</SimpleGrid> </SimpleGrid>
<Alert variant="light" color="blue" icon={<IconInfoCircle size={17} />}> <Alert variant="light" color="blue" icon={<IconInfoCircle size={17} />}>
@@ -458,7 +458,7 @@ export function SeafarerRegistrationPage() {
<ReviewRow label="Middle Name" value={`${middleName.en}${middleName.am ? ` / ${middleName.am}` : ''}`} /> <ReviewRow label="Middle Name" value={`${middleName.en}${middleName.am ? ` / ${middleName.am}` : ''}`} />
<ReviewRow label="Last Name" value={`${lastName.en}${lastName.am ? ` / ${lastName.am}` : ''}`} /> <ReviewRow label="Last Name" value={`${lastName.en}${lastName.am ? ` / ${lastName.am}` : ''}`} />
<ReviewRow label="Gender" value={gender ?? ''} /> <ReviewRow label="Gender" value={gender ?? ''} />
<ReviewRow label="Date of Birth" value={`${dob?.toLocaleDateString('en-US') ?? ''}${dob ? ` (${toEthiopicDateLabel(dob)})` : ''}`} /> <ReviewRow label="Date of Birth" value={`${dob}${dob ? ` (${toEthiopicDateLabel(dob)})` : ''}`} />
<ReviewRow label="Place of Birth" value={placeOfBirth} /> <ReviewRow label="Place of Birth" value={placeOfBirth} />
<ReviewRow label="Nationality" value={nationality ?? ''} /> <ReviewRow label="Nationality" value={nationality ?? ''} />
<ReviewRow label="Marital Status" value={maritalStatus ?? ''} /> <ReviewRow label="Marital Status" value={maritalStatus ?? ''} />

View File

@@ -1,4 +1,5 @@
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { AmharicDatePicker } from "../../../components/AmharicDatePicker";
import { import {
Alert, Alert,
Badge, Badge,
@@ -315,20 +316,18 @@ function BSTCard({
onChange={(e) => onIssuer(e.currentTarget.value)} onChange={(e) => onIssuer(e.currentTarget.value)}
/> />
<SimpleGrid cols={slot.refreshYears > 0 ? 2 : 1} spacing="xs"> <SimpleGrid cols={slot.refreshYears > 0 ? 2 : 1} spacing="xs">
<TextInput <AmharicDatePicker
label="Issue Date" label="Issue Date"
type="date"
size="xs" size="xs"
value={issueDate} value={issueDate}
onChange={(e) => onIssueDate(e.currentTarget.value)} onChange={onIssueDate}
/> />
{slot.refreshYears > 0 && ( {slot.refreshYears > 0 && (
<TextInput <AmharicDatePicker
label={`Expiry (${slot.refreshYears}yr)`} label={`Expiry (${slot.refreshYears}yr)`}
type="date"
size="xs" size="xs"
value={expiryDate} value={expiryDate}
onChange={(e) => onExpiryDate(e.currentTarget.value)} onChange={onExpiryDate}
/> />
)} )}
</SimpleGrid> </SimpleGrid>
@@ -665,19 +664,17 @@ export function SeamanBookApplicationPage() {
/> />
</SimpleGrid> </SimpleGrid>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md"> <SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
<TextInput <AmharicDatePicker
label="Issue Date" label="Issue Date"
type="date"
required required
value={medIssueDate} value={medIssueDate}
onChange={(e) => setMedIssueDate(e.currentTarget.value)} onChange={setMedIssueDate}
/> />
<TextInput <AmharicDatePicker
label="Expiry Date" label="Expiry Date"
type="date"
required required
value={medExpiryDate} value={medExpiryDate}
onChange={(e) => setMedExpiryDate(e.currentTarget.value)} onChange={setMedExpiryDate}
/> />
</SimpleGrid> </SimpleGrid>
<SectionHead title="Upload Certificate" /> <SectionHead title="Upload Certificate" />
@@ -1006,12 +1003,11 @@ export function SeamanBookApplicationPage() {
value={paymentRef} value={paymentRef}
onChange={(e) => setPaymentRef(e.currentTarget.value)} onChange={(e) => setPaymentRef(e.currentTarget.value)}
/> />
<TextInput <AmharicDatePicker
label="Payment Date" label="Payment Date"
type="date"
required required
value={paymentDate} value={paymentDate}
onChange={(e) => setPaymentDate(e.currentTarget.value)} onChange={setPaymentDate}
/> />
</SimpleGrid> </SimpleGrid>
</> </>

View File

@@ -1,6 +1,7 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useApiMutation } from '@ema-platform/api'; import { useApiMutation } from '@ema-platform/api';
import { AmharicDatePicker } from '../../../components/AmharicDatePicker';
import { import {
Alert, Alert,
Badge, Badge,
@@ -324,8 +325,8 @@ export function ShippingAgentLicenseApplicationPage() {
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md"> <SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
<TextInput label="Shipping Company Name" required value={shippingCompanyName} onChange={(e) => setShippingCompanyName(e.currentTarget.value)} /> <TextInput label="Shipping Company Name" required value={shippingCompanyName} onChange={(e) => setShippingCompanyName(e.currentTarget.value)} />
<TextInput label="Agreement Reference Number" required value={agreementRefNumber} onChange={(e) => setAgreementRefNumber(e.currentTarget.value)} /> <TextInput label="Agreement Reference Number" required value={agreementRefNumber} onChange={(e) => setAgreementRefNumber(e.currentTarget.value)} />
<TextInput label="Agreement Start Date" type="date" required value={agreementStartDate} onChange={(e) => setAgreementStartDate(e.currentTarget.value)} /> <AmharicDatePicker label="Agreement Start Date" required value={agreementStartDate} onChange={setAgreementStartDate} />
<TextInput label="Agreement End Date" type="date" required value={agreementEndDate} onChange={(e) => setAgreementEndDate(e.currentTarget.value)} /> <AmharicDatePicker label="Agreement End Date" required value={agreementEndDate} onChange={setAgreementEndDate} />
</SimpleGrid> </SimpleGrid>
</Stack> </Stack>
)} )}

View File

@@ -1,6 +1,7 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useApiMutation } from '@ema-platform/api'; import { useApiMutation } from '@ema-platform/api';
import { AmharicDatePicker } from '../../../components/AmharicDatePicker';
import { import {
Alert, Alert,
Badge, Badge,
@@ -372,7 +373,7 @@ export function WaiverApplicationPage() {
<SectionHead title="Invoice Information" /> <SectionHead title="Invoice Information" />
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md"> <SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
<TextInput label="Invoice Number" required value={invoiceNumber} onChange={(e) => setInvoiceNumber(e.currentTarget.value)} /> <TextInput label="Invoice Number" required value={invoiceNumber} onChange={(e) => setInvoiceNumber(e.currentTarget.value)} />
<TextInput type="date" label="Invoice Date" required value={invoiceDate} onChange={(e) => setInvoiceDate(e.currentTarget.value)} /> <AmharicDatePicker label="Invoice Date" required value={invoiceDate} onChange={setInvoiceDate} />
<NumberInput label="Invoice Amount" required min={0} value={invoiceAmount} onChange={setInvoiceAmount} /> <NumberInput label="Invoice Amount" required min={0} value={invoiceAmount} onChange={setInvoiceAmount} />
</SimpleGrid> </SimpleGrid>
</Stack> </Stack>
@@ -393,10 +394,10 @@ export function WaiverApplicationPage() {
<TextInput label="Shipping Line / Carrier Name" required value={carrierName} onChange={(e) => setCarrierName(e.currentTarget.value)} /> <TextInput label="Shipping Line / Carrier Name" required value={carrierName} onChange={(e) => setCarrierName(e.currentTarget.value)} />
<TextInput label="Bill of Lading Number" value={billOfLadingNumber} onChange={(e) => setBillOfLadingNumber(e.currentTarget.value)} /> <TextInput label="Bill of Lading Number" value={billOfLadingNumber} onChange={(e) => setBillOfLadingNumber(e.currentTarget.value)} />
{isPreWaiver && ( {isPreWaiver && (
<TextInput type="date" label="Estimated Arrival Date" required value={estimatedArrivalDate} onChange={(e) => setEstimatedArrivalDate(e.currentTarget.value)} /> <AmharicDatePicker label="Estimated Arrival Date" required value={estimatedArrivalDate} onChange={setEstimatedArrivalDate} />
)} )}
{isPostWaiver && ( {isPostWaiver && (
<TextInput type="date" label="Actual Arrival Date" required value={actualArrivalDate} onChange={(e) => setActualArrivalDate(e.currentTarget.value)} /> <AmharicDatePicker label="Actual Arrival Date" required value={actualArrivalDate} onChange={setActualArrivalDate} />
)} )}
</SimpleGrid> </SimpleGrid>
</Stack> </Stack>

View File

@@ -81,6 +81,7 @@ export const am: Translations = {
learnMore: 'ተጨማሪ ይወቁ', learnMore: 'ተጨማሪ ይወቁ',
toggleTheme: 'ብርሃን / ጨለማ ገጽታ ቀይር', toggleTheme: 'ብርሃን / ጨለማ ገጽታ ቀይር',
welcome: 'እንኳን ደህና መጡ', welcome: 'እንኳን ደህና መጡ',
switchCalendar: 'የቀን መቁጠሪያ ዓይነት ቀይር',
}, },
auth: { auth: {

View File

@@ -79,6 +79,7 @@ export const en = {
learnMore: 'Learn more', learnMore: 'Learn more',
toggleTheme: 'Toggle light / dark mode', toggleTheme: 'Toggle light / dark mode',
welcome: 'Welcome', welcome: 'Welcome',
switchCalendar: 'Switch calendar type',
}, },
auth: { auth: {

69
package-lock.json generated
View File

@@ -25,6 +25,7 @@
"country-flag-icons": "^1.6.20", "country-flag-icons": "^1.6.20",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dayjs": "^1.11.20", "dayjs": "^1.11.20",
"ethiopian-calendar-date-converter": "^2.1.6",
"i18n-iso-countries": "^7.14.0", "i18n-iso-countries": "^7.14.0",
"i18next": "^25.6.0", "i18next": "^25.6.0",
"js-cookie": "^3.0.8", "js-cookie": "^3.0.8",
@@ -2043,7 +2044,7 @@
"version": "1.10.0", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"devOptional": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@emnapi/wasi-threads": "1.2.1", "@emnapi/wasi-threads": "1.2.1",
@@ -2054,7 +2055,7 @@
"version": "1.10.0", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"devOptional": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.4.0" "tslib": "^2.4.0"
@@ -2064,7 +2065,7 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
"devOptional": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.4.0" "tslib": "^2.4.0"
@@ -4133,6 +4134,7 @@
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz",
"integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -8403,6 +8405,66 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.10.0",
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.10.0",
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.4",
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@tybys/wasm-util": "^0.10.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
},
"peerDependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.10.2",
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
"version": "2.8.1",
"inBundle": true,
"license": "0BSD",
"optional": true
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.3.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz",
@@ -8950,6 +9012,7 @@
"version": "0.10.2", "version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {

View File

@@ -30,6 +30,7 @@
"country-flag-icons": "^1.6.20", "country-flag-icons": "^1.6.20",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dayjs": "^1.11.20", "dayjs": "^1.11.20",
"ethiopian-calendar-date-converter": "^2.1.6",
"i18n-iso-countries": "^7.14.0", "i18n-iso-countries": "^7.14.0",
"i18next": "^25.6.0", "i18next": "^25.6.0",
"js-cookie": "^3.0.8", "js-cookie": "^3.0.8",