Files
clinicpro/assets/admin/components/appointments/SlotPicker.tsx
T
hamed 684cf1f783 feat: implement useUrlState hook for managing URL-based state in admin pages
- 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.
2026-07-29 21:04:40 +03:30

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>
);
}