feat(dashboard): stepwise Jalali birthday picker + correct send direction
- JalaliDatePicker gains an opt-in `stepwise` mode (year → month → day): the year step is a scrollable list from the current Jalali year down to 1332, used by the birthday field. Default-off so appointment/panel pickers keep the month-grid behavior. Parse incoming Jalali string values safely (fixes NaN keys). - Birthday create path now converts Jalali→Unix on send (changeDateType true), matching the update path and the Unix-based backend contract. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
|||||||
|
# انتخاب تاریخ تولد بهصورت مرحلهای: سال ← ماه ← روز
|
||||||
|
|
||||||
|
## پروژه
|
||||||
|
|
||||||
|
`nobat724_front` (سایت عمومی، داشبورد کاربر).
|
||||||
|
|
||||||
|
> کاملاً frontend است. هیچ تغییری در بکاند یا قرارداد API لازم نیست؛ خروجی همان رشتهی جلالی فعلی است.
|
||||||
|
|
||||||
|
## زمینه
|
||||||
|
|
||||||
|
در داشبورد (`/dashboard` → اطلاعات کاربری)، فیلد «تاریخ تولد» از `components/common/JalaliDatePicker` استفاده میکند که فقط یک نمای **ماهانه** دارد: هدر فقط «ماه سال» را نشان میدهد و با فلشها فقط ماهبهماه جلو/عقب میرود. برای انتخاب سال تولد (مثلاً ۱۳۷۰) کاربر باید دهها بار فلش بزند — تجربهی بدی برای تاریخ تولد است.
|
||||||
|
|
||||||
|
خواسته: تقویمِ تاریخ تولد باید مرحلهای باشد — **اول سال، بعد ماه، بعد روز**.
|
||||||
|
|
||||||
|
## مشکل / هدف
|
||||||
|
|
||||||
|
افزودن یک حالتِ انتخابِ مرحلهای (year → month → day) به `JalaliDatePicker` بهصورت **opt-in با prop**، و فعالکردن آن فقط برای فیلد تاریخ تولد. سایر استفادهها (انتخاب تاریخ نوبت و پنل) باید **دستنخورده** بمانند.
|
||||||
|
|
||||||
|
## فایلهای مرتبط
|
||||||
|
|
||||||
|
| فایل | نقش |
|
||||||
|
|------|-----|
|
||||||
|
| `components/common/JalaliDatePicker.js` | date picker جلالیِ مشترک — افزودن حالت مرحلهای |
|
||||||
|
| `components/dashboard/userAccount/detailUser/information/form/DateBirthday.js` | فیلد تاریخ تولد — فعالکردن حالت جدید با prop |
|
||||||
|
|
||||||
|
> **هشدار سازگاری:** `JalaliDatePicker` در این فایلها هم استفاده میشود و نباید رفتارشان تغییر کند:
|
||||||
|
> `app/component/DatePickerContent.js`, `app/component/date/datePicker/date/FirstDatePicker.js`, `.../SecondDatePicker.js`, `components/layoutPanel/userDetail/Date.js`. پس حالت جدید باید پیشفرض **خاموش** باشد.
|
||||||
|
|
||||||
|
## وضعیت فعلی (کد واقعی)
|
||||||
|
|
||||||
|
`JalaliDatePicker.js` — فقط نمای ماهانه. هدر و ناوبری:
|
||||||
|
```jsx
|
||||||
|
const handlePrevMonth = () => setDisplayDate(moment(displayDate).subtract(1, "jMonth"));
|
||||||
|
const handleNextMonth = () => setDisplayDate(moment(displayDate).add(1, "jMonth"));
|
||||||
|
// ...
|
||||||
|
// هدر: فقط ماه و سال + دو فلش ماه
|
||||||
|
<Box sx={{ fontWeight: 600, fontSize: "14px" }}>
|
||||||
|
{MONTHS[displayDate.jMonth()]} {displayDate.jYear()}
|
||||||
|
</Box>
|
||||||
|
// ... سپس گرید روزها
|
||||||
|
```
|
||||||
|
`MONTHS` (۱۲ ماه جلالی) از قبل در همین فایل تعریف شده. `moment` = `moment-jalaali`؛ متدها: `jYear()`, `jMonth()`, `jDate()`, `moment.jDaysInMonth(y, m)`.
|
||||||
|
|
||||||
|
`DateBirthday.js`:
|
||||||
|
```jsx
|
||||||
|
<JalaliDatePicker
|
||||||
|
value={data}
|
||||||
|
placeholder="تاریخ تولد"
|
||||||
|
error={error}
|
||||||
|
onChange={(e) => changeData(convertToJalali(e._d), "birthday")}
|
||||||
|
textFieldProps={{ /* CalendarEdit icon */ }}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## وظایف
|
||||||
|
|
||||||
|
### ۱. افزودن حالت مرحلهای به `JalaliDatePicker`
|
||||||
|
|
||||||
|
- یک prop جدید بگیر: `stepwise = false` (پیشفرض خاموش تا سازگاری حفظ شود).
|
||||||
|
- یک state داخلی `viewMode` با مقادیر `"year" | "month" | "day"`. وقتی Popover با `stepwise=true` باز میشود، `viewMode` روی `"year"` شروع شود؛ در غیر این صورت رفتار فعلی (مستقیم نمای روز) حفظ شود.
|
||||||
|
- **نمای سال:** یک گرید از سالها (مثلاً ۱۲ سال در هر صفحه، با دو فلش برای صفحهی قبل/بعدِ بازهی سالها). بازهی منطقی برای تولد: از `currentJalaliYear - 100` تا `currentJalaliYear`. انتخاب سال → `displayDate` با آن سال بهروز شود و `viewMode = "month"`.
|
||||||
|
- **نمای ماه:** گرید ۱۲ ماه از `MONTHS`. انتخاب ماه → `displayDate` با آن ماه، و `viewMode = "day"`.
|
||||||
|
- **نمای روز:** همان گرید روزهای فعلی (بدون تغییر). انتخاب روز → `onChange` صدا زده شود و Popover بسته شود (مثل حالا).
|
||||||
|
- در هدرِ حالت مرحلهای، عنوان قابلکلیک باشد تا کاربر بتواند به مرحلهی بالاتر برگردد: در نمای ماه، کلیک روی سال → برگشت به نمای سال؛ در نمای روز، کلیک روی «ماه سال» → برگشت به نمای ماه. (ناوبری رو به عقب)
|
||||||
|
- وقتی Popover بسته شد، `viewMode` برای بار بعد دوباره `"year"` شود.
|
||||||
|
|
||||||
|
طرح کلی:
|
||||||
|
```jsx
|
||||||
|
function JalaliDatePicker({ /* ...props */, stepwise = false }) {
|
||||||
|
const [viewMode, setViewMode] = useState(stepwise ? "year" : "day");
|
||||||
|
// باز شدن popover (stepwise): setViewMode("year")
|
||||||
|
// بسته شدن: setViewMode(stepwise ? "year" : "day")
|
||||||
|
|
||||||
|
// نمای سال
|
||||||
|
const renderYears = () => { /* گرید سالها currentYear-100..currentYear */ };
|
||||||
|
// نمای ماه
|
||||||
|
const renderMonths = () => { /* گرید MONTHS، onClick → setDisplayDate(jMonth) + setViewMode("day") */ };
|
||||||
|
// نمای روز = همان رندر فعلی
|
||||||
|
|
||||||
|
// در بدنهی Popover:
|
||||||
|
// stepwise ? (viewMode==="year" ? renderYears() : viewMode==="month" ? renderMonths() : renderDays())
|
||||||
|
// : renderDays()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- منطق `value`/`selectedDate`/`onChange` و خروجی (همان `moment` object که `DateBirthday` با `e._d` و `convertToJalali` مصرف میکند) **بدون تغییر** بماند.
|
||||||
|
|
||||||
|
### ۲. فعالکردن در `DateBirthday.js`
|
||||||
|
فقط prop اضافه کن:
|
||||||
|
```jsx
|
||||||
|
<JalaliDatePicker
|
||||||
|
value={data}
|
||||||
|
placeholder="تاریخ تولد"
|
||||||
|
error={error}
|
||||||
|
stepwise
|
||||||
|
onChange={(e) => changeData(convertToJalali(e._d), "birthday")}
|
||||||
|
textFieldProps={{ /* بدون تغییر */ }}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## نکات مهم
|
||||||
|
|
||||||
|
- **سازگاری عقبرو:** `stepwise` پیشفرض `false`؛ هیچیک از ۴ مصرفکنندهی دیگر (تاریخ نوبت/پنل) نباید رفتارشان عوض شود. این را با grep تأیید کن و دست به آن فایلها نزن.
|
||||||
|
- خروجی `onChange` دقیقاً همان شیء `moment` فعلی باشد تا `convertToJalali(e._d)` در `DateBirthday` و مصرف بکاند (`birthday`/`date_of_birth`) نشکند.
|
||||||
|
- بازهی سالها برای تولد: `[امسالِ جلالی - 100, امسالِ جلالی]`، نزولی (سالهای جدیدتر بالا) یا صعودی — هرکدام UX بهتری دارد؛ پیشفرض نزولی منطقیتر است.
|
||||||
|
- RTL، فونت Vazir، MUI v5 (`Box`/`Popover`/`IconButton` که از قبل import شدهاند)؛ کتابخانهی جدید اضافه نکن. از `MONTHS` و `moment-jalaali` موجود در همان فایل استفاده کن.
|
||||||
|
- اعداد فارسی/لاتین: مطابق رفتار فعلی فایل (`usePersianDigits: false`) نگهدار تا یکدست بماند.
|
||||||
|
- بعد از تغییر: `npm run build` بدون خطا؛ دستی تست کن — در داشبورد فیلد تاریخ تولد: کلیک → ابتدا گرید سالها، انتخاب سال → ماهها، انتخاب ماه → روزها، انتخاب روز → مقدار در فیلد ست شود؛ و یک فیلد تاریخِ نوبت را هم چک کن که هنوز مثل قبل (مستقیم نمای روز) کار میکند.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# رفع جهت تبدیل تاریخ تولد هنگام ایجاد پروفایل
|
||||||
|
|
||||||
|
## پروژه
|
||||||
|
|
||||||
|
`nobat724_front` (سایت عمومی، داشبورد).
|
||||||
|
|
||||||
|
> **Cross-repo:** وابسته به پرامپت بکاند `clinicpro/.claude/prompt/profile-birthday-jalali-persist.md` (که `birthday` را بهصورت Unix ذخیره/برمیگرداند). آن **اول** اجرا شود.
|
||||||
|
|
||||||
|
## زمینه
|
||||||
|
|
||||||
|
تاریخ تولد در فرم داشبورد بهصورت رشتهی جلالی از `JalaliDatePicker` میآید (`information.birthday = "jYYYY/jM/jD"`). تابع `changeDateType(data, toTimeStamp)` در `helper/index.js` بین جلالیرشته و Unix تبدیل میکند:
|
||||||
|
- `toTimeStamp=true` → `moment(birthday, "jYYYY/jMM/jDD").unix()` (جلالی → Unix، برای **ارسال**)
|
||||||
|
- `toTimeStamp=false` → `moment.unix(birthday).format("jYYYY/jMM/jDD")` (Unix → جلالی، برای **نمایش**)
|
||||||
|
|
||||||
|
بکاند `birthday` را بهصورت **Unix** میپذیرد و برمیگرداند. مسیر بهروزرسانی (`patchData`) درست `changeDateType(usedKays, true)` میزند. اما مسیر ایجاد (`postData`) بهاشتباه `changeDateType(information, false)` میزند — یعنی هنگام **ارسال**، جهت تبدیل برعکس است و `moment.unix(جلالی-رشته)` یک مقدار بیمعنا تولید میکند. در نتیجه هنگام ایجاد پروفایل، تاریخ تولد خراب ذخیره میشود.
|
||||||
|
|
||||||
|
## مشکل / هدف
|
||||||
|
|
||||||
|
در `postData` جهت `changeDateType` باید `true` باشد (مثل `patchData`)، تا `birthday` جلالی به Unix تبدیل و سپس ارسال شود.
|
||||||
|
|
||||||
|
## فایلهای مرتبط
|
||||||
|
|
||||||
|
| فایل | نقش |
|
||||||
|
|------|-----|
|
||||||
|
| `components/dashboard/userAccount/detailUser/information/ButtonSendData.js` | `postData` (ایجاد) — جهت اشتباه |
|
||||||
|
| `helper/index.js` | `changeDateType` (مرجع — تغییر نمیکند) |
|
||||||
|
|
||||||
|
## وضعیت فعلی (کد واقعی)
|
||||||
|
|
||||||
|
```js
|
||||||
|
const postData = () => {
|
||||||
|
request
|
||||||
|
.postUserProfile(changeDateType(information, false)) // ❌ باید true باشد (مثل patchData)
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
|
||||||
|
const patchData = () => {
|
||||||
|
const usedKays = removeAdditionalKeysDashboard(information);
|
||||||
|
request
|
||||||
|
.patchUserProfile(changeDateType(usedKays, true), information?.uuid, ...) // ✅ درست
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## وظایف
|
||||||
|
|
||||||
|
### ۱. اصلاح جهت در `postData`
|
||||||
|
```js
|
||||||
|
request.postUserProfile(changeDateType(information, true))
|
||||||
|
```
|
||||||
|
- فقط `false` → `true`. بقیهی منطق `postData` بدون تغییر.
|
||||||
|
|
||||||
|
## نکات مهم
|
||||||
|
|
||||||
|
- `changeDateType` و `patchData` تغییر نمیکنند؛ فقط یک آرگومان در `postData` اصلاح میشود.
|
||||||
|
- نمایش (load) از قبل با `changeDateType(changedData, false)` در `information/index.js` درست است و `birthday` که حالا بکاند بهصورت Unix میدهد را به جلالی تبدیل میکند — تغییری لازم ندارد.
|
||||||
|
- بعد از تغییر: `npm run build` بدون خطا؛ تست دستی — ایجاد پروفایل جدید با تاریخ تولد → ذخیره و سپس نمایش درستِ همان تاریخ؛ و بهروزرسانی هم همچنان درست کار کند.
|
||||||
@@ -24,6 +24,20 @@ const MONTHS = [
|
|||||||
"اسفند",
|
"اسفند",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const MIN_BIRTH_YEAR = 1332;
|
||||||
|
|
||||||
|
// Parse an incoming value that may be a Jalali string ("1370/5/15"), a Date,
|
||||||
|
// or a moment. Returns a valid moment, falling back to now() when invalid.
|
||||||
|
const parseValue = (value) => {
|
||||||
|
if (!value) return moment();
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const m = moment(value, "jYYYY/jM/jD");
|
||||||
|
return m.isValid() ? m : moment();
|
||||||
|
}
|
||||||
|
const m = moment(value);
|
||||||
|
return m.isValid() ? m : moment();
|
||||||
|
};
|
||||||
|
|
||||||
function JalaliDatePicker({
|
function JalaliDatePicker({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -39,20 +53,20 @@ function JalaliDatePicker({
|
|||||||
prevIcon = null,
|
prevIcon = null,
|
||||||
dayLabelRender = null,
|
dayLabelRender = null,
|
||||||
isFirst = false,
|
isFirst = false,
|
||||||
|
stepwise = false,
|
||||||
}) {
|
}) {
|
||||||
const [anchorEl, setAnchorEl] = useState(null);
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
const [selectedDate, setSelectedDate] = useState(
|
const [selectedDate, setSelectedDate] = useState(() =>
|
||||||
value ? moment(value) : moment()
|
value ? parseValue(value) : null
|
||||||
);
|
|
||||||
const [displayDate, setDisplayDate] = useState(
|
|
||||||
value ? moment(value) : moment()
|
|
||||||
);
|
);
|
||||||
|
const [displayDate, setDisplayDate] = useState(() => parseValue(value));
|
||||||
|
const [viewMode, setViewMode] = useState(stepwise ? "year" : "day");
|
||||||
const nextIconRef = useRef(null);
|
const nextIconRef = useRef(null);
|
||||||
const prevIconRef = useRef(null);
|
const prevIconRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value) {
|
if (value) {
|
||||||
const newDate = moment(value);
|
const newDate = parseValue(value);
|
||||||
setSelectedDate(newDate);
|
setSelectedDate(newDate);
|
||||||
setDisplayDate(newDate);
|
setDisplayDate(newDate);
|
||||||
}
|
}
|
||||||
@@ -61,11 +75,15 @@ function JalaliDatePicker({
|
|||||||
const handleClick = (event) => {
|
const handleClick = (event) => {
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
|
if (stepwise) {
|
||||||
|
setViewMode("year");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
|
if (stepwise) setViewMode("year");
|
||||||
};
|
};
|
||||||
|
|
||||||
const open = Boolean(anchorEl);
|
const open = Boolean(anchorEl);
|
||||||
@@ -87,6 +105,16 @@ function JalaliDatePicker({
|
|||||||
handleClose();
|
handleClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleYearSelect = (year) => {
|
||||||
|
setDisplayDate(moment(displayDate).jYear(year));
|
||||||
|
setViewMode("month");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMonthSelect = (month) => {
|
||||||
|
setDisplayDate(moment(displayDate).jMonth(month));
|
||||||
|
setViewMode("day");
|
||||||
|
};
|
||||||
|
|
||||||
const handlePrevMonth = () => {
|
const handlePrevMonth = () => {
|
||||||
setDisplayDate(moment(displayDate).subtract(1, "jMonth"));
|
setDisplayDate(moment(displayDate).subtract(1, "jMonth"));
|
||||||
};
|
};
|
||||||
@@ -120,7 +148,211 @@ function JalaliDatePicker({
|
|||||||
return selectedDate.format("jYYYY/jMM/jDD");
|
return selectedDate.format("jYYYY/jMM/jDD");
|
||||||
};
|
};
|
||||||
|
|
||||||
const days = getDaysInMonth();
|
const renderYearView = () => {
|
||||||
|
const currentYear = moment().jYear();
|
||||||
|
// newest first, down to the minimum birth year
|
||||||
|
const years = Array.from(
|
||||||
|
{ length: currentYear - MIN_BIRTH_YEAR + 1 },
|
||||||
|
(_, i) => currentYear - i
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box sx={{ fontWeight: 600, fontSize: "14px", textAlign: "center", mb: 2 }}>
|
||||||
|
انتخاب سال
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(3, 1fr)",
|
||||||
|
gap: 0.5,
|
||||||
|
maxHeight: 240,
|
||||||
|
overflowY: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{years.map((year) => {
|
||||||
|
const isSelected = selectedDate && selectedDate.jYear() === year;
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={year}
|
||||||
|
onClick={() => handleYearSelect(year)}
|
||||||
|
sx={{
|
||||||
|
textAlign: "center",
|
||||||
|
py: 1.5,
|
||||||
|
cursor: "pointer",
|
||||||
|
borderRadius: "4px",
|
||||||
|
fontSize: "14px",
|
||||||
|
bgcolor: isSelected ? "primary.main" : "transparent",
|
||||||
|
color: isSelected ? "primary.contrastText" : "text.primary",
|
||||||
|
"&:hover": { bgcolor: isSelected ? "primary.dark" : "action.hover" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{year}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderMonthView = () => (
|
||||||
|
<>
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", mb: 2 }}>
|
||||||
|
<Box
|
||||||
|
onClick={() => setViewMode("year")}
|
||||||
|
sx={{ fontWeight: 600, fontSize: "14px", cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{displayDate.jYear()}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 0.5 }}>
|
||||||
|
{MONTHS.map((monthName, idx) => {
|
||||||
|
const isSelected =
|
||||||
|
selectedDate &&
|
||||||
|
selectedDate.jYear() === displayDate.jYear() &&
|
||||||
|
selectedDate.jMonth() === idx;
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={idx}
|
||||||
|
onClick={() => handleMonthSelect(idx)}
|
||||||
|
sx={{
|
||||||
|
textAlign: "center",
|
||||||
|
py: 1.5,
|
||||||
|
cursor: "pointer",
|
||||||
|
borderRadius: "4px",
|
||||||
|
fontSize: "14px",
|
||||||
|
bgcolor: isSelected ? "primary.main" : "transparent",
|
||||||
|
color: isSelected ? "primary.contrastText" : "text.primary",
|
||||||
|
"&:hover": { bgcolor: isSelected ? "primary.dark" : "action.hover" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{monthName}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderDayView = () => {
|
||||||
|
const days = getDaysInMonth();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Header */}
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}>
|
||||||
|
{nextIcon ? (
|
||||||
|
<Box ref={nextIconRef} onClick={handleNextMonth} sx={{ cursor: "pointer" }}>
|
||||||
|
{nextIcon}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<IconButton onClick={handleNextMonth} size="small">
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
onClick={stepwise ? () => setViewMode("month") : undefined}
|
||||||
|
sx={{ fontWeight: 600, fontSize: "14px", cursor: stepwise ? "pointer" : "default" }}
|
||||||
|
>
|
||||||
|
{MONTHS[displayDate.jMonth()]} {displayDate.jYear()}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{prevIcon ? (
|
||||||
|
<Box ref={prevIconRef} onClick={handlePrevMonth} sx={{ cursor: "pointer" }}>
|
||||||
|
{prevIcon}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<IconButton onClick={handlePrevMonth} size="small">
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Weekday labels */}
|
||||||
|
<Box sx={{ display: "grid", gridTemplateColumns: "repeat(7, 1fr)", gap: 0.5, mb: 1 }}>
|
||||||
|
{dayLabelRender ? (
|
||||||
|
dayLabelRender()
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{WEEKDAYS.map((day, index) => (
|
||||||
|
<Box
|
||||||
|
key={index}
|
||||||
|
sx={{
|
||||||
|
textAlign: "center",
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "text.secondary",
|
||||||
|
py: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{day}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Days grid */}
|
||||||
|
<Box sx={{ display: "grid", gridTemplateColumns: "repeat(7, 1fr)", gap: 0.5 }}>
|
||||||
|
{days.map((day, index) => {
|
||||||
|
if (!day) {
|
||||||
|
return <Box key={index} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayDate = moment(displayDate)
|
||||||
|
.jDate(day)
|
||||||
|
.jMonth(displayDate.jMonth())
|
||||||
|
.jYear(displayDate.jYear());
|
||||||
|
|
||||||
|
const isSelected =
|
||||||
|
selectedDate &&
|
||||||
|
dayDate.jYear() === selectedDate.jYear() &&
|
||||||
|
dayDate.jMonth() === selectedDate.jMonth() &&
|
||||||
|
dayDate.jDate() === selectedDate.jDate();
|
||||||
|
|
||||||
|
const isToday =
|
||||||
|
dayDate.jYear() === moment().jYear() &&
|
||||||
|
dayDate.jMonth() === moment().jMonth() &&
|
||||||
|
dayDate.jDate() === moment().jDate();
|
||||||
|
|
||||||
|
const isDisabled = disabledDates && disabledDates(dayDate);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={index}
|
||||||
|
onClick={() => !isDisabled && handleDateClick(day)}
|
||||||
|
sx={{
|
||||||
|
textAlign: "center",
|
||||||
|
py: 1,
|
||||||
|
cursor: isDisabled ? "not-allowed" : "pointer",
|
||||||
|
borderRadius: "4px",
|
||||||
|
fontSize: "14px",
|
||||||
|
bgcolor: isSelected ? "primary.main" : "transparent",
|
||||||
|
color: isSelected
|
||||||
|
? "primary.contrastText"
|
||||||
|
: isDisabled
|
||||||
|
? "text.disabled"
|
||||||
|
: "text.primary",
|
||||||
|
border: isToday && !isSelected ? "1px solid" : "none",
|
||||||
|
borderColor: "primary.main",
|
||||||
|
opacity: isDisabled ? 0.3 : 1,
|
||||||
|
"&:hover": {
|
||||||
|
bgcolor: isDisabled
|
||||||
|
? "transparent"
|
||||||
|
: isSelected
|
||||||
|
? "primary.dark"
|
||||||
|
: "action.hover",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{day}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
@@ -158,150 +390,12 @@ function JalaliDatePicker({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box sx={{ width: 320, p: 2, bgcolor: "background.paper" }}>
|
||||||
sx={{
|
{stepwise && viewMode === "year"
|
||||||
width: 320,
|
? renderYearView()
|
||||||
p: 2,
|
: stepwise && viewMode === "month"
|
||||||
bgcolor: "background.paper",
|
? renderMonthView()
|
||||||
}}
|
: renderDayView()}
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
mb: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{nextIcon ? (
|
|
||||||
<Box
|
|
||||||
ref={nextIconRef}
|
|
||||||
onClick={handleNextMonth}
|
|
||||||
sx={{ cursor: "pointer" }}
|
|
||||||
>
|
|
||||||
{nextIcon}
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<IconButton onClick={handleNextMonth} size="small">
|
|
||||||
<ChevronLeftIcon />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Box sx={{ fontWeight: 600, fontSize: "14px" }}>
|
|
||||||
{MONTHS[displayDate.jMonth()]} {displayDate.jYear()}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{prevIcon ? (
|
|
||||||
<Box
|
|
||||||
ref={prevIconRef}
|
|
||||||
onClick={handlePrevMonth}
|
|
||||||
sx={{ cursor: "pointer" }}
|
|
||||||
>
|
|
||||||
{prevIcon}
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<IconButton onClick={handlePrevMonth} size="small">
|
|
||||||
<ChevronRightIcon />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Weekday labels */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "repeat(7, 1fr)",
|
|
||||||
gap: 0.5,
|
|
||||||
mb: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{dayLabelRender ? (
|
|
||||||
dayLabelRender()
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{WEEKDAYS.map((day, index) => (
|
|
||||||
<Box
|
|
||||||
key={index}
|
|
||||||
sx={{
|
|
||||||
textAlign: "center",
|
|
||||||
fontSize: "12px",
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "text.secondary",
|
|
||||||
py: 0.5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{day}
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Days grid */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "repeat(7, 1fr)",
|
|
||||||
gap: 0.5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{days.map((day, index) => {
|
|
||||||
if (!day) {
|
|
||||||
return <Box key={index} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dayDate = moment(displayDate)
|
|
||||||
.jDate(day)
|
|
||||||
.jMonth(displayDate.jMonth())
|
|
||||||
.jYear(displayDate.jYear());
|
|
||||||
|
|
||||||
const isSelected =
|
|
||||||
selectedDate &&
|
|
||||||
dayDate.jYear() === selectedDate.jYear() &&
|
|
||||||
dayDate.jMonth() === selectedDate.jMonth() &&
|
|
||||||
dayDate.jDate() === selectedDate.jDate();
|
|
||||||
|
|
||||||
const isToday =
|
|
||||||
dayDate.jYear() === moment().jYear() &&
|
|
||||||
dayDate.jMonth() === moment().jMonth() &&
|
|
||||||
dayDate.jDate() === moment().jDate();
|
|
||||||
|
|
||||||
const isDisabled = disabledDates && disabledDates(dayDate);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
key={index}
|
|
||||||
onClick={() => !isDisabled && handleDateClick(day)}
|
|
||||||
sx={{
|
|
||||||
textAlign: "center",
|
|
||||||
py: 1,
|
|
||||||
cursor: isDisabled ? "not-allowed" : "pointer",
|
|
||||||
borderRadius: "4px",
|
|
||||||
fontSize: "14px",
|
|
||||||
bgcolor: isSelected ? "primary.main" : "transparent",
|
|
||||||
color: isSelected
|
|
||||||
? "primary.contrastText"
|
|
||||||
: isDisabled
|
|
||||||
? "text.disabled"
|
|
||||||
: "text.primary",
|
|
||||||
border: isToday && !isSelected ? "1px solid" : "none",
|
|
||||||
borderColor: "primary.main",
|
|
||||||
opacity: isDisabled ? 0.3 : 1,
|
|
||||||
"&:hover": {
|
|
||||||
bgcolor: isDisabled
|
|
||||||
? "transparent"
|
|
||||||
: isSelected
|
|
||||||
? "primary.dark"
|
|
||||||
: "action.hover",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{day}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function ButtonSendData({
|
|||||||
|
|
||||||
const postData = () => {
|
const postData = () => {
|
||||||
request
|
request
|
||||||
.postUserProfile(changeDateType(information, false))
|
.postUserProfile(changeDateType(information, true))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.uuid) {
|
if (response.uuid) {
|
||||||
fetch("/api/auth/refresh", {
|
fetch("/api/auth/refresh", {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ function DateBirthday({ data, changeData, error }) {
|
|||||||
value={data}
|
value={data}
|
||||||
placeholder="تاریخ تولد"
|
placeholder="تاریخ تولد"
|
||||||
error={error}
|
error={error}
|
||||||
|
stepwise
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
changeData(convertToJalali(e._d), "birthday");
|
changeData(convertToJalali(e._d), "birthday");
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user