fix(booking): report the real time range for service-mode slots
adaptServiceSlots labelled its single session "زمانهای خالی" and set end_time to
the last slot's *start* time, so the range shown was shorter than reality. It now
uses the last slot's end_time (falling back to start + total_duration_minutes) and
labels the session with the actual range.
Shift separation was requested but is not implementable from this payload: with a
flat start_times list, a gap between shifts is indistinguishable from a gap left
by a booked appointment. The normal step is duration + buffer, so any threshold
that splits shifts either splits every slot of a long service (step above the
threshold) or invents tabs around booked appointments. A fabricated tab claims a
shift that does not exist, which is worse than one correct tab. Real grouping
belongs to the server-side endpoint task 06 adds.
Also repairs three tests that had been red since adaptSlots changed shape: they
still asserted the old { morning, evening } contract while the function returns an
array of sessions. Suite goes from 4 failures to 1 (an unrelated pre-existing
getStateInfo network timeout).
Task: clinicpro/docs/new_feture/taskes/task-00b-nobat724-service-mode/
Slot-mode contract: adaptSlots untouched
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+35
-7
@@ -14,22 +14,50 @@ export function adaptSlots(slotsResponse) {
|
||||
}));
|
||||
}
|
||||
|
||||
// حالت سرویسی: پاسخ appointment-service-slots فقط start_times دارد (همه کافی).
|
||||
// آنها را در یک session قالببندی میکنیم تا مثل حالت اسلاتی رندر شوند.
|
||||
/**
|
||||
* حالت سرویسی: پاسخ `appointment-service-slots` فقط `start_times` مسطح میدهد و مرزِ
|
||||
* شیفتها را نمیگوید. یک session ساخته میشود تا مثل حالت اسلاتی رندر شود.
|
||||
*
|
||||
* ⚠️ **چرا شیفتها تفکیک نمیشوند:** با فهرست مسطح، شکافِ بین دو شیفت از شکافِ یک نوبتِ
|
||||
* اشغالشده قابل تفکیک نیست. گام عادی `مدت + بافر` است، و دو نوبت پشتسرهم شکافی
|
||||
* میسازد که از تعطیلیِ میانِ صبح و عصر تشخیصپذیر نیست. هر آستانهای که این دو را
|
||||
* جدا کند، روی سرویسهای بلند (گام > آستانه) هر اسلات را یک تب میکند و روی نوبتهای
|
||||
* اشغال، تبِ جعلی میسازد — یعنی اطلاعات غلط، که از یک تبِ درست بدتر است.
|
||||
*
|
||||
* راه درست، گروهبندی از سمت سرور است (endpoint چندمنبعی، تسک ۰۶). تا آن زمان یک
|
||||
* session با **بازهٔ واقعی** برگردانده میشود.
|
||||
*/
|
||||
export function adaptServiceSlots(slotsResponse) {
|
||||
const starts =
|
||||
slotsResponse?.data?.start_times ?? slotsResponse?.start_times ?? [];
|
||||
const payload = slotsResponse?.data ?? slotsResponse ?? {};
|
||||
const starts = payload.start_times ?? [];
|
||||
if (!starts.length) return [];
|
||||
|
||||
const first = starts[0];
|
||||
const last = starts[starts.length - 1];
|
||||
// پایانِ آخرین نوبت، نه زمانِ **شروعِ** آن — قبلاً `start_time` گذاشته میشد و برچسب
|
||||
// بازه را کوتاهتر از واقعیت نشان میداد.
|
||||
const endTime = last.end_time ?? addMinutes(last.start_time, payload.total_duration_minutes);
|
||||
|
||||
return [
|
||||
{
|
||||
start_time: starts[0].start_time,
|
||||
end_time: starts[starts.length - 1].start_time,
|
||||
label: "زمانهای خالی",
|
||||
start_time: first.start_time,
|
||||
end_time: endTime,
|
||||
label: `${first.start_time} - ${endTime}`,
|
||||
slots: starts.map((s) => ({ ...s, is_available: true })),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** `"09:00" + 35` → `"09:35"`. فقط fallback؛ پاسخ سرور `end_time` دارد. */
|
||||
function addMinutes(time, minutes) {
|
||||
const [h, m] = String(time).split(":").map(Number);
|
||||
if (!Number.isFinite(h) || !Number.isFinite(m) || !Number.isFinite(minutes)) return time;
|
||||
const total = h * 60 + m + minutes;
|
||||
const hh = String(Math.floor(total / 60) % 24).padStart(2, "0");
|
||||
const mm = String(total % 60).padStart(2, "0");
|
||||
return `${hh}:${mm}`;
|
||||
}
|
||||
|
||||
export function hasAvailable(slots) {
|
||||
return Array.isArray(slots) && slots.some((slot) => slot.is_available);
|
||||
}
|
||||
|
||||
+91
-16
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { adaptSlots, hasAvailable } from '@/lib/appointmentSlots';
|
||||
import { adaptSlots, adaptServiceSlots, hasAvailable } from '@/lib/appointmentSlots';
|
||||
import { getAccessToken, setAccessToken, clearAccessToken } from '@/lib/tokenStore';
|
||||
import { sanitizeHtml, safeJsonParse } from '@/lib/sanitize';
|
||||
import { defineAbilitiesFor } from '@/lib/ability';
|
||||
@@ -7,28 +7,48 @@ import { setRefreshCookie, clearRefreshCookie, COOKIE_NAME } from '@/lib/refresh
|
||||
import { buildPatientUser } from '@/lib/representationAdapters';
|
||||
|
||||
describe('appointmentSlots', () => {
|
||||
// مرزِ شیفت را بکاند میدهد؛ فرانت تقسیم صبح/عصر نمیسازد. سه تست قبلی روی
|
||||
// قرارداد قدیمیِ `{ morning, evening }` نوشته شده بودند و از زمانی که خروجی به
|
||||
// آرایهٔ session تغییر کرد قرمز مانده بودند.
|
||||
const resp = {
|
||||
sessions: [
|
||||
{ slots: [
|
||||
{ start_time: '09:00', is_available: true },
|
||||
{ start_time: '11:30', is_available: false },
|
||||
{ start_time: '14:00', is_available: true },
|
||||
] },
|
||||
{
|
||||
start_time: '09:00',
|
||||
end_time: '12:00',
|
||||
slots: [
|
||||
{ start_time: '09:00', is_available: true },
|
||||
{ start_time: '11:30', is_available: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
start_time: '16:00',
|
||||
end_time: '20:00',
|
||||
slots: [{ start_time: '16:00', is_available: true }],
|
||||
},
|
||||
],
|
||||
};
|
||||
it('adaptSlots اسلاتها را بر اساس 12:00 به صبح/عصر تقسیم میکند', () => {
|
||||
const { morning, evening } = adaptSlots(resp);
|
||||
expect(morning).toHaveLength(2);
|
||||
expect(evening).toHaveLength(1);
|
||||
expect(evening[0].start_time).toBe('14:00');
|
||||
|
||||
it('adaptSlots هر شیفتِ بکاند را یک session با برچسب بازه میکند', () => {
|
||||
const sessions = adaptSlots(resp);
|
||||
expect(sessions).toHaveLength(2);
|
||||
expect(sessions[0].label).toBe('09:00 - 12:00');
|
||||
expect(sessions[1].label).toBe('16:00 - 20:00');
|
||||
expect(sessions[0].slots).toHaveLength(2);
|
||||
});
|
||||
it('ساختار data.sessions را هم میپذیرد', () => {
|
||||
const { morning } = adaptSlots({ data: resp });
|
||||
expect(morning).toHaveLength(2);
|
||||
|
||||
it('adaptSlots ساختار data.sessions را هم میپذیرد', () => {
|
||||
expect(adaptSlots({ data: resp })).toHaveLength(2);
|
||||
});
|
||||
it('ورودی خالی → آرایههای خالی', () => {
|
||||
expect(adaptSlots(null)).toEqual({ morning: [], evening: [] });
|
||||
|
||||
it('adaptSlots شیفتِ بدون اسلات را حذف میکند', () => {
|
||||
const withEmpty = { sessions: [...resp.sessions, { start_time: '21:00', end_time: '22:00', slots: [] }] };
|
||||
expect(adaptSlots(withEmpty)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('adaptSlots ورودی خالی → آرایهٔ خالی', () => {
|
||||
expect(adaptSlots(null)).toEqual([]);
|
||||
});
|
||||
|
||||
it('hasAvailable', () => {
|
||||
expect(hasAvailable([{ is_available: false }, { is_available: true }])).toBe(true);
|
||||
expect(hasAvailable([{ is_available: false }])).toBe(false);
|
||||
@@ -37,6 +57,61 @@ describe('appointmentSlots', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('adaptServiceSlots', () => {
|
||||
// پاسخ واقعی: شیفت ۰۹:۰۰–۱۲:۰۰، دو سرویس ۲۰+۱۵، بافر ۱۰ ⇒ گام ۴۵ دقیقه.
|
||||
const serviceResp = {
|
||||
data: {
|
||||
total_duration_minutes: 35,
|
||||
buffer_minutes: 10,
|
||||
start_times: [
|
||||
{ start: 1785562200, end: 1785564300, start_time: '09:00', end_time: '09:35' },
|
||||
{ start: 1785564900, end: 1785567000, start_time: '09:45', end_time: '10:20' },
|
||||
{ start: 1785567600, end: 1785569700, start_time: '10:30', end_time: '11:05' },
|
||||
{ start: 1785570300, end: 1785572400, start_time: '11:15', end_time: '11:50' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
it('یک session با بازهٔ واقعی میسازد', () => {
|
||||
const [session] = adaptServiceSlots(serviceResp);
|
||||
expect(session.start_time).toBe('09:00');
|
||||
// پایانِ آخرین نوبت، نه زمانِ شروعش.
|
||||
expect(session.end_time).toBe('11:50');
|
||||
expect(session.label).toBe('09:00 - 11:50');
|
||||
expect(session.slots).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('همهٔ زمانهای پیشنهادی available علامت میخورند', () => {
|
||||
const [session] = adaptServiceSlots(serviceResp);
|
||||
expect(session.slots.every((s) => s.is_available)).toBe(true);
|
||||
expect(hasAvailable(session.slots)).toBe(true);
|
||||
});
|
||||
|
||||
it('ساختار بدون data را هم میپذیرد', () => {
|
||||
expect(adaptServiceSlots(serviceResp.data)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('نبودِ end_time در پاسخ → از total_duration_minutes ساخته میشود', () => {
|
||||
const withoutEnd = {
|
||||
data: {
|
||||
total_duration_minutes: 35,
|
||||
start_times: [{ start_time: '09:00' }],
|
||||
},
|
||||
};
|
||||
expect(adaptServiceSlots(withoutEnd)[0].end_time).toBe('09:35');
|
||||
});
|
||||
|
||||
it('start_times خالی → آرایهٔ خالی', () => {
|
||||
expect(adaptServiceSlots({ data: { start_times: [] } })).toEqual([]);
|
||||
expect(adaptServiceSlots(null)).toEqual([]);
|
||||
});
|
||||
|
||||
it('یک اسلات تنها → بازهٔ همان اسلات', () => {
|
||||
const single = { data: { start_times: [{ start_time: '15:00', end_time: '15:30' }] } };
|
||||
expect(adaptServiceSlots(single)[0].label).toBe('15:00 - 15:30');
|
||||
});
|
||||
});
|
||||
|
||||
describe('tokenStore', () => {
|
||||
beforeEach(() => clearAccessToken());
|
||||
it('set/get/clear', () => {
|
||||
|
||||
Reference in New Issue
Block a user