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).
482 lines
18 KiB
TypeScript
482 lines
18 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
||
import {
|
||
daysInMonth,
|
||
gregorianToJalali,
|
||
isoToJalaliStr,
|
||
jalaliStrToIso,
|
||
jalaliToGregorian,
|
||
MONTHS,
|
||
toAsciiDigits,
|
||
toFa,
|
||
WEEKDAYS,
|
||
} from "../lib/jalali";
|
||
import { faNum } from "../lib/formatters";
|
||
import { cn } from "../lib/utils";
|
||
import { useClickOutside } from "./useClickOutside";
|
||
|
||
export interface JalaliDatePickerProps {
|
||
value: string | null; // ISO date (یا YYYY-MM-DD با time اختیاری)
|
||
onChange: (val: string | null) => void;
|
||
placeholder?: string;
|
||
className?: string;
|
||
withTime?: boolean; // نمایش ساعت:دقیقه هم (پیشنهاد/پذیرش/رسید)
|
||
disabled?: boolean;
|
||
}
|
||
|
||
function todayJalali(): [number, number, number] {
|
||
const now = new Date();
|
||
return gregorianToJalali(now.getFullYear(), now.getMonth() + 1, now.getDate());
|
||
}
|
||
|
||
function isoToJalali(iso: string | null): [number, number, number] | null {
|
||
if (!iso) return null;
|
||
const m = String(iso).match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||
if (m) {
|
||
const gy = parseInt(m[1], 10);
|
||
const gm = parseInt(m[2], 10);
|
||
const gd = parseInt(m[3], 10);
|
||
return gregorianToJalali(gy, gm, gd);
|
||
}
|
||
const d = new Date(iso);
|
||
if (Number.isNaN(d.getTime())) return null;
|
||
return gregorianToJalali(d.getFullYear(), d.getMonth() + 1, d.getDate());
|
||
}
|
||
|
||
// ساخت ISO با زمان محلی (تاریخ شمسی + ساعت:دقیقه)
|
||
function buildIso(jy: number, jm: number, jd: number, h: number, m: number): string {
|
||
const [gy, gm, gd] = jalaliToGregorian(jy, jm, jd);
|
||
return new Date(gy, gm - 1, gd, h, m).toISOString();
|
||
}
|
||
|
||
// ساخت تاریخ استاندارد YYYY-MM-DD بدون مؤلفه زمان
|
||
function buildDateOnly(jy: number, jm: number, jd: number): string {
|
||
const [gy, gm, gd] = jalaliToGregorian(jy, jm, jd);
|
||
return `${gy}-${String(gm).padStart(2, "0")}-${String(gd).padStart(2, "0")}`;
|
||
}
|
||
|
||
// وقتی هنوز زمان مشخصی ست نشده، ساعت/دقیقهی فعلی سیستم پیشفرض است
|
||
// (نه ۰۰:۰۰) — دکمهی «امروز» و انتخاب روز، همین زمان را نگه میدارند.
|
||
function nowTime(): { h: number; m: number } {
|
||
const d = new Date();
|
||
return { h: d.getHours(), m: d.getMinutes() };
|
||
}
|
||
|
||
function timeOf(iso: string | null): { h: number; m: number } {
|
||
if (!iso) return nowTime();
|
||
const d = new Date(iso);
|
||
if (Number.isNaN(d.getTime())) return nowTime();
|
||
return { h: d.getHours(), m: d.getMinutes() };
|
||
}
|
||
|
||
// تقویم شمسی — پورت Jalali.buildPicker به React (با پشتیبانی ساعت:دقیقه)
|
||
export function JalaliDatePicker({
|
||
value,
|
||
onChange,
|
||
placeholder,
|
||
className,
|
||
withTime = false,
|
||
disabled = false,
|
||
}: JalaliDatePickerProps) {
|
||
const [open, setOpen] = useState(false);
|
||
const [curJ, setCurJ] = useState<[number, number, number]>(() => isoToJalali(value) ?? todayJalali());
|
||
const initialTime = timeOf(value);
|
||
const [hourText, setHourText] = useState<string>(() => faNum(String(initialTime.h).padStart(2, "0")));
|
||
const [minuteText, setMinuteText] = useState<string>(() => faNum(String(initialTime.m).padStart(2, "0")));
|
||
|
||
const [text, setText] = useState<string>(() => {
|
||
if (!value) return "";
|
||
const j = isoToJalaliStr(value);
|
||
if (!withTime) return j;
|
||
const d = new Date(value);
|
||
if (Number.isNaN(d.getTime())) return j;
|
||
const hh = String(d.getHours()).padStart(2, "0");
|
||
const mm = String(d.getMinutes()).padStart(2, "0");
|
||
return `${j} ${faNum(hh + ":" + mm)}`;
|
||
});
|
||
|
||
useEffect(() => {
|
||
if (!value) {
|
||
setText("");
|
||
const now = nowTime();
|
||
setHourText(faNum(String(now.h).padStart(2, "0")));
|
||
setMinuteText(faNum(String(now.m).padStart(2, "0")));
|
||
return;
|
||
}
|
||
const j = isoToJalaliStr(value);
|
||
const t = timeOf(value);
|
||
if (!withTime) {
|
||
setText(j);
|
||
} else {
|
||
const hh = String(t.h).padStart(2, "0");
|
||
const mm = String(t.m).padStart(2, "0");
|
||
setText(`${j} ${faNum(hh + ":" + mm)}`);
|
||
|
||
// Only update local input texts if numeric values differ, to avoid wiping in-progress typing
|
||
const curH = parseInt(toAsciiDigits(hourText) || "0", 10);
|
||
if (curH !== t.h) {
|
||
setHourText(faNum(hh));
|
||
}
|
||
const curM = parseInt(toAsciiDigits(minuteText) || "0", 10);
|
||
if (curM !== t.m) {
|
||
setMinuteText(faNum(mm));
|
||
}
|
||
}
|
||
const jParts = isoToJalali(value);
|
||
if (jParts) setCurJ(jParts);
|
||
}, [value, withTime]);
|
||
|
||
const ref = useClickOutside<HTMLDivElement>(() => setOpen(false));
|
||
|
||
// تاریخ انتخابشده به صورت محلی (نه UTC) برای مقایسه با سلولهای تقویم
|
||
function selectedLocalDate(): string | null {
|
||
if (!value) return null;
|
||
const m = String(value).match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||
if (m) return `${m[1]}-${m[2]}-${m[3]}`;
|
||
const d = new Date(value);
|
||
if (Number.isNaN(d.getTime())) return null;
|
||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||
}
|
||
const selectedIso = selectedLocalDate();
|
||
|
||
const grid = useMemo(() => {
|
||
const [jy, jm] = curJ;
|
||
const firstG = jalaliToGregorian(jy, jm, 1);
|
||
const firstWd = (new Date(firstG[0], firstG[1] - 1, firstG[2]).getDay() + 1) % 7; // شنبه = 0
|
||
const dim = daysInMonth(jy, jm);
|
||
const today = new Date();
|
||
const tIso = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
|
||
const cells: { d: number; iso: string; isToday: boolean }[] = [];
|
||
for (let i = 0; i < firstWd; i++) cells.push({ d: 0, iso: "", isToday: false });
|
||
for (let d = 1; d <= dim; d++) {
|
||
const [gy, gm, gd] = jalaliToGregorian(jy, jm, d);
|
||
const iso = `${gy}-${String(gm).padStart(2, "0")}-${String(gd).padStart(2, "0")}`;
|
||
cells.push({ d, iso, isToday: iso === tIso });
|
||
}
|
||
return cells;
|
||
}, [curJ]);
|
||
|
||
const nav = (dir: string) => {
|
||
setCurJ(([jy, jm]) => {
|
||
let ny = jy;
|
||
let nm = jm;
|
||
if (dir === "m-1") nm = nm === 12 ? 1 : nm + 1;
|
||
if (dir === "m+1") nm = nm === 1 ? 12 : nm - 1;
|
||
if (dir === "y-1") ny += 1;
|
||
if (dir === "y+1") ny -= 1;
|
||
if (nm < 1) {
|
||
nm = 12;
|
||
ny -= 1;
|
||
}
|
||
if (nm > 12) {
|
||
nm = 1;
|
||
ny += 1;
|
||
}
|
||
return [ny, nm, 1];
|
||
});
|
||
};
|
||
|
||
const selectDay = (jy: number, jm: number, jd: number) => {
|
||
const currentH = parseInt(toAsciiDigits(hourText) || "0", 10);
|
||
const currentM = parseInt(toAsciiDigits(minuteText) || "0", 10);
|
||
const validH = Math.min(23, Math.max(0, currentH));
|
||
const validM = Math.min(59, Math.max(0, currentM));
|
||
const val = withTime ? buildIso(jy, jm, jd, validH, validM) : buildDateOnly(jy, jm, jd);
|
||
onChange(val);
|
||
setText(
|
||
`${toFa(jy)}/${toFa(String(jm).padStart(2, "0"))}/${toFa(String(jd).padStart(2, "0"))}${withTime ? " " + faNum(String(validH).padStart(2, "0") + ":" + String(validM).padStart(2, "0")) : ""}`
|
||
);
|
||
setOpen(false);
|
||
};
|
||
|
||
const handleHourChange = (raw: string) => {
|
||
let digits = toAsciiDigits(raw).replace(/\D/g, "");
|
||
if (digits.length === 3) {
|
||
digits = digits.slice(-1);
|
||
} else if (digits.length > 3) {
|
||
digits = digits.slice(0, 2);
|
||
}
|
||
if (!digits) {
|
||
setHourText("");
|
||
return;
|
||
}
|
||
const n = parseInt(digits, 10);
|
||
const clamped = Math.min(23, Math.max(0, n));
|
||
const textToShow = digits.length === 2 && n > 23 ? String(clamped) : digits;
|
||
setHourText(faNum(textToShow));
|
||
|
||
const [jy, jm, jd] = curJ;
|
||
const currentM = parseInt(toAsciiDigits(minuteText) || "0", 10);
|
||
const validM = Math.min(59, Math.max(0, currentM));
|
||
onChange(buildIso(jy, jm, jd, clamped, validM));
|
||
const j = value ? isoToJalaliStr(value) : isoToJalaliStr(buildIso(jy, jm, jd, clamped, validM));
|
||
setText(`${j} ${faNum(String(clamped).padStart(2, "0") + ":" + String(validM).padStart(2, "0"))}`);
|
||
};
|
||
|
||
const handleHourBlur = () => {
|
||
const digits = toAsciiDigits(hourText).replace(/\D/g, "");
|
||
const n = digits ? parseInt(digits, 10) : 0;
|
||
const clamped = Math.min(23, Math.max(0, n));
|
||
setHourText(faNum(String(clamped).padStart(2, "0")));
|
||
|
||
const [jy, jm, jd] = curJ;
|
||
const currentM = parseInt(toAsciiDigits(minuteText) || "0", 10);
|
||
const validM = Math.min(59, Math.max(0, currentM));
|
||
onChange(buildIso(jy, jm, jd, clamped, validM));
|
||
const j = value ? isoToJalaliStr(value) : isoToJalaliStr(buildIso(jy, jm, jd, clamped, validM));
|
||
setText(`${j} ${faNum(String(clamped).padStart(2, "0") + ":" + String(validM).padStart(2, "0"))}`);
|
||
};
|
||
|
||
const handleMinuteChange = (raw: string) => {
|
||
let digits = toAsciiDigits(raw).replace(/\D/g, "");
|
||
if (digits.length === 3) {
|
||
digits = digits.slice(-1);
|
||
} else if (digits.length > 3) {
|
||
digits = digits.slice(0, 2);
|
||
}
|
||
if (!digits) {
|
||
setMinuteText("");
|
||
return;
|
||
}
|
||
const n = parseInt(digits, 10);
|
||
const clamped = Math.min(59, Math.max(0, n));
|
||
const textToShow = digits.length === 2 && n > 59 ? String(clamped) : digits;
|
||
setMinuteText(faNum(textToShow));
|
||
|
||
const [jy, jm, jd] = curJ;
|
||
const currentH = parseInt(toAsciiDigits(hourText) || "0", 10);
|
||
const validH = Math.min(23, Math.max(0, currentH));
|
||
onChange(buildIso(jy, jm, jd, validH, clamped));
|
||
const j = value ? isoToJalaliStr(value) : isoToJalaliStr(buildIso(jy, jm, jd, validH, clamped));
|
||
setText(`${j} ${faNum(String(validH).padStart(2, "0") + ":" + String(clamped).padStart(2, "0"))}`);
|
||
};
|
||
|
||
const handleMinuteBlur = () => {
|
||
const digits = toAsciiDigits(minuteText).replace(/\D/g, "");
|
||
const n = digits ? parseInt(digits, 10) : 0;
|
||
const clamped = Math.min(59, Math.max(0, n));
|
||
setMinuteText(faNum(String(clamped).padStart(2, "0")));
|
||
|
||
const [jy, jm, jd] = curJ;
|
||
const currentH = parseInt(toAsciiDigits(hourText) || "0", 10);
|
||
const validH = Math.min(23, Math.max(0, currentH));
|
||
onChange(buildIso(jy, jm, jd, validH, clamped));
|
||
const j = value ? isoToJalaliStr(value) : isoToJalaliStr(buildIso(jy, jm, jd, validH, clamped));
|
||
setText(`${j} ${faNum(String(validH).padStart(2, "0") + ":" + String(clamped).padStart(2, "0"))}`);
|
||
};
|
||
|
||
const pickToday = () => {
|
||
const now = new Date();
|
||
const [jy, jm, jd] = todayJalali();
|
||
const nh = now.getHours();
|
||
const nm = now.getMinutes();
|
||
setHourText(faNum(String(nh).padStart(2, "0")));
|
||
setMinuteText(faNum(String(nm).padStart(2, "0")));
|
||
const val = withTime
|
||
? buildIso(jy, jm, jd, nh, nm)
|
||
: buildDateOnly(jy, jm, jd);
|
||
onChange(val);
|
||
setText(
|
||
withTime
|
||
? `${toFa(jy)}/${toFa(String(jm).padStart(2, "0"))}/${toFa(String(jd).padStart(2, "0"))} ${faNum(String(nh).padStart(2, "0") + ":" + String(nm).padStart(2, "0"))}`
|
||
: `${toFa(jy)}/${toFa(String(jm).padStart(2, "0"))}/${toFa(String(jd).padStart(2, "0"))}`,
|
||
);
|
||
setOpen(false);
|
||
};
|
||
|
||
const clear = () => {
|
||
onChange(null);
|
||
setText("");
|
||
const now = nowTime();
|
||
setHourText(faNum(String(now.h).padStart(2, "0")));
|
||
setMinuteText(faNum(String(now.m).padStart(2, "0")));
|
||
setOpen(false);
|
||
};
|
||
|
||
const onInputChange = (v: string) => {
|
||
setText(v);
|
||
if (!v.trim()) {
|
||
onChange(null);
|
||
return;
|
||
}
|
||
const iso = jalaliStrToIso(v);
|
||
if (iso) {
|
||
if (!withTime) {
|
||
const nums = String(v)
|
||
.split(/[/\-.]/)
|
||
.map((x) => parseInt(toAsciiDigits(x), 10));
|
||
if (nums.length >= 3 && !nums.some((n) => Number.isNaN(n))) {
|
||
const [gy, gm, gd] = jalaliToGregorian(nums[0], nums[1], nums[2]);
|
||
onChange(`${gy}-${String(gm).padStart(2, "0")}-${String(gd).padStart(2, "0")}`);
|
||
} else {
|
||
onChange(iso.slice(0, 10));
|
||
}
|
||
} else {
|
||
onChange(iso);
|
||
}
|
||
const j = isoToJalali(iso);
|
||
if (j) setCurJ([j[0], j[1], j[2]]);
|
||
}
|
||
};
|
||
|
||
const handleInputBlur = () => {
|
||
if (!text.trim()) {
|
||
onChange(null);
|
||
return;
|
||
}
|
||
const iso = jalaliStrToIso(text);
|
||
if (iso) {
|
||
if (!withTime) {
|
||
const nums = String(text)
|
||
.split(/[/\-.]/)
|
||
.map((x) => parseInt(toAsciiDigits(x), 10));
|
||
if (nums.length >= 3 && !nums.some((n) => Number.isNaN(n))) {
|
||
const [gy, gm, gd] = jalaliToGregorian(nums[0], nums[1], nums[2]);
|
||
const dOnly = `${gy}-${String(gm).padStart(2, "0")}-${String(gd).padStart(2, "0")}`;
|
||
onChange(dOnly);
|
||
setText(isoToJalaliStr(dOnly));
|
||
} else {
|
||
onChange(iso.slice(0, 10));
|
||
setText(isoToJalaliStr(iso));
|
||
}
|
||
} else {
|
||
onChange(iso);
|
||
const j = isoToJalaliStr(iso);
|
||
const d = new Date(iso);
|
||
if (!Number.isNaN(d.getTime())) {
|
||
const hh = String(d.getHours()).padStart(2, "0");
|
||
const mm = String(d.getMinutes()).padStart(2, "0");
|
||
setText(`${j} ${faNum(hh + ":" + mm)}`);
|
||
}
|
||
}
|
||
} else if (value) {
|
||
const j = isoToJalaliStr(value);
|
||
if (!withTime) {
|
||
setText(j);
|
||
} else {
|
||
const d = new Date(value);
|
||
if (!Number.isNaN(d.getTime())) {
|
||
const hh = String(d.getHours()).padStart(2, "0");
|
||
const mm = String(d.getMinutes()).padStart(2, "0");
|
||
setText(`${j} ${faNum(hh + ":" + mm)}`);
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div ref={ref} className={cn("relative", className)}>
|
||
<input
|
||
type="text"
|
||
disabled={disabled}
|
||
className="flex h-9 w-full rounded-md border border-line bg-panel px-3 py-1 text-sm text-ink shadow-sm transition-colors placeholder:text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 disabled:cursor-not-allowed disabled:opacity-60"
|
||
value={text}
|
||
placeholder={placeholder ?? "۱۴۰۳/۰۵/۱۲"}
|
||
autoComplete="off"
|
||
onFocus={() => !disabled && setOpen(true)}
|
||
onClick={() => !disabled && setOpen(true)}
|
||
onChange={(e) => onInputChange(e.target.value)}
|
||
onBlur={handleInputBlur}
|
||
/>
|
||
{open && !disabled && (
|
||
<div
|
||
className={cn(
|
||
"absolute inset-x-0 top-full z-30 mt-1 rounded-md border border-line bg-panel shadow-md",
|
||
// حداکثر عرض = viewport (موبایل باریک سرریز نمیشود؛ دسکتاپ بیتغییر)
|
||
"max-w-[calc(100vw-2rem)]",
|
||
withTime ? "w-80 p-3" : "w-64 p-2",
|
||
)}
|
||
>
|
||
{/* 1. Header: Month / Year Navigation */}
|
||
<div className="mb-1.5 flex items-center justify-between gap-1">
|
||
<button type="button" className="cursor-pointer rounded px-1.5 text-sm hover:bg-slate-bg" onClick={() => nav("y+1")}>
|
||
«
|
||
</button>
|
||
<button type="button" className="cursor-pointer rounded px-1.5 text-sm hover:bg-slate-bg" onClick={() => nav("m+1")}>
|
||
‹
|
||
</button>
|
||
<span className="text-[13px] font-bold">
|
||
{MONTHS[curJ[1] - 1]} {toFa(curJ[0])}
|
||
</span>
|
||
<button type="button" className="cursor-pointer rounded px-1.5 text-sm hover:bg-slate-bg" onClick={() => nav("m-1")}>
|
||
›
|
||
</button>
|
||
<button type="button" className="cursor-pointer rounded px-1.5 text-sm hover:bg-slate-bg" onClick={() => nav("y-1")}>
|
||
»
|
||
</button>
|
||
</div>
|
||
|
||
{/* 2. Day of Week Header & Days Grid */}
|
||
<div className="grid grid-cols-7 gap-0.5 text-center">
|
||
{WEEKDAYS.map((w) => (
|
||
<div key={w} className="py-0.5 text-[10px] font-bold text-muted">
|
||
{w}
|
||
</div>
|
||
))}
|
||
{grid.map((cell, i) =>
|
||
cell.d === 0 ? (
|
||
<div key={i} />
|
||
) : (
|
||
<button
|
||
key={i}
|
||
type="button"
|
||
className={cn(
|
||
"cursor-pointer rounded py-0.5 text-[12px] hover:bg-slate-bg max-md:py-1.5",
|
||
cell.isToday && !selectedIso && "bg-blue-bg font-bold text-[#1d4ed8]",
|
||
cell.isToday && selectedIso && "font-bold text-[#1d4ed8]",
|
||
cell.iso === selectedIso && "bg-accent text-white hover:bg-accent-dark",
|
||
)}
|
||
onClick={() => selectDay(curJ[0], curJ[1], cell.d)}
|
||
>
|
||
{toFa(cell.d)}
|
||
</button>
|
||
),
|
||
)}
|
||
</div>
|
||
|
||
{/* 3. Time Inputs Row (Positioned BELOW the calendar grid) — Hour on left, Minute on right */}
|
||
{withTime && (
|
||
<div className="mt-2 flex items-center justify-center gap-3 rounded-md border border-line/70 bg-slate-bg/40 py-1.5">
|
||
<div className="flex items-center gap-1.5">
|
||
<input
|
||
type="text"
|
||
className="num-input h-7 w-12 rounded-md border border-line bg-panel px-1 text-center text-[13px] tabular-nums focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 max-md:h-9 max-md:w-14"
|
||
value={minuteText}
|
||
inputMode="numeric"
|
||
onFocus={(e) => e.target.select()}
|
||
onChange={(e) => handleMinuteChange(e.target.value)}
|
||
onBlur={handleMinuteBlur}
|
||
/>
|
||
<span className="text-[11.5px] font-semibold text-muted">دقیقه</span>
|
||
</div>
|
||
<span className="font-bold text-muted">:</span>
|
||
<div className="flex items-center gap-1.5">
|
||
<input
|
||
type="text"
|
||
className="num-input h-7 w-12 rounded-md border border-line bg-panel px-1 text-center text-[13px] tabular-nums focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 max-md:h-9 max-md:w-14"
|
||
value={hourText}
|
||
inputMode="numeric"
|
||
onFocus={(e) => e.target.select()}
|
||
onChange={(e) => handleHourChange(e.target.value)}
|
||
onBlur={handleHourBlur}
|
||
/>
|
||
<span className="text-[11.5px] font-semibold text-muted">ساعت</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
|
||
{/* 4. Action Buttons Footer */}
|
||
<div className="mt-2 flex justify-between border-t border-line pt-2">
|
||
<button type="button" className="cursor-pointer rounded px-2 py-0.5 text-[11.5px] text-accent hover:bg-slate-bg" onClick={pickToday}>
|
||
امروز
|
||
</button>
|
||
<button type="button" className="cursor-pointer rounded px-2 py-0.5 text-[11.5px] text-muted hover:bg-slate-bg" onClick={clear}>
|
||
پاک کردن
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|