- Refactor multiple admin pages (BlogsPage, ClinicsPage, DoctorsPage, etc.) to utilize the new useUrlState hook for managing pagination, search, and filter states via URL. - Ensure that the state persists in the URL, allowing users to return to the same state when navigating back from detail pages. - Update relevant components to handle state changes appropriately and maintain clean URLs by removing default values. - Add SlotPicker component for selecting appointment slots based on availability. - Create tests for useUrlState to validate its functionality and ensure correct behavior when interacting with the URL. - Update API documentation to reflect changes in appointment creation and slot selection processes.
99 lines
4.1 KiB
TypeScript
99 lines
4.1 KiB
TypeScript
import { useQuery } from '@tanstack/react-query';
|
|
import { api } from '../../lib/api';
|
|
import type { ApiResponse } from '../../lib/api';
|
|
import { useClinicContext } from '../../hooks/useClinicContext';
|
|
|
|
interface Slot {
|
|
start: number;
|
|
end: number;
|
|
start_time: string;
|
|
end_time: string;
|
|
is_available: boolean;
|
|
}
|
|
interface Session { start_time: string; end_time: string; slots: Slot[] }
|
|
|
|
export interface PickedSlot { start: number; end: number; start_time: string }
|
|
|
|
/**
|
|
* انتخاب نوبت در حالت نوبتدهی **اسلاتی**: همان اسلاتهای واقعیِ برنامهٔ کاری پزشک
|
|
* (`appointment-slots`) که تایملاین هم نشان میدهد — نه ساعتِ دستی. اسلات پرشده
|
|
* غیرقابل انتخاب است، پس ثبت نوبت روی زمان اشغال یا خارج از برنامه ممکن نیست.
|
|
*/
|
|
export default function SlotPicker({
|
|
doctorUuid, date, value, onSelect, clinicUuidOverride,
|
|
}: {
|
|
doctorUuid: string;
|
|
date: string;
|
|
value: PickedSlot | null;
|
|
onSelect: (slot: PickedSlot | null) => void;
|
|
/** undefined = محیط جاری؛ مقدار صریح (شامل null) = محل انتخابشده خارج از context */
|
|
clinicUuidOverride?: string | null;
|
|
}) {
|
|
const contextClinicUuid = useClinicContext();
|
|
const clinicUuid = clinicUuidOverride === undefined ? contextClinicUuid : clinicUuidOverride;
|
|
|
|
const q = useQuery<ApiResponse<{ sessions: Session[]; empty_reason?: { title?: string; hint?: string } | null }>>({
|
|
queryKey: ['appt-slot-picker', doctorUuid, date, clinicUuid],
|
|
queryFn: () => api.get(
|
|
`/api/v1/appointment-slots?doctor_uuid=${doctorUuid}&date=${date}&management=1`
|
|
+ (clinicUuid ? `&clinic_uuid=${encodeURIComponent(clinicUuid)}` : ''),
|
|
),
|
|
enabled: !!doctorUuid && !!date,
|
|
});
|
|
|
|
const data = q.data?.data as any;
|
|
const sessions: Session[] = data?.sessions ?? [];
|
|
const emptyReason = data?.empty_reason;
|
|
|
|
if (q.isLoading) {
|
|
return <div style={{ fontSize: 12.5, color: 'var(--text-3)', margin: '6px 0' }}>در حال دریافت زمانهای خالی...</div>;
|
|
}
|
|
|
|
if (sessions.length === 0) {
|
|
return (
|
|
<div style={{ fontSize: 12.5, color: 'var(--danger)', margin: '6px 0' }}>
|
|
{emptyReason?.title ?? 'برای این روز برنامهٔ کاری تعریف نشده است'}
|
|
{emptyReason?.hint ? <span style={{ color: 'var(--text-3)' }}> — {emptyReason.hint}</span> : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
{sessions.map((s, i) => (
|
|
<div key={`${s.start_time}-${i}`}>
|
|
<div style={{ fontSize: 12, color: 'var(--text-3)', marginBottom: 6 }} dir="ltr">
|
|
{s.start_time} — {s.end_time}
|
|
</div>
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
|
{s.slots.map((slot) => {
|
|
const active = value?.start === slot.start;
|
|
return (
|
|
<button
|
|
key={slot.start}
|
|
type="button"
|
|
dir="ltr"
|
|
disabled={!slot.is_available}
|
|
title={slot.is_available ? undefined : 'این زمان قبلاً رزرو شده است'}
|
|
onClick={() => onSelect(active ? null : { start: slot.start, end: slot.end, start_time: slot.start_time })}
|
|
style={{
|
|
fontSize: 13, padding: '6px 12px', borderRadius: 'var(--r-sm)', fontFamily: 'inherit',
|
|
cursor: slot.is_available ? 'pointer' : 'not-allowed',
|
|
opacity: slot.is_available ? 1 : 0.45,
|
|
border: active ? '1px solid var(--primary)' : '1px solid var(--border)',
|
|
background: active ? 'var(--primary)' : 'var(--surface)',
|
|
color: active ? 'var(--on-primary)' : 'var(--text)',
|
|
textDecoration: slot.is_available ? 'none' : 'line-through',
|
|
}}
|
|
>
|
|
{slot.start_time}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|