diff --git a/apps/portal/src/app/components/AmharicDatePicker.tsx b/apps/portal/src/app/components/AmharicDatePicker.tsx index 2bad4c4c8..5ef8c4f37 100644 --- a/apps/portal/src/app/components/AmharicDatePicker.tsx +++ b/apps/portal/src/app/components/AmharicDatePicker.tsx @@ -1,6 +1,13 @@ import { useState } from 'react'; 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 { DayPicker as EthiopicDayPicker } from '@daypicker/ethiopic'; import { DayPicker as GregorianDayPicker } from '@daypicker/react'; @@ -52,23 +59,48 @@ const ETH_FORMATTERS = { formatMonthDropdown: (month: Date) => ethMonthName(month), }; -export function toEthiopicDateLabel(date: Date): string { +// Local-date parse/format for the "YYYY-MM-DD" wire format (matches native +// 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 { - const eth = toEthDateTime(date); + const eth = toEthDateTime(d); return `EC ${eth.year}-${String(eth.month).padStart(2, '0')}-${String(eth.date).padStart(2, '0')}`; } catch { - return date.toLocaleDateString('en-US'); + return d.toLocaleDateString('en-US'); } } const YEAR_DROPDOWN_END = new Date(new Date().getFullYear() + 50, 11, 31); export interface AmharicDatePickerProps { - label?: string; - value?: Date | null; - onChange?: (date: Date | null) => void; + label?: React.ReactNode; + value?: string | null; + onChange?: (value: string) => void; required?: boolean; placeholder?: string; + error?: React.ReactNode; + disabled?: boolean; + size?: MantineSize; + name?: string; + onBlur?: () => void; } export function AmharicDatePicker({ @@ -77,21 +109,28 @@ export function AmharicDatePicker({ onChange, required, placeholder, + error, + disabled, + size, + name, + onBlur, }: AmharicDatePickerProps) { - const { i18n } = useTranslation(); + const { t, i18n } = useTranslation(); const [calendarType, setCalendarType] = useState<'EN' | 'AMH'>(() => i18n.language?.startsWith('am') ? 'AMH' : 'EN', ); const [opened, { close, toggle }] = useDisclosure(false); - const displayValue = value + const selected = parseISO(value); + + const displayValue = selected ? calendarType === 'EN' - ? value.toLocaleDateString('en-US', { + ? selected.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', }) - : toAmharicDisplay(value) + : toAmharicDisplay(selected) : ''; return ( @@ -109,24 +148,32 @@ export function AmharicDatePicker({ required={required} value={displayValue} readOnly + disabled={disabled} + size={size} + name={name} + error={error} placeholder={placeholder} - onClick={toggle} + onClick={() => !disabled && toggle()} + onBlur={onBlur} leftSection={ } leftSectionWidth="calc(4.375rem * var(--mantine-scale))" rightSection={ - + toggle()}> } @@ -138,14 +185,14 @@ export function AmharicDatePicker({ { - onChange?.(date ?? null); + onChange?.(date ? formatISO(date) : ''); close(); }} /> @@ -153,12 +200,12 @@ export function AmharicDatePicker({ { - onChange?.(date ?? null); + onChange?.(date ? formatISO(date) : ''); close(); }} /> @@ -169,7 +216,7 @@ export function AmharicDatePicker({ variant="subtle" size="xs" onClick={() => { - onChange?.(null); + onChange?.(''); close(); }} > @@ -179,7 +226,7 @@ export function AmharicDatePicker({ variant="light" size="xs" onClick={() => { - onChange?.(new Date()); + onChange?.(formatISO(new Date())); close(); }} > diff --git a/apps/portal/src/app/features/basic-safety-training/pages/BasicSafetyTrainingPage.tsx b/apps/portal/src/app/features/basic-safety-training/pages/BasicSafetyTrainingPage.tsx index 1b17e5913..cb124d58f 100644 --- a/apps/portal/src/app/features/basic-safety-training/pages/BasicSafetyTrainingPage.tsx +++ b/apps/portal/src/app/features/basic-safety-training/pages/BasicSafetyTrainingPage.tsx @@ -1,4 +1,5 @@ import { useRef, useState } from 'react'; +import { AmharicDatePicker } from '../../../components/AmharicDatePicker'; import { Alert, Badge, @@ -202,9 +203,9 @@ function UploadModal({ setIssuer(e.currentTarget.value)} size="sm" /> setCertNumber(e.currentTarget.value)} size="sm" /> - setIssueDate(e.currentTarget.value)} size="sm" /> + {item.refreshYears > 0 && ( - setExpiryDate(e.currentTarget.value)} size="sm" /> + )}
diff --git a/apps/portal/src/app/features/certificates/pages/CoCApplicationPage.tsx b/apps/portal/src/app/features/certificates/pages/CoCApplicationPage.tsx index 7eacb0124..087aae11d 100644 --- a/apps/portal/src/app/features/certificates/pages/CoCApplicationPage.tsx +++ b/apps/portal/src/app/features/certificates/pages/CoCApplicationPage.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { AmharicDatePicker } from '../../../components/AmharicDatePicker'; import { Alert, Badge, @@ -999,11 +1000,10 @@ export function CoCApplicationPage() { size="sm" required /> - setPaymentDate(e.currentTarget.value)} + onChange={setPaymentDate} size="sm" required /> diff --git a/apps/portal/src/app/features/endorsement/pages/EndorsementPage.tsx b/apps/portal/src/app/features/endorsement/pages/EndorsementPage.tsx index 635c05c73..8b601eb84 100644 --- a/apps/portal/src/app/features/endorsement/pages/EndorsementPage.tsx +++ b/apps/portal/src/app/features/endorsement/pages/EndorsementPage.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { CountrySelect, getCountryName } from '@ema-platform/ui'; +import { AmharicDatePicker } from '../../../components/AmharicDatePicker'; import { Alert, Badge, @@ -133,8 +134,8 @@ function ApplicationWizard({ onDone }: { onDone: () => void }) { setIssuer(e.currentTarget.value)} required /> setCocType(e.currentTarget.value)} required /> - setIssueDate(e.currentTarget.value)} required /> - setExpiryDate(e.currentTarget.value)} required /> + + )} diff --git a/apps/portal/src/app/features/joint-investment-license/pages/JointInvestmentLicenseApplicationPage.tsx b/apps/portal/src/app/features/joint-investment-license/pages/JointInvestmentLicenseApplicationPage.tsx index d1ada89b1..100b08ed2 100644 --- a/apps/portal/src/app/features/joint-investment-license/pages/JointInvestmentLicenseApplicationPage.tsx +++ b/apps/portal/src/app/features/joint-investment-license/pages/JointInvestmentLicenseApplicationPage.tsx @@ -1,6 +1,7 @@ import { useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useApiMutation } from '@ema-platform/api'; +import { AmharicDatePicker } from '../../../components/AmharicDatePicker'; import { Alert, Badge, @@ -447,7 +448,7 @@ export function JointInvestmentLicenseApplicationPage() { setBudgetYear(e.currentTarget.value)} /> - setLicenseValidityDate(e.currentTarget.value)} /> + )} diff --git a/apps/portal/src/app/features/medical/pages/MedicalCertificatePage.tsx b/apps/portal/src/app/features/medical/pages/MedicalCertificatePage.tsx index dbf0c9f31..ec1b41b8f 100644 --- a/apps/portal/src/app/features/medical/pages/MedicalCertificatePage.tsx +++ b/apps/portal/src/app/features/medical/pages/MedicalCertificatePage.tsx @@ -1,4 +1,5 @@ import { useRef, useState } from 'react'; +import { AmharicDatePicker } from '../../../components/AmharicDatePicker'; import { Alert, Badge, @@ -224,18 +225,16 @@ export function MedicalCertificatePage() { size="sm" /> - setIssuedDate(e.currentTarget.value)} + onChange={setIssuedDate} size="sm" /> - setExpiryDate(e.currentTarget.value)} + onChange={setExpiryDate} size="sm" /> diff --git a/apps/portal/src/app/features/mto-license/pages/MtoLicenseApplicationPage.tsx b/apps/portal/src/app/features/mto-license/pages/MtoLicenseApplicationPage.tsx index bcf77c176..1d6ac64f8 100644 --- a/apps/portal/src/app/features/mto-license/pages/MtoLicenseApplicationPage.tsx +++ b/apps/portal/src/app/features/mto-license/pages/MtoLicenseApplicationPage.tsx @@ -1,6 +1,7 @@ import { useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useApiMutation } from '@ema-platform/api'; +import { AmharicDatePicker } from '../../../components/AmharicDatePicker'; import { Alert, Badge, @@ -572,8 +573,8 @@ export function MtoLicenseApplicationPage() { - setInsuranceValidityDate(e.currentTarget.value)} /> - setBondValidityDate(e.currentTarget.value)} /> + + )} diff --git a/apps/portal/src/app/features/profile/components/ProfileFormContent.tsx b/apps/portal/src/app/features/profile/components/ProfileFormContent.tsx index 5ed92dd72..bf9b1aee1 100644 --- a/apps/portal/src/app/features/profile/components/ProfileFormContent.tsx +++ b/apps/portal/src/app/features/profile/components/ProfileFormContent.tsx @@ -1,6 +1,7 @@ import { Loader, Select, SimpleGrid, TextInput } from '@mantine/core'; import type { FieldErrors, UseFormRegister, UseFormSetValue, UseFormWatch, UseFormTrigger } from 'react-hook-form'; import { z } from 'zod'; +import { AmharicDatePicker } from '../../../components/AmharicDatePicker'; export const profileSchema = z.object({ professionId: z.string().min(1, 'Select your profession'), @@ -86,11 +87,13 @@ export function ProfileFormContent({ onBlur={() => trigger('gender')} name="gender" /> - setValue('dob', val, { shouldValidate: true })} + onBlur={() => trigger('dob')} + name="dob" error={errors.dob?.message} /> void }) { + const [issueDate, setIssueDate] = useState(''); + const [expiryDate, setExpiryDate] = useState(''); + return ( @@ -471,8 +475,8 @@ function AddTrainingModal({ opened, onClose }: { opened: boolean; onClose: () => - - + +