- Center the whole page in a max-width container; move the toolbar above the card and make the doctor tabs the card header (as in the tauri turns design). - Rebuild the timeline card to match the reference: inner start/end ring-dot time markers, patient name / phone / «سرویس» lines, and status pill + عملیات on the card's left; outer marker rail with time on the right. - Timeline rows are width-capped and centered on the page. - Empty slots render the light-blue «افزودن نوبت +» card with a «نوبت جدید» pill. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
474 lines
21 KiB
TypeScript
474 lines
21 KiB
TypeScript
import React, { useEffect, useRef, useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import {
|
|
PlusIcon, ChevronRightIcon, ChevronLeftIcon, CalendarDaysIcon,
|
|
AdjustmentsHorizontalIcon,
|
|
} from '@heroicons/react/24/outline';
|
|
import { toast } from 'sonner';
|
|
import { api } from '../lib/api';
|
|
import type { PaginatedResponse, ApiResponse } from '../lib/api';
|
|
import type { Appointment } from '../types';
|
|
import { formatDate, toGregorianDate } from '../lib/utils';
|
|
import { useAuthStore } from '../stores/authStore';
|
|
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';
|
|
import type { TurnsViewMode } from '../components/appointments/TurnsViewToggle';
|
|
import DoctorTabs from '../components/appointments/DoctorTabs';
|
|
import TurnsTimeline from '../components/appointments/TurnsTimeline';
|
|
import TurnsTable from '../components/appointments/TurnsTable';
|
|
import { CANCELLED_STATUSES } from '../components/appointments/turnStatus';
|
|
import type { TimelineSlot } from '../components/appointments/types';
|
|
|
|
const EMPTY_ARR: Appointment[] = [];
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Date Navigator (طرح tauri — ناوبری روزانه)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
const WEEK_DAYS_FA = ['یکشنبه', 'دوشنبه', 'سهشنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه', 'شنبه'];
|
|
|
|
function getPersianWeekDay(gregorianDate: string): string {
|
|
return WEEK_DAYS_FA[new Date(gregorianDate + 'T12:00:00').getDay()];
|
|
}
|
|
|
|
const navBtnSx: React.CSSProperties = {
|
|
background: 'var(--surface)', border: '1px solid var(--border)',
|
|
borderRadius: 'var(--r-sm)', height: 36, width: 36,
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
cursor: 'pointer', color: 'var(--text-2)',
|
|
};
|
|
|
|
function DateNavigator({ date, onChange }: { date: string; onChange: (d: string) => void }) {
|
|
const [showCal, setShowCal] = useState(false);
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
const weekDay = getPersianWeekDay(date);
|
|
const isFriday = new Date(date + 'T12:00:00').getDay() === 5;
|
|
|
|
function addDays(n: number) {
|
|
const d = new Date(date + 'T12:00:00');
|
|
d.setDate(d.getDate() + n);
|
|
onChange(toGregorianDate(d));
|
|
}
|
|
|
|
return (
|
|
<div ref={ref} style={{ display: 'flex', alignItems: 'center', gap: 2, position: 'relative' }}>
|
|
<button className="btn sm" style={navBtnSx} onClick={() => addDays(1)}>
|
|
<ChevronRightIcon style={{ width: 15, height: 15 }} />
|
|
</button>
|
|
<div style={{
|
|
padding: '0 14px', height: 44,
|
|
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
|
|
background: 'var(--surface)', border: '1px solid var(--border)',
|
|
borderRadius: 'var(--r-sm)', minWidth: 130, gap: 1,
|
|
}}>
|
|
<span style={{ fontSize: 13, fontWeight: 700, lineHeight: 1.2, color: isFriday ? '#ef4444' : 'var(--text)' }}>
|
|
{weekDay}
|
|
</span>
|
|
<span style={{ fontSize: 11, color: 'var(--text-2)', lineHeight: 1.2 }}>
|
|
{formatDate(date)}
|
|
</span>
|
|
</div>
|
|
<button className="btn sm" style={navBtnSx} onClick={() => addDays(-1)}>
|
|
<ChevronLeftIcon style={{ width: 15, height: 15 }} />
|
|
</button>
|
|
<button className="btn sm" style={navBtnSx} onClick={() => setShowCal(c => !c)}>
|
|
<CalendarDaysIcon style={{ width: 15, height: 15 }} />
|
|
</button>
|
|
{showCal && (
|
|
<PersianCalendar value={date} onChange={v => { onChange(v); setShowCal(false); }} onClose={() => setShowCal(false)} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Quick booking modal (کلیک روی اسلات خالیِ زمانبندی)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
interface BookingSlot { start: number; end: number; start_time: string; end_time: string; doctor_uuid: string; doctor_name: string; }
|
|
|
|
function NewAppointmentModal({
|
|
slot, onClose, onSuccess,
|
|
}: { slot: BookingSlot; onClose: () => void; onSuccess: () => void }) {
|
|
const [mobile, setMobile] = useState('');
|
|
const [patientName, setPatientName] = useState('');
|
|
|
|
const role = useAuthStore(s => s.primaryRole);
|
|
const createEndpoint = role === 'admin' ? '/api/v1/admin/appointment' : '/api/v1/my/appointment';
|
|
|
|
const isValid = mobile.length >= 10 && patientName.trim().length >= 2;
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: () => api.post(createEndpoint, {
|
|
doctor_uuid: slot.doctor_uuid,
|
|
slot_start: slot.start,
|
|
slot_end: slot.end,
|
|
patient_mobile: mobile,
|
|
patient_name: patientName.trim(),
|
|
}),
|
|
onSuccess: () => {
|
|
toast.success('نوبت با موفقیت ثبت شد');
|
|
onSuccess();
|
|
onClose();
|
|
},
|
|
onError: (e: any) => {
|
|
const msg = e?.response?.data?.errors?.[0]?.message ?? e?.message ?? 'خطا در ثبت نوبت';
|
|
toast.error(msg);
|
|
},
|
|
});
|
|
|
|
const inputSx: React.CSSProperties = {
|
|
width: '100%', height: 38, padding: '0 10px', borderRadius: 'var(--r-sm)',
|
|
border: '1px solid var(--border)', background: 'var(--surface)', fontSize: 13,
|
|
boxSizing: 'border-box',
|
|
};
|
|
const labelSx: React.CSSProperties = {
|
|
display: 'block', fontSize: 12, fontWeight: 600, color: 'var(--text-2)', marginBottom: 6,
|
|
};
|
|
|
|
return (
|
|
<div style={{
|
|
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)', zIndex: 500,
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
}} onClick={onClose}>
|
|
<div style={{
|
|
background: 'var(--surface)', borderRadius: 'var(--r)', padding: 24,
|
|
minWidth: 320, maxWidth: 400, width: '90vw', boxShadow: 'var(--shadow-lg)',
|
|
}} onClick={e => e.stopPropagation()}>
|
|
<div style={{ fontWeight: 700, fontSize: 16, marginBottom: 4 }}>ثبت نوبت</div>
|
|
<div style={{ fontSize: 13, color: 'var(--text-2)', marginBottom: 16 }}>
|
|
{slot.start_time} تا {slot.end_time} — {slot.doctor_name}
|
|
</div>
|
|
|
|
<div style={{ marginBottom: 12 }}>
|
|
<label style={labelSx}>نام و نام خانوادگی بیمار *</label>
|
|
<input
|
|
type="text"
|
|
value={patientName}
|
|
onChange={e => setPatientName(e.target.value)}
|
|
placeholder="مثال: علی محمدی"
|
|
style={inputSx}
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
|
|
<div style={{ marginBottom: 16 }}>
|
|
<label style={labelSx}>شماره موبایل بیمار *</label>
|
|
<input
|
|
type="tel"
|
|
value={mobile}
|
|
onChange={e => setMobile(e.target.value)}
|
|
placeholder="مثال: 09123456789"
|
|
style={{ ...inputSx, direction: 'ltr' }}
|
|
/>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
|
<button className="btn sm" onClick={onClose}>انصراف</button>
|
|
<button
|
|
className="btn primary sm"
|
|
onClick={() => mutation.mutate()}
|
|
disabled={!isValid || mutation.isPending}
|
|
>
|
|
{mutation.isPending ? '...' : 'ثبت نوبت'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Main Page — «نوبت ها» (طرح tauri)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
interface TodayStats { total: number; completed: number; waiting: number; cancelled: number; }
|
|
|
|
export default function AppointmentsPage() {
|
|
const navigate = useNavigate();
|
|
const primaryRole = useAuthStore(s => s.primaryRole);
|
|
const dbUuid = useAuthStore(s => s.dbUuid);
|
|
const isAdmin = primaryRole === 'admin';
|
|
const isClinic = primaryRole === 'clinic';
|
|
const isDoctor = primaryRole === 'doctor';
|
|
const isRepresentation = primaryRole === 'representation';
|
|
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
const [selectedDate, setSelectedDate] = useState(today);
|
|
const [viewMode, setViewMode] = useState<TurnsViewMode>('timeline');
|
|
const [selectedDoctorUuid, setSelectedDoctorUuid] = useState<string>(isDoctor && dbUuid ? dbUuid : '');
|
|
const [bookingSlot, setBookingSlot] = useState<BookingSlot | null>(null);
|
|
const [filtersOpen, setFiltersOpen] = useState(false);
|
|
const [filters, setFilters] = useState<AppointmentFilters>(EMPTY_FILTERS);
|
|
const qc = useQueryClient();
|
|
|
|
// ── Today stats
|
|
const statsQuery = useQuery<ApiResponse<TodayStats>>({
|
|
queryKey: ['appt-today-stats', selectedDate, isAdmin],
|
|
queryFn: () => api.get(
|
|
isAdmin
|
|
? `/api/v1/admin/appointments/today-stats?date=${selectedDate}`
|
|
: `/api/v1/my/appointments/today-stats?date=${selectedDate}`
|
|
),
|
|
});
|
|
const stats = statsQuery.data?.data ?? { total: 0, completed: 0, waiting: 0, cancelled: 0 };
|
|
|
|
// ── Appointments query
|
|
const apptEndpoint = isAdmin
|
|
? '/api/v1/admin/appointments'
|
|
: isRepresentation
|
|
? '/api/v1/representation/appointments'
|
|
: '/api/v1/my/appointments';
|
|
const apptQueryKey = ['appointments', apptEndpoint, selectedDate, selectedDoctorUuid];
|
|
const apptParams = new URLSearchParams({ date: selectedDate, limit: '500' });
|
|
if (selectedDoctorUuid) apptParams.set('doctor_uuid', selectedDoctorUuid);
|
|
|
|
const apptQuery = useQuery<PaginatedResponse<Appointment>>({
|
|
queryKey: apptQueryKey,
|
|
queryFn: () => api.get(`${apptEndpoint}?${apptParams}`),
|
|
});
|
|
const appointments: Appointment[] = apptQuery.data?.data ?? EMPTY_ARR;
|
|
const filteredAppointments = applyAppointmentFilters(appointments, filters);
|
|
const filtersActive = filters !== EMPTY_FILTERS && JSON.stringify(filters) !== JSON.stringify(EMPTY_FILTERS);
|
|
|
|
// Table pagination — client-side (full day stays loaded for timeline + doctor tabs).
|
|
const TABLE_PAGE_SIZE = 20;
|
|
const [tablePage, setTablePage] = useState(1);
|
|
useEffect(() => { setTablePage(1); }, [selectedDate, selectedDoctorUuid, filters]);
|
|
const pagedAppointments = filteredAppointments.slice((tablePage - 1) * TABLE_PAGE_SIZE, tablePage * TABLE_PAGE_SIZE);
|
|
|
|
// ── Clinic doctors (authoritative list for tabs)
|
|
const clinicDoctorsQuery = useQuery<ApiResponse<{ data: { uuid: string; name: string }[] }>>({
|
|
queryKey: ['clinic-doctors', dbUuid],
|
|
queryFn: () => api.get(`/api/v1/clinic/doctor-list/${dbUuid}`),
|
|
enabled: isClinic && !!dbUuid,
|
|
});
|
|
const clinicDoctorsList = clinicDoctorsQuery.data?.data?.data ?? [];
|
|
|
|
const doctors = React.useMemo(() => {
|
|
const map = new Map<string, string>();
|
|
clinicDoctorsList.forEach(d => map.set(d.uuid, d.name));
|
|
appointments.forEach(a => {
|
|
if (a.doctor_uuid && a.doctor_name && !map.has(a.doctor_uuid)) {
|
|
map.set(a.doctor_uuid, a.doctor_name);
|
|
}
|
|
});
|
|
return Array.from(map.entries()).map(([uuid, name]) => ({ uuid, name }));
|
|
}, [appointments, clinicDoctorsList]);
|
|
|
|
const showDoctorTabs = (isClinic || isAdmin) && doctors.length >= 2;
|
|
const showDoctorCol = isAdmin || (isClinic && !selectedDoctorUuid);
|
|
|
|
// ── Slots query (timeline)
|
|
const slotsQueryKey = ['appt-slots', selectedDoctorUuid, selectedDate];
|
|
const slotsQuery = useQuery<ApiResponse<any>>({
|
|
queryKey: slotsQueryKey,
|
|
queryFn: () => api.get(`/api/v1/appointment-slots?doctor_uuid=${selectedDoctorUuid}&date=${selectedDate}`),
|
|
enabled: viewMode === 'timeline' && !!selectedDoctorUuid,
|
|
});
|
|
|
|
// ── Merge sessions + appointments → flat timeline slots
|
|
const timelineSlots: TimelineSlot[] = React.useMemo(() => {
|
|
if (viewMode !== 'timeline') return [];
|
|
const rawSessions: any[] = (slotsQuery.data?.data as any)?.sessions ?? [];
|
|
|
|
const activeByStart = new Map<number, Appointment>();
|
|
const cancelledByStart = new Map<number, Appointment>();
|
|
appointments.forEach(a => {
|
|
const key = typeof a.slot_start === 'number' ? a.slot_start : parseInt(String(a.slot_start), 10);
|
|
if (CANCELLED_STATUSES.has(a.status)) {
|
|
const existing = cancelledByStart.get(key);
|
|
if (!existing || a.created_at > existing.created_at) cancelledByStart.set(key, a);
|
|
} else {
|
|
activeByStart.set(key, a);
|
|
}
|
|
});
|
|
|
|
const out: TimelineSlot[] = [];
|
|
rawSessions.forEach((session: any) => {
|
|
(session.slots as any[]).forEach((s: any) => {
|
|
const slotStart = typeof s.start === 'number' ? s.start : parseInt(String(s.start), 10);
|
|
const slotEnd = typeof s.end === 'number' ? s.end : parseInt(String(s.end), 10);
|
|
out.push({
|
|
start: slotStart,
|
|
end: slotEnd,
|
|
start_time: s.start_time ?? new Date(slotStart * 1000).toLocaleTimeString('fa-IR', { hour: '2-digit', minute: '2-digit' }),
|
|
end_time: s.end_time ?? new Date(slotEnd * 1000).toLocaleTimeString('fa-IR', { hour: '2-digit', minute: '2-digit' }),
|
|
is_available: s.is_available as boolean,
|
|
appointment: activeByStart.get(slotStart) ?? null,
|
|
cancelled_appointment: cancelledByStart.get(slotStart) ?? null,
|
|
});
|
|
});
|
|
});
|
|
return out;
|
|
}, [viewMode, slotsQuery.data, appointments]);
|
|
|
|
// ── Slot click → quick booking modal
|
|
function handleSlotClick(slot: TimelineSlot) {
|
|
if (isRepresentation) return;
|
|
const doctorName = doctors.find(d => d.uuid === selectedDoctorUuid)?.name ?? '';
|
|
setBookingSlot({
|
|
start: slot.start,
|
|
end: slot.end,
|
|
start_time: slot.start_time,
|
|
end_time: slot.end_time,
|
|
doctor_uuid: selectedDoctorUuid,
|
|
doctor_name: doctorName,
|
|
});
|
|
}
|
|
|
|
function openDetail(a: Appointment) {
|
|
navigate(`/admin/appointments/${a.uuid}`);
|
|
}
|
|
|
|
return (
|
|
<div style={{ padding: '20px 24px' }}>
|
|
<div style={{ maxWidth: 1050, margin: '0 auto' }}>
|
|
{/* عنوان */}
|
|
<h1 style={{ fontSize: 20, fontWeight: 700, color: 'var(--text)', marginBottom: 16 }}>نوبت ها</h1>
|
|
|
|
{/* نوار آمار */}
|
|
<TurnsStatInfo stats={stats} />
|
|
|
|
{/* نوار ابزار (بیرونِ کارت، مطابق طرح) */}
|
|
<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>
|
|
)}
|
|
|
|
<StaffFilterSelect value={filters.staffUuid} onChange={(v) => setFilters(f => ({ ...f, staffUuid: v }))} />
|
|
|
|
<button
|
|
aria-label="فیلترها"
|
|
className="btn sm"
|
|
onClick={() => setFiltersOpen(true)}
|
|
style={{
|
|
border: `1px solid ${filtersActive ? 'var(--primary)' : 'var(--border)'}`,
|
|
color: filtersActive ? 'var(--primary)' : 'var(--text-2)',
|
|
background: 'var(--surface)',
|
|
}}
|
|
>
|
|
<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>
|
|
)}
|
|
|
|
<div style={{ flex: 1 }} />
|
|
|
|
<DateNavigator date={selectedDate} onChange={setSelectedDate} />
|
|
</div>
|
|
|
|
{/* کارت اصلی — تب دکترها (هدر) + محتوا */}
|
|
<div style={{
|
|
background: 'var(--surface)', border: '1px solid var(--border)',
|
|
borderRadius: 'var(--r)', overflow: 'hidden',
|
|
}}>
|
|
{showDoctorTabs && (
|
|
<DoctorTabs doctors={doctors} selected={selectedDoctorUuid} onSelect={setSelectedDoctorUuid} />
|
|
)}
|
|
<div style={{ padding: 16 }}>
|
|
{viewMode === 'table' ? (
|
|
<>
|
|
<TurnsTable
|
|
items={pagedAppointments}
|
|
loading={apptQuery.isLoading}
|
|
queryKey={apptQueryKey}
|
|
showDoctor={showDoctorCol}
|
|
/>
|
|
{filteredAppointments.length > TABLE_PAGE_SIZE && (
|
|
<div style={{ marginTop: 14 }}>
|
|
<Pagination page={tablePage} total={filteredAppointments.length} limit={TABLE_PAGE_SIZE} onPageChange={setTablePage} />
|
|
</div>
|
|
)}
|
|
</>
|
|
) : (
|
|
<>
|
|
{!selectedDoctorUuid ? (
|
|
<div style={{ padding: 40, textAlign: 'center', color: 'var(--text-3)' }}>
|
|
برای نمایش زمانبندی، ابتدا یک پزشک انتخاب کنید
|
|
</div>
|
|
) : (
|
|
<TurnsTimeline
|
|
slots={timelineSlots}
|
|
loading={apptQuery.isLoading || slotsQuery.isLoading}
|
|
queryKey={apptQueryKey}
|
|
onView={openDetail}
|
|
onBook={handleSlotClick}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* مودال ثبت سریع نوبت */}
|
|
{bookingSlot && (
|
|
<NewAppointmentModal
|
|
slot={bookingSlot}
|
|
onClose={() => setBookingSlot(null)}
|
|
onSuccess={() => {
|
|
qc.invalidateQueries({ queryKey: apptQueryKey });
|
|
qc.invalidateQueries({ queryKey: slotsQueryKey });
|
|
qc.invalidateQueries({ queryKey: ['appt-today-stats', selectedDate] });
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* مودال فیلترها */}
|
|
{filtersOpen && (
|
|
<AppointmentFiltersModal value={filters} onApply={setFilters} onClose={() => setFiltersOpen(false)} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** فیلتر پرسنل نوار ابزار — ردیفهای بارگذاریشدهٔ روز را بر اساس پرسنل فیلتر میکند. */
|
|
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'),
|
|
});
|
|
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>
|
|
);
|
|
}
|