feat(frontend): add React UI — procurement office, wizard, suppliers, inventory panel (dash port), role-based login

This commit is contained in:
ali_mohaghegh
2026-08-25 19:08:15 +03:30
parent 170aed66ea
commit 4e105c5cd5
103 changed files with 16352 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
// ============================================================
// ابزار تقویم شمسی — پورت 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 = ["ش", "ی", "د", "س", "چ", "پ", "ج"];
function toEnSafe(s: string): string {
return String(s).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" string to a JS Date (gregorian) at local midnight
export function jalaliStrToIso(str: string | null | undefined): string | null {
if (!str) return null;
const nums = String(str)
.split(/[/\-.]/)
.map((x) => parseInt(toEnSafe(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();
}
export function daysInMonth(jy: number, jm: number): number {
if (jm <= 6) return 31;
if (jm <= 11) return 30;
return isLeapJalali(jy) ? 30 : 29;
}