feat(appointments): profile-aware doctor tabs + toolbar to match tauri turns
- Multi-doctor clinic profile (clinic/admin) shows the doctor tabs for multi-doctor management; clinic auto-selects the first doctor so the timeline loads immediately (as in the reference). Admin keeps the «همه» tab. - Independent doctor profile (doctor role, incl. clinic-scoped guest) shows no tabs and only its own schedule — no clinic/doctor selection. - Toolbar reordered to match the reference: date + service select («سرویس مورد نظر را انتخاب کنید...») + view toggle on the right; filter + «افزودن نوبت» on the left. The in-toolbar doctor picker is removed (doctor selection is via tabs); the personnel select is replaced by a service filter bound to the existing itemUuid filter (client-side, no new endpoint). Tests: added clinic-profile tests (tabs shown, no «همه», first doctor auto-selected) and independent-doctor test (no tabs, service filter shown). No API endpoints changed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,6 @@ import Pagination from '../components/ui/Pagination';
|
||||
import AppointmentFiltersModal, { applyAppointmentFilters, EMPTY_FILTERS } from '../components/AppointmentFiltersModal';
|
||||
import type { AppointmentFilters } from '../components/AppointmentFiltersModal';
|
||||
import PersianCalendar from '../components/ui/PersianCalendar';
|
||||
import SearchableSelect from '../components/ui/SearchableSelect';
|
||||
// اجزای طرح نوبتهای tauri
|
||||
import TurnsStatInfo from '../components/appointments/TurnsStatInfo';
|
||||
import TurnsViewToggle from '../components/appointments/TurnsViewToggle';
|
||||
@@ -264,8 +263,27 @@ export default function AppointmentsPage() {
|
||||
return Array.from(map.entries()).map(([uuid, name]) => ({ uuid, name }));
|
||||
}, [appointments, clinicDoctorsList]);
|
||||
|
||||
const showDoctorTabs = (isClinic || isAdmin) && doctors.length >= 2;
|
||||
const showDoctorCol = isAdmin || (isClinic && !selectedDoctorUuid);
|
||||
// سرویسهای موجود در نوبتهای امروز (برای فیلتر «سرویس مورد نظر...»).
|
||||
const serviceOptions = React.useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
appointments.forEach(a => {
|
||||
if (a.service_item?.uuid && a.service_item?.name) map.set(a.service_item.uuid, a.service_item.name);
|
||||
});
|
||||
return Array.from(map.entries()).map(([uuid, name]) => ({ uuid, name }));
|
||||
}, [appointments]);
|
||||
|
||||
// نوع پروفایل: کلینیک چندپزشکه (تب دکترها + مدیریت چند پزشک) در برابر پزشک مستقل.
|
||||
// نقش clinic/admin = چندپزشکه؛ نقش doctor (حتی مهمانِ کلینیک) = مستقل، فقط برنامهٔ خودش.
|
||||
const isMultiDoctorClinic = isClinic || isAdmin;
|
||||
const showDoctorTabs = isMultiDoctorClinic && doctors.length >= 1;
|
||||
const showDoctorCol = isAdmin && !selectedDoctorUuid;
|
||||
|
||||
// در کلینیک، اولین دکتر بهصورت پیشفرض انتخاب میشود تا زمانبندی مثل طرح پر باشد.
|
||||
useEffect(() => {
|
||||
if (isClinic && !selectedDoctorUuid && doctors.length > 0) {
|
||||
setSelectedDoctorUuid(doctors[0].uuid);
|
||||
}
|
||||
}, [isClinic, selectedDoctorUuid, doctors]);
|
||||
|
||||
// ── Slots query (timeline)
|
||||
const slotsQueryKey = ['appt-slots', selectedDoctorUuid, selectedDate];
|
||||
@@ -342,23 +360,20 @@ export default function AppointmentsPage() {
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', marginBottom: 16,
|
||||
}}>
|
||||
{/* افزودن نوبت → صفحهٔ کامل */}
|
||||
{!isRepresentation && (
|
||||
<button
|
||||
className="btn primary sm"
|
||||
onClick={() => {
|
||||
const q = selectedDoctorUuid ? `?doctor=${selectedDoctorUuid}&date=${selectedDate}` : `?date=${selectedDate}`;
|
||||
navigate(`/admin/appointments/new${q}`);
|
||||
}}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 5 }}
|
||||
>
|
||||
<PlusIcon style={{ width: 15, height: 15 }} />
|
||||
افزودن نوبت
|
||||
</button>
|
||||
)}
|
||||
{/* سمت راست: تاریخ + سرویس + سوییچ نما (مطابق طرح) */}
|
||||
<DateNavigator date={selectedDate} onChange={setSelectedDate} />
|
||||
|
||||
<StaffFilterSelect value={filters.staffUuid} onChange={(v) => setFilters(f => ({ ...f, staffUuid: v }))} />
|
||||
<ServiceFilterSelect
|
||||
value={filters.itemUuid}
|
||||
options={serviceOptions}
|
||||
onChange={(v) => setFilters(f => ({ ...f, itemUuid: v }))}
|
||||
/>
|
||||
|
||||
<TurnsViewToggle viewMode={viewMode} onChange={setViewMode} />
|
||||
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
{/* سمت چپ: فیلتر + افزودن نوبت */}
|
||||
<button
|
||||
aria-label="فیلترها"
|
||||
className="btn sm"
|
||||
@@ -372,25 +387,19 @@ export default function AppointmentsPage() {
|
||||
<AdjustmentsHorizontalIcon style={{ width: 16 }} />
|
||||
</button>
|
||||
|
||||
<TurnsViewToggle viewMode={viewMode} onChange={setViewMode} />
|
||||
|
||||
{/* انتخاب پزشک (admin / clinic) */}
|
||||
{!isDoctor && (
|
||||
<div style={{ minWidth: 200 }}>
|
||||
<SearchableSelect
|
||||
options={doctors.map(d => ({ value: d.uuid, label: d.name }))}
|
||||
value={selectedDoctorUuid || null}
|
||||
onChange={(v) => setSelectedDoctorUuid(v ? String(v) : '')}
|
||||
placeholder="انتخاب پزشک..."
|
||||
isClearable
|
||||
height={36}
|
||||
/>
|
||||
</div>
|
||||
{!isRepresentation && (
|
||||
<button
|
||||
className="btn primary sm"
|
||||
onClick={() => {
|
||||
const q = selectedDoctorUuid ? `?doctor=${selectedDoctorUuid}&date=${selectedDate}` : `?date=${selectedDate}`;
|
||||
navigate(`/admin/appointments/new${q}`);
|
||||
}}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 5 }}
|
||||
>
|
||||
<PlusIcon style={{ width: 15, height: 15 }} />
|
||||
افزودن نوبت
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
<DateNavigator date={selectedDate} onChange={setSelectedDate} />
|
||||
</div>
|
||||
|
||||
{/* کارت اصلی — تب دکترها (هدر) + محتوا */}
|
||||
@@ -399,7 +408,7 @@ export default function AppointmentsPage() {
|
||||
borderRadius: 'var(--r)', overflow: 'hidden',
|
||||
}}>
|
||||
{showDoctorTabs && (
|
||||
<DoctorTabs doctors={doctors} selected={selectedDoctorUuid} onSelect={setSelectedDoctorUuid} />
|
||||
<DoctorTabs doctors={doctors} selected={selectedDoctorUuid} onSelect={setSelectedDoctorUuid} showAll={isAdmin} />
|
||||
)}
|
||||
<div style={{ padding: 16 }}>
|
||||
{viewMode === 'table' ? (
|
||||
@@ -458,16 +467,15 @@ export default function AppointmentsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
/** فیلتر پرسنل نوار ابزار — ردیفهای بارگذاریشدهٔ روز را بر اساس پرسنل فیلتر میکند. */
|
||||
function StaffFilterSelect({ value, onChange }: { value: string; onChange: (v: string) => void }) {
|
||||
const staffQ = useQuery<ApiResponse<{ uuid: string; full_name: string }[]>>({
|
||||
queryKey: ['staff-list'], queryFn: () => api.get('/api/v1/staff'),
|
||||
});
|
||||
/** فیلتر سرویس نوار ابزار — ردیفهای بارگذاریشدهٔ روز را بر اساس سرویس فیلتر میکند. */
|
||||
function ServiceFilterSelect({ value, options, onChange }: {
|
||||
value: string; options: { uuid: string; name: string }[]; onChange: (v: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<select aria-label="پرسنل" value={value} onChange={e => onChange(e.target.value)}
|
||||
style={{ height: 36, borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', fontSize: 12.5, fontFamily: 'inherit', padding: '0 10px', minWidth: 170 }}>
|
||||
<option value="">پرسنل را انتخاب کنید...</option>
|
||||
{(staffQ.data?.data ?? []).map(s => <option key={s.uuid} value={s.uuid}>{s.full_name}</option>)}
|
||||
<select aria-label="سرویس" value={value} onChange={e => onChange(e.target.value)}
|
||||
style={{ height: 44, borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', fontSize: 13, fontFamily: 'inherit', padding: '0 12px', minWidth: 280 }}>
|
||||
<option value="">سرویس مورد نظر را انتخاب کنید...</option>
|
||||
{options.map(s => <option key={s.uuid} value={s.uuid}>{s.name}</option>)}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user