refactor: normalize date handling to Tehran timezone

- Updated date handling in BlogSeoFields and ScheduleSection to use Tehran timezone utilities for consistency.
- Introduced `toTehranClockTime`, `tehranWallClockToUnix`, and `todayIso` functions for accurate date representation.
- Modified various components to utilize these new utilities, ensuring that date strings are correctly formatted and timestamps are accurately converted.
- Enhanced API documentation to clarify the handling of date fields, emphasizing the importance of server-local midnight.
- Added tests to verify that date overrides and holidays maintain the correct day without shifting due to timezone discrepancies.
This commit is contained in:
hamed
2026-07-27 19:37:44 +03:30
parent 2963e2ac74
commit b423a0ae4d
21 changed files with 267 additions and 57 deletions
+2 -2
View File
@@ -14,7 +14,7 @@ import { WalletChargeLink } from '../components/AppointmentActions';
import { useDoctorBookingServices } from '../hooks/useDoctorBookingServices';
import ServiceSlotPicker from '../components/appointments/ServiceSlotPicker';
import { tehranWallClockToUnix, rialToToman, tomanToRial } from '../lib/utils';
import { digitsOnly } from '../lib/utils';
import { digitsOnly, todayIso } from '../lib/utils';
/**
* افزودن نوبت — صفحهٔ کامل (بازسازی `CreateTurn.jsx` طرح tauri). از همان endpointهای
@@ -43,7 +43,7 @@ export default function AppointmentCreatePage() {
const dbUuid = useAuthStore(s => s.dbUuid);
const isDoctor = primaryRole === 'doctor';
const today = new Date().toISOString().slice(0, 10);
const today = todayIso();
// ── پزشک
const [doctorUuid, setDoctorUuid] = useState(isDoctor && dbUuid ? dbUuid : (params.get('doctor') ?? ''));
+2 -2
View File
@@ -9,7 +9,7 @@ import { toast } from 'sonner';
import { api } from '../lib/api';
import type { PaginatedResponse, ApiResponse } from '../lib/api';
import type { Appointment } from '../types';
import { formatDate, toGregorianDate, formatTime, digitsOnly, sanitizeMobileInput, rialToToman, tomanToRial, formatRial } from '../lib/utils';
import { formatDate, toGregorianDate, todayIso, formatTime, digitsOnly, sanitizeMobileInput, rialToToman, tomanToRial, formatRial } from '../lib/utils';
import PriceInput from '../components/ui/PriceInput';
import Modal from '../components/ui/Modal';
import { useAuthStore } from '../stores/authStore';
@@ -507,7 +507,7 @@ export default function AppointmentsPage() {
const canCancelAppt = can('appointments', 'cancel');
const [params] = useSearchParams();
const today = new Date().toISOString().slice(0, 10);
const today = todayIso();
// پس از ویرایش/ثبت، صفحه با ?date=... باز می‌شود تا همان روز نمایش داده شود.
const [selectedDate, setSelectedDate] = useState(params.get('date') || today);
const [viewMode, setViewMode] = useState<TurnsViewMode>('timeline');
+2 -3
View File
@@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { ArrowPathIcon } from '@heroicons/react/24/outline';
import { api } from '../lib/api';
import { formatRial, formatNumber } from '../lib/utils';
import { formatRial, formatNumber, toGregorianDate, todayIso } from '../lib/utils';
import type { ClaimPatientRow } from '../types';
import type { ApiResponse, PaginatedResponse } from '../lib/api';
import PageHeader from '../components/ui/PageHeader';
@@ -35,9 +35,8 @@ const PAYMENT_OPTIONS = [
const isoNDaysAgo = (days: number): string => {
const d = new Date();
d.setDate(d.getDate() - days);
return d.toISOString().slice(0, 10);
return toGregorianDate(d);
};
const todayIso = (): string => new Date().toISOString().slice(0, 10);
const toUnix = (iso: string): number | null => {
if (!iso) return null;
+2 -3
View File
@@ -7,7 +7,7 @@ import SearchableSelect from '../components/ui/SearchableSelect';
import StatCard from '../components/ui/StatCard';
import StatusBadge from '../components/ui/StatusBadge';
import DataTable, { type Column } from '../components/ui/DataTable';
import { formatRial, formatDate, formatNumber, toDate } from '../lib/utils';
import { formatRial, formatDate, formatNumber, toDate, toGregorianDate, todayIso } from '../lib/utils';
import {
usePayments,
usePaymentsSummary,
@@ -25,9 +25,8 @@ const STATUS_OPTIONS = [
const isoNDaysAgo = (days: number): string => {
const d = new Date();
d.setDate(d.getDate() - days);
return d.toISOString().slice(0, 10);
return toGregorianDate(d);
};
const todayIso = (): string => new Date().toISOString().slice(0, 10);
/** HH:MM (Persian digits) from a unix timestamp. */
function formatTime(unix: number): string {
+2 -2
View File
@@ -13,7 +13,7 @@ import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
import type { PatientRecord } from '../types';
import { useAuthStore } from '../stores/authStore';
import { formatDate, formatDateTime, formatRial, formatTime, formatNumber } from '../lib/utils';
import { formatDate, formatDateTime, formatRial, formatTime, formatNumber, unixToIso } from '../lib/utils';
import DataTable, { type Column } from '../components/ui/DataTable';
import type { WalletTxn } from '../hooks/usePatientWallet';
import type { WalletModalSubmit } from '../components/WalletTransactionModal';
@@ -390,7 +390,7 @@ function MedicalRecordsTab({ uuid }: { uuid: string }) {
const open = (m?: MedicalItem) => {
setTitle(m?.title ?? ''); setBody(m?.body ?? '');
setDate(m?.recorded_at ? new Date(m.recorded_at * 1000).toISOString().slice(0, 10) : '');
setDate(unixToIso(m?.recorded_at));
setModal(m ?? 'create');
};
+2 -2
View File
@@ -12,7 +12,7 @@ import type { PatientRecord } from '../types';
import PersianDateInput from '../components/ui/PersianDateInput';
import SearchableSelect from '../components/ui/SearchableSelect';
import { numericField } from '../lib/forms';
import { iranNationalCodeSchema, iranMobileSchema } from '../lib/utils';
import { iranNationalCodeSchema, iranMobileSchema, unixToIso } from '../lib/utils';
const REFERRAL_OPTIONS = ['اینستاگرام', 'معرفی دوستان و آشنایان', 'جستجوی اینترنتی', 'تابلو مطب', 'سایر'];
@@ -29,7 +29,7 @@ const schema = z.object({
type Form = z.infer<typeof schema>;
const toEpoch = (iso?: string) => (iso ? Math.floor(new Date(iso).getTime() / 1000) : null);
const fromEpoch = (ts?: number | null) => (ts ? new Date(ts * 1000).toISOString().slice(0, 10) : '');
const fromEpoch = (ts?: number | null) => unixToIso(ts);
/** تشکیل/ویرایش پرونده — patient record create & edit form (Figma "تشکیل پرونده"). */
export default function PatientRecordFormPage() {
+1 -1
View File
@@ -114,7 +114,7 @@ export default function PaymentDetailPage() {
{payment.refunds.map((r, i) => (
<InfoRow
key={i}
label={formatDateTime(new Date(r.at * 1000).toISOString())}
label={formatDateTime(r.at)}
value={
<span>
{formatRial(r.amount)}