improvement: rtl direction, solar calendar and persian formatting added for persian users.

This commit is contained in:
2026-07-14 01:27:11 +03:30
parent 27eae25f61
commit ec2b23f4b1
50 changed files with 1410 additions and 528 deletions

View File

@@ -1,6 +1,6 @@
'use client';
import { useTranslations } from 'next-intl';
import { useLocale, useTranslations } from 'next-intl';
import { CalendarDays } from 'lucide-react';
import { Card } from '@/components/ui/shared/Card';
import { ScheduleDayPicker } from '@/components/ui/shared/ScheduleDayPicker';
@@ -11,6 +11,7 @@ import {
} from '@/components/shared/treatmentTypeDisplay';
import type { TreatmentCatalogEntry } from '@/types/treatment-catalog';
import type { TreatmentAppointment } from '@/types/treatment';
import { APP_DATE, formatAppTimeRange } from '@/lib/i18n/format';
interface AppointmentsStripProps {
stripHidden: boolean;
@@ -35,6 +36,7 @@ export function AppointmentsStrip({
treatmentCatalog,
loading = false,
}: AppointmentsStripProps) {
const locale = useLocale();
const t = useTranslations('treatment');
if (stripHidden) {
@@ -84,15 +86,7 @@ export function AppointmentsStrip({
)}
{appointments.map((a) => {
const sel = a.id === selectedAppointmentId;
const start = new Date(a.startAt);
const end = new Date(a.endAt);
const timeLabel = `${start.toLocaleTimeString(undefined, {
hour: 'numeric',
minute: '2-digit',
})} ${end.toLocaleTimeString(undefined, {
hour: 'numeric',
minute: '2-digit',
})}`;
const timeLabel = formatAppTimeRange(a.startAt, a.endAt, locale);
const purposeLabel = treatmentTypeLabelFromCatalog(a.purpose, treatmentCatalog);
const purposeIndex = treatmentCatalog.findIndex((e) => e.code === a.purpose);
return (
@@ -104,7 +98,7 @@ export function AppointmentsStrip({
padding="none"
style={treatmentTypeBannerStyle(a.purpose, purposeIndex < 0 ? 0 : purposeIndex)}
className={`
text-left rounded-[var(--radius-sm)] px-3 py-2 w-full sm:w-auto sm:min-w-[200px] sm:max-w-[280px] transition-shadow min-h-[52px]
text-start rounded-[var(--radius-sm)] px-3 py-2 w-full sm:w-auto sm:min-w-[200px] sm:max-w-[280px] transition-shadow min-h-[52px]
focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/45
${sel ? 'ring-2 ring-primary ring-offset-2 ring-offset-background-secondary shadow-[inset_0_1px_0_rgba(255,255,255,0.06)]' : 'hover:brightness-110'}
`}

View File

@@ -1,6 +1,6 @@
'use client';
import { useTranslations } from 'next-intl';
import { useLocale, useTranslations } from 'next-intl';
import { formatCaseSentLines } from '@/components/treatment/caseSendLabel';
import type { LabCaseSendInfo, LinkedOrganizationOption } from '@/types/treatment';
@@ -16,6 +16,7 @@ interface CaseSentLabelProps {
}
export function CaseSentLabel({ treatmentCase, orgs, className = 'text-xs text-text-muted' }: CaseSentLabelProps) {
const locale = useLocale();
const t = useTranslations('treatment');
const organizationIds =
treatmentCase.sendToOrganizationIds ??
@@ -24,7 +25,7 @@ export function CaseSentLabel({ treatmentCase, orgs, className = 'text-xs text-t
organizationIds,
sentAt: treatmentCase.sentAt ?? null,
orgs,
}, t);
}, t, locale);
if (lines.length === 0) return null;

View File

@@ -5,6 +5,7 @@ import { useTranslations } from 'next-intl';
import { Button } from '@/components/ui/shared/Button';
import { Checkbox } from '@/components/ui/shared/Checkbox';
import { isDetailReadyForLabDispatch, isLabCaseCompleted } from '@/components/treatment/treatmentDetailRules';
import { AppDateInput } from '@/components/ui/shared/AppDateInput';
import { toDateInputValue } from '@/components/lab/labCaseDueDateDisplay';
import { FORM_SELECT_CLASS } from '@/components/shared/formSelectStyles';
import { LinkedOrganizationSearchCombobox } from '@/components/ui/treatment/LinkedOrganizationSearchCombobox';
@@ -283,20 +284,17 @@ export function LabCasesDispatchPanel({
{t('dueDateLabel')}{' '}
<span className="font-normal text-text-muted">({t('dueDateOptional')})</span>
</label>
<input
<AppDateInput
id={dueDateInputId}
type="date"
value={inputValue}
disabled={!canEditDueDate}
onChange={(e) => {
if (!sent) {
updateActiveLabCase({ dueDate: e.target.value || null });
}
onChange={(next) => {
updateActiveLabCase({ dueDate: next || null });
}}
onBlur={(e) => {
if (sent) void handleSentDueDateBlur(e.target.value);
onBlur={(committed) => {
if (sent) void handleSentDueDateBlur(committed);
}}
className={`${FORM_SELECT_CLASS} w-full max-w-[11rem] rounded-md px-2 py-1.5 text-sm`}
className={`${FORM_SELECT_CLASS} w-full max-w-[11rem] rounded-md py-1.5 text-sm`}
/>
</div>
{sent && caseFullyComplete && activeLabCase.dueDate ? (

View File

@@ -1,6 +1,6 @@
'use client';
import { useTranslations } from 'next-intl';
import { useLocale, useTranslations } from 'next-intl';
import { AlertTriangle } from 'lucide-react';
import { Button } from '@/components/ui/shared/Button';
import { DetailLabSendBadge } from '@/components/ui/treatment/DetailLabSendBadge';
@@ -9,6 +9,7 @@ import { treatmentTypeLabelFromCatalog } from '@/components/shared/treatmentType
import type { LabDispatchAttentionItem } from '@/components/treatment/labDispatchAttention';
import type { LinkedOrganizationOption } from '@/types/treatment';
import type { TreatmentCatalogEntry } from '@/types/treatment-catalog';
import { APP_DATE, formatAppDate } from '@/lib/i18n/format';
interface LabDispatchAttentionPanelProps {
items: LabDispatchAttentionItem[];
@@ -27,6 +28,7 @@ export function LabDispatchAttentionPanel({
onGoToDispatch,
compact = false,
}: LabDispatchAttentionPanelProps) {
const locale = useLocale();
const t = useTranslations('treatment');
if (items.length === 0) {
@@ -54,11 +56,7 @@ export function LabDispatchAttentionPanel({
const teeth = item.detail.teeth.length
? [...item.detail.teeth].sort().join(', ')
: t('teethNone');
const dateLabel = new Date(item.treatmentAt).toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
});
const dateLabel = formatAppDate(item.treatmentAt, locale, APP_DATE.short);
return (
<li

View File

@@ -1,13 +1,15 @@
'use client';
import { useMemo, useState } from 'react';
import { useTranslations } from 'next-intl';
import { useLocale, useTranslations } from 'next-intl';
import { Button } from '@/components/ui/shared/Button';
import { Checkbox } from '@/components/ui/shared/Checkbox';
import { FORM_SELECT_CLASS } from '@/components/shared/formSelectStyles';
import { TreatmentHistoryDetailLine } from '@/components/ui/treatment/TreatmentHistoryDetailLine';
import { DetailLabSendBadge } from '@/components/ui/treatment/DetailLabSendBadge';
import { filterTreatmentHistoryItems } from '@/components/treatment/treatmentHistoryFilters';
import { AppDateInput } from '@/components/ui/shared/AppDateInput';
import { APP_DATE, formatAppDateTime } from '@/lib/i18n/format';
import type { LinkedOrganizationOption, PastTreatment } from '@/types/treatment';
import type { TreatmentCatalogEntry } from '@/types/treatment-catalog';
@@ -25,16 +27,8 @@ interface PastTreatmentsPanelProps {
compact?: boolean;
}
function formatHistoryTimestamp(iso: string): string {
const date = new Date(iso);
return date.toLocaleString(undefined, {
weekday: 'short',
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
});
function formatHistoryTimestamp(iso: string, locale: string): string {
return formatAppDateTime(iso, locale, APP_DATE.history);
}
export function PastTreatmentsPanel({
@@ -50,6 +44,7 @@ export function PastTreatmentsPanel({
onSelectTreatment,
compact = false,
}: PastTreatmentsPanelProps) {
const locale = useLocale();
const t = useTranslations('treatment');
const [notShippedOnly, setNotShippedOnly] = useState(false);
const [filterDate, setFilterDate] = useState('');
@@ -71,7 +66,7 @@ export function PastTreatmentsPanel({
setFilterDate('');
}
const filterInputClass = `${FORM_SELECT_CLASS} rounded-md px-2 py-1.5 text-xs min-w-[9.5rem]`;
const filterInputClass = `${FORM_SELECT_CLASS} rounded-md py-1.5 text-xs min-w-[9.5rem]`;
const filtersBlock = (
<div
@@ -89,10 +84,9 @@ export function PastTreatmentsPanel({
<span className="text-xs font-medium text-text-muted whitespace-nowrap">
{t('historyFilterDate')}
</span>
<input
type="date"
<AppDateInput
value={filterDate}
onChange={(e) => setFilterDate(e.target.value)}
onChange={setFilterDate}
className={filterInputClass}
/>
</label>
@@ -156,7 +150,7 @@ export function PastTreatmentsPanel({
className="text-xs font-medium text-text-primary tabular-nums"
dateTime={treatment.treatmentAt}
>
{formatHistoryTimestamp(treatment.treatmentAt)}
{formatHistoryTimestamp(treatment.treatmentAt, locale)}
</time>
{isLiveDraft ? (
<span className="text-[10px] font-medium uppercase tracking-wide text-primary">

View File

@@ -1,7 +1,8 @@
'use client';
import { useTranslations } from 'next-intl';
import { useLocale, useTranslations } from 'next-intl';
import { TreatmentDetailSummaryRow } from '@/components/ui/treatment/TreatmentDetailSummaryRow';
import { APP_DATE, formatAppDate } from '@/lib/i18n/format';
import type { LinkedOrganizationOption, PastTreatment } from '@/types/treatment';
import type { TreatmentCatalogEntry } from '@/types/treatment-catalog';
@@ -23,6 +24,7 @@ export function TreatmentPreviewCard({
orgs,
embedded = false,
}: TreatmentPreviewCardProps) {
const locale = useLocale();
const t = useTranslations('treatment');
return (
@@ -38,12 +40,7 @@ export function TreatmentPreviewCard({
className="text-xs text-text-muted tabular-nums block"
dateTime={treatment.treatmentAt}
>
{new Date(treatment.treatmentAt).toLocaleDateString(undefined, {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
})}
{formatAppDate(treatment.treatmentAt, locale, APP_DATE.withWeekday)}
</time>
) : null}
<div className="space-y-2 max-h-[min(280px,40vh)] overflow-y-auto pr-1">

View File

@@ -28,7 +28,7 @@ export function TreatmentRailSection({
: 'surface-card';
return (
<section className={`${shellClass} overflow-hidden`}>
<section className={`treatment-rail-section ${shellClass} overflow-hidden`}>
<button
type="button"
onClick={() => setExpanded((open) => !open)}

View File

@@ -1,7 +1,7 @@
'use client';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslations } from 'next-intl';
import { useLocale, useTranslations } from 'next-intl';
import { useRouter } from '@/i18n/navigation';
import { Button } from '@/components/ui/shared/Button';
import { Checkbox } from '@/components/ui/shared/Checkbox';
@@ -46,6 +46,7 @@ import { scrollWithinMainScrollContainer } from '@/components/shared/scrollWithi
import { useMarkTabReadOnVisit, useTabBadgeCounts } from '@/lib/hooks/useTabBadgeCounts';
import { tabBadgesChangedEventName } from '@/lib/tabBadgeUtils';
import { notifyTabBadgesChanged } from '@/lib/tabBadgeUtils';
import { APP_DATE, formatAppDate } from '@/lib/i18n/format';
import { useAuth } from '@/lib/hooks/useAuth';
import { getUserFacingError } from '@/components/shared/formatApiError';
import { useToast } from '@/lib/hooks/useToast';
@@ -284,13 +285,13 @@ export function TreatmentWorkspace({
currentOrganization,
initialAppointmentId = null,
}: TreatmentWorkspaceProps) {
const locale = useLocale();
const t = useTranslations('treatment');
const tErrors = useTranslations('errors');
const tPatients = useTranslations('patients');
const router = useRouter();
const { user } = useAuth();
const { showError, showSuccess, messages: toastMessages } = useToast();
const locale = user?.language ?? 'en';
const canView = canViewTreatment(currentOrganization);
const canEdit = canEditTreatment(currentOrganization);
useMarkTabReadOnVisit();
@@ -1571,7 +1572,7 @@ export function TreatmentWorkspace({
</p>
)}
<div className="grid grid-cols-1 xl:grid-cols-[minmax(300px,380px)_minmax(0,1fr)] gap-4 items-start">
<div className="treatment-layout-grid grid grid-cols-1 xl:grid-cols-[minmax(300px,380px)_minmax(0,1fr)] gap-4 items-start">
<div className="space-y-3 min-w-0 xl:max-w-[380px]">
<div className="surface-card p-3 space-y-3">
<PatientSearchCombobox
@@ -1619,12 +1620,7 @@ export function TreatmentWorkspace({
<div className="rounded-[var(--radius-md)] border border-primary/40 bg-primary/5 px-3 py-2 space-y-2">
<p className="text-xs text-text-primary">
{t('browseBanner', {
date: new Date(previewTreatment.treatmentAt).toLocaleDateString(undefined, {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
}),
date: formatAppDate(previewTreatment.treatmentAt, locale, APP_DATE.withWeekday),
})}
</p>
<div className="flex flex-wrap gap-2">