Files
ERP/frontend/src/lib/jalali.ts
T
whqreza 43ff048899 fix(wizard,quotes,dates): reliable error routing, single-project accept, Persian date/time input
Wizard:
- failed saves now route to the exact step+field: a new data-field attribute on every required section (supplier_id, line_items, termsheets, project, quote_date, expiry_date, notes, proforma) plus resolveErrorTarget() that maps ApiError.field / message keywords (supplier, line item/material/unit/quantity, termsheet/schedule/milestone, project, dates, notes, file) to the offending step — the «رفع خطا» toast action scrolls/focuses the right control instead of guessing
- material records are no longer created on the fly while typing (rememberMaterial no longer fires addMaterial per blur); materials/specs/units are written once during the final save, avoiding half-created catalog rows

Accept flow:
- quote_service no longer rewrites LineItemProjects on accept — those candidate-project rows stay 100% untouched and only item_decisions.project_id is written, matching the per-item target-project model and the UI payload (item_projects: [])

Dates/time:
- JalaliDatePicker: hour/minute are now editable text fields (Persian digits, clamping, 3-digit retype handling) with a controlled useEffect that only rewrites the inputs when the numeric value actually changes — no more wiping in-progress typing; date-only detection tightened to a full YYYY-MM-DD match so datetime strings are not misread

Also: /api validation logging (RequestValidationError) now includes the path and error list for faster diagnosis.

Verified: api + frontend image builds (python, tsc, vite) clean on top of origin/production (incl. the team's mobile layout merges).
2026-09-17 16:13:27 +03:30

158 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// ابزار تقویم شمسی — پورت jalali.js به TypeScript
// تبدیل‌ها با الگوریتم استاندارد Kazimierz M. Borkowski
// ============================================================
const FA_DIGITS = ["۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"];
export function div(a: number, b: number): number {
return Math.floor(a / b);
}
export function jalaliToGregorian(jy: number, jm: number, jd: number): [number, number, number] {
jy = +jy;
jm = +jm;
jd = +jd;
let gy = jy <= 979 ? 621 : 1600;
jy -= jy <= 979 ? 0 : 979;
let days =
365 * jy +
div(jy, 33) * 8 +
div((jy % 33) + 3, 4) +
78 +
jd +
(jm < 7 ? (jm - 1) * 31 : (jm - 7) * 30 + 186);
gy += 400 * div(days, 146097);
days %= 146097;
if (days > 36524) {
gy += 100 * div(--days, 36524);
if (days === 36524) days = 0;
days++;
}
gy += 4 * div(days, 1461);
days %= 1461;
if (days > 365) {
gy += div(days - 1, 365);
days = (days - 1) % 365;
}
let gd = days + 1;
const sal_a = [0, 31, (gy % 4 === 0 && gy % 100 !== 0) || gy % 400 === 0 ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let gm = 0;
for (; gm < 13 && gd > sal_a[gm]; gm++) gd -= sal_a[gm];
return [gy, gm, gd];
}
export function gregorianToJalali(gy: number, gm: number, gd: number): [number, number, number] {
gy = +gy;
gm = +gm;
gd = +gd;
const g_d_m = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
let jy = gy <= 1600 ? 0 : 979;
gy -= gy <= 1600 ? 621 : 1600;
let days =
365 * gy +
div(gy + 3, 4) -
div(gy + 99, 100) +
div(gy + 399, 400) -
80 +
gd +
g_d_m[gm - 1];
jy += 33 * div(days, 12053);
days %= 12053;
jy += 4 * div(days, 1461);
days %= 1461;
if (days > 365) {
jy += div(days - 1, 365);
days = (days - 1) % 365;
}
const jm = days < 186 ? 1 + div(days, 31) : 7 + div(days - 186, 30);
const jd = 1 + (days < 186 ? days % 31 : (days - 186) % 30);
return [jy, jm, jd];
}
export function isLeapJalali(jy: number): boolean {
const breaks = [-61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181, 1210, 1635, 2060, 2097, 2192, 2262, 2324, 2394, 2456, 3178];
const jp = breaks[0];
const jm = breaks[1];
let jump = 0;
let jp2 = jp;
for (const jl of breaks) {
jump = jl - jp2;
if (jy <= jl) break;
jp2 = jl;
}
let n = jy - jm;
if (n < jump) n = n + jump + 1;
return (n % 33) % 4 === 1;
}
// ---------- formatting (Persian digits) ----------
export function toFa(s: string | number): string {
return String(s).replace(/[0-9]/g, (d) => FA_DIGITS[Number(d)]);
}
// ماه‌های شمسی (نسخه‌ی درست‌شده — «مهر» به‌جای «آذر» تکراری)
export const MONTHS = [
"فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور",
"مهر", "آبان", "آذر", "دی", "بهمن", "اسفند",
];
export const WEEKDAYS = ["ش", "ی", "د", "س", "چ", "پ", "ج"];
export function toAsciiDigits(s: string | number | null | undefined): string {
if (s == null) return "";
return String(s)
.replace(/[۰-۹]/g, (d) => String("۰۱۲۳۴۵۶۷۸۹".indexOf(d)))
.replace(/[٠-٩]/g, (d) => String("٠١٢٣٤٥٦٧٨٩".indexOf(d)));
}
// format an ISO (gregorian) string to "۱۴۰۳/۰۵/۱۲"
export function isoToJalaliStr(iso: string | null | undefined): string {
if (!iso) return "";
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return "";
const [jy, jm, jd] = gregorianToJalali(d.getFullYear(), d.getMonth() + 1, d.getDate());
return `${toFa(jy)}/${toFa(String(jm).padStart(2, "0"))}/${toFa(String(jd).padStart(2, "0"))}`;
}
// format with month name: "۱۸ مرداد ۱۴۰۴"
export function isoToJalaliLong(iso: string | null | undefined): string {
if (!iso) return "";
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return "";
const [jy, jm, jd] = gregorianToJalali(d.getFullYear(), d.getMonth() + 1, d.getDate());
return `${toFa(jd)} ${MONTHS[jm - 1]} ${toFa(jy)}`;
}
// parse a jalali "yyyy/m/d" or "yyyy/m/d h:m" string to a JS Date (gregorian) ISO string
export function jalaliStrToIso(str: string | null | undefined): string | null {
if (!str) return null;
const s = toAsciiDigits(str).trim();
const m = s.match(/^(1[34]\d{2})[/\-.](\d{1,2})[/\-.](\d{1,2})(?:[ T](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?/);
if (!m) {
const nums = s
.split(/[/\-.]/)
.map((x) => parseInt(x, 10));
if (nums.length < 3 || nums.some((n) => Number.isNaN(n))) return null;
const [jy, jm, jd] = nums;
const [gy, gm, gd] = jalaliToGregorian(jy, jm, jd);
const dt = new Date(gy, gm - 1, gd);
return Number.isNaN(dt.getTime()) ? null : dt.toISOString();
}
const jy = parseInt(m[1], 10);
const jm = parseInt(m[2], 10);
const jd = parseInt(m[3], 10);
const h = m[4] != null ? parseInt(m[4], 10) : 0;
const min = m[5] != null ? parseInt(m[5], 10) : 0;
const sec = m[6] != null ? parseInt(m[6], 10) : 0;
const [gy, gm, gd] = jalaliToGregorian(jy, jm, jd);
const dt = new Date(gy, gm - 1, gd, h, min, sec);
return Number.isNaN(dt.getTime()) ? null : dt.toISOString();
}
export function daysInMonth(jy: number, jm: number): number {
if (jm <= 6) return 31;
if (jm <= 11) return 30;
return isLeapJalali(jy) ? 30 : 29;
}