fix(appointment): client guard for slots that pass while page is open
The backend already returns is_available:false for past slots, but a slot can lapse after the list is loaded. Disable any slot whose start (unix seconds) is before now in the time grid, so a just-passed slot can't be clicked. The grid disabling makes a SendAppo-level guard redundant — a past slot can no longer be selected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
|||||||
|
# تأیید و سختسازی غیرفعالبودن اسلاتهای گذشته در صفحهی نوبت
|
||||||
|
|
||||||
|
## پروژه
|
||||||
|
|
||||||
|
`nobat724_front` — سایت عمومی. **بعد از پرامپت backend اجرا شود.**
|
||||||
|
|
||||||
|
> **Cross-repo:** وابسته به اصلاح `is_available` در backend:
|
||||||
|
> `clinicpro/.claude/prompt/mark-past-slots-unavailable.md`
|
||||||
|
> بعد از آن، اسلاتهای گذشتهی روز جاری از API با `is_available: false` میآیند و UI خودکار آنها را خاکستری میکند. این پرامپت آن را تأیید و یک گارد سبک سمت کلاینت اضافه میکند.
|
||||||
|
|
||||||
|
## زمینه
|
||||||
|
|
||||||
|
گرید ساعت (`List.js`) از قبل دکمه را با `disabled={!item.is_available}` غیرفعال و خاکستری میکند. پس وقتی backend اسلات گذشته را `is_available: false` بدهد، **بدون تغییر فرانت** خاکستری میشود. تنها ریسک باقیمانده: کاربر صفحه را ساعت ۹:۵۹ باز کرده، اسلات ۱۰:۰۰ آن لحظه `is_available: true` بوده و در state مانده؛ ساعت ۱۰:۰۱ همان آبجکت قدیمی هنوز کلیکپذیر است چون داده دوباره fetch نشده. backend موقع ثبت رد میکند (۴۲۲)، ولی بهتر است UI هم همان لحظه جلوی کلیک را بگیرد.
|
||||||
|
|
||||||
|
## مشکل / هدف
|
||||||
|
|
||||||
|
۱. تأیید کن گرید ساعت `is_available` را درست رعایت میکند (انتظار: نیازی به تغییر نیست).
|
||||||
|
۲. یک گارد سبک سمت کلاینت اضافه کن: اسلاتی که `start` (Unix ثانیه) آن از «اکنون» گذشته، حتی اگر `is_available: true` آمده باشد، در UI غیرفعال شود.
|
||||||
|
|
||||||
|
## فایلهای مرتبط
|
||||||
|
|
||||||
|
| فایل | نقش |
|
||||||
|
|------|-----|
|
||||||
|
| `app/component/date/dateTime/hours/List.js` | گرید ساعت؛ `disabled={!item.is_available}` |
|
||||||
|
| `lib/appointmentSlots.js` | adapter اسلاتها (`sessions[]` → morning/evening) — اسلاتها `start`/`is_available` دارند |
|
||||||
|
| `app/component/date/dateTime/SendAppo.js` | دکمهی «تایید نوبت»؛ `disabled={!hour}` |
|
||||||
|
|
||||||
|
## وضعیت فعلی (کد واقعی)
|
||||||
|
|
||||||
|
### `hours/List.js`
|
||||||
|
|
||||||
|
```js
|
||||||
|
<Button
|
||||||
|
onClick={() => setHour(item)}
|
||||||
|
disabled={!item.is_available}
|
||||||
|
...
|
||||||
|
>
|
||||||
|
...
|
||||||
|
<p className={`... ${item.is_available ? "text-[#525252]" : "text-[#D7D7D7]"} ...`}>
|
||||||
|
{item.start_time}
|
||||||
|
</p>
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### آبجکت اسلات (از `adaptSlots` / API)
|
||||||
|
|
||||||
|
```js
|
||||||
|
{ start: 1781933400, end: 1781934600, start_time: "09:00", end_time: "09:20", location_id: 2581, is_available: true }
|
||||||
|
```
|
||||||
|
|
||||||
|
## وظایف
|
||||||
|
|
||||||
|
### ۱. گارد زمان گذشته در گرید ساعت
|
||||||
|
|
||||||
|
در `List.js` یک محاسبهی کوچک اضافه کن که اسلات گذشته را غیرقابلانتخاب کند (مبنا: `item.start` بر حسب ثانیه):
|
||||||
|
|
||||||
|
```js
|
||||||
|
const isPast = (item) => item?.start && item.start * 1000 < Date.now();
|
||||||
|
const isSelectable = (item) => item.is_available && !isPast(item);
|
||||||
|
```
|
||||||
|
|
||||||
|
سپس در دکمه `disabled={!isSelectable(item)}` و در کلاس متن `isSelectable(item) ? ... : ...` را جایگزین `item.is_available` کن.
|
||||||
|
|
||||||
|
> `start` ثانیه است؛ `Date.now()` میلیثانیه — پس `start * 1000`.
|
||||||
|
|
||||||
|
### ۲. (اختیاری) عدم انتخاب اسلات گذشته در SendAppo
|
||||||
|
|
||||||
|
اگر منطقی بود، در `SendAppo` هم قبل از `setSelectedSlot(hour)` مطمئن شو `hour.start * 1000 >= Date.now()`؛ در غیر این صورت اجازه نده مرحله جلو برود. (اگر گارد گرید کافی است و کاربر اصلاً نمیتواند اسلات گذشته را انتخاب کند، این مرحله را رد کن و در گزارش ذکر کن.)
|
||||||
|
|
||||||
|
### ۳. تأیید رفتار end-to-end
|
||||||
|
|
||||||
|
- با `date=<امروز>` و یک ساعت که چند اسلات اول روز گذشتهاند، تأیید کن اسلاتهای گذشته خاکستری و غیرقابلکلیکاند و اسلاتهای آینده فعال.
|
||||||
|
|
||||||
|
## نکات مهم
|
||||||
|
|
||||||
|
- **بدون تایمر/رندر مکرر اضافه نکن** مگر لازم باشد؛ یک محاسبه در زمان رندر کافی است (هر بار state تغییر کند یا کامپوننت رندر شود ارزیابی میشود). نیازی به `setInterval` برای بهروزرسانی هر ثانیه نیست مگر کاربر بخواهد.
|
||||||
|
- **منبع اصلی همچنان backend است:** این گارد فقط لبهی «اسلاتی که همین حالا گذشت» را میگیرد. اگر backend درست `is_available:false` بدهد، این گارد در عمل کماثر ولی بیضرر و درست است.
|
||||||
|
- **تایمزون:** `start` از API بر حسب Unix ثانیه (UTC مبنا) است؛ `Date.now()` هم Unix میلیثانیه است — مقایسه درست است بدون تبدیل تایمزون.
|
||||||
|
- RTL/Jalali/multi-domain را خراب نکن؛ منطق `morning`/`evening` و adapter دستنخورده بماند.
|
||||||
|
- تست: `npm run build` و سپس بررسی دستی روی صفحهی نوبت پزشک تست. سپس commit با پیام توصیفی.
|
||||||
@@ -4,6 +4,8 @@ import React from "react";
|
|||||||
function List({ appo, hour, setHour, value }) {
|
function List({ appo, hour, setHour, value }) {
|
||||||
const list = appo && appo[value ? "evening" : "morning"];
|
const list = appo && appo[value ? "evening" : "morning"];
|
||||||
const checkHour = (item) => JSON.stringify(item) === JSON.stringify(hour);
|
const checkHour = (item) => JSON.stringify(item) === JSON.stringify(hour);
|
||||||
|
const isSelectable = (item) =>
|
||||||
|
item.is_available && !(item?.start && item.start * 1000 < Date.now());
|
||||||
|
|
||||||
if (list && list.length) {
|
if (list && list.length) {
|
||||||
return (
|
return (
|
||||||
@@ -12,7 +14,7 @@ function List({ appo, hour, setHour, value }) {
|
|||||||
<li key={idx}>
|
<li key={idx}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setHour(item)}
|
onClick={() => setHour(item)}
|
||||||
disabled={!item.is_available}
|
disabled={!isSelectable(item)}
|
||||||
className={`
|
className={`
|
||||||
!w-full !rounded-[8px] !bg-[#EFEFEF] !flex !justify-center !items-center !py-[10px] !px-[14px] !h-[40px] md:!h-[42px] lg:!h-[44px]
|
!w-full !rounded-[8px] !bg-[#EFEFEF] !flex !justify-center !items-center !py-[10px] !px-[14px] !h-[40px] md:!h-[42px] lg:!h-[44px]
|
||||||
${checkHour(item) ? "!bg-[#F79463] " : ""}
|
${checkHour(item) ? "!bg-[#F79463] " : ""}
|
||||||
@@ -21,7 +23,7 @@ function List({ appo, hour, setHour, value }) {
|
|||||||
<p
|
<p
|
||||||
className={`
|
className={`
|
||||||
text-[16px] font-semibold text-[#525252]
|
text-[16px] font-semibold text-[#525252]
|
||||||
${item.is_available ? "text-[#525252]" : "text-[#D7D7D7]"}
|
${isSelectable(item) ? "text-[#525252]" : "text-[#D7D7D7]"}
|
||||||
${checkHour(item) ? "!text-[#FAFAFA] " : ""}
|
${checkHour(item) ? "!text-[#FAFAFA] " : ""}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user