Replace all native <select> elements in the admin panel with SearchableSelect for a consistent UI experience. This change enhances accessibility, supports RTL and dark mode, and improves the overall design by utilizing a common component. The updates include adjustments to state management and event handling to ensure seamless integration with existing functionality across various pages and components.

This commit is contained in:
hamed
2026-07-16 08:56:59 +03:30
parent 7b8a5c2775
commit b1b99903ec
21 changed files with 571 additions and 288 deletions
+43 -17
View File
@@ -31,7 +31,6 @@ const addMinutes = (time: string, min: number) => {
};
const label = { fontSize: 12.5, color: 'var(--text-3)', display: 'block' } as const;
const sel: React.CSSProperties = { width: '100%', height: 38, borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', fontSize: 13, fontFamily: 'inherit', padding: '0 10px' };
const sectionTitle: React.CSSProperties = { fontSize: 14, fontWeight: 700, color: 'var(--text)', margin: '18px 0 12px' };
export default function AppointmentCreatePage() {
@@ -228,25 +227,47 @@ export default function AppointmentCreatePage() {
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10, marginBottom: 10 }}>
<div>
<label style={label}>بخش</label>
<select aria-label="بخش" style={{ ...sel, marginTop: 6 }} value={sectionUuid} onChange={e => { setSectionUuid(e.target.value); setItemUuid(''); }}>
<option value="">انتخاب بخش</option>
{(sectionsQ.data?.data ?? []).map(o => <option key={o.uuid} value={o.uuid}>{o.name}</option>)}
</select>
<div style={{ marginTop: 6 }}>
<SearchableSelect
options={(sectionsQ.data?.data ?? []).map(o => ({ value: o.uuid, label: o.name ?? '' }))}
value={sectionUuid || null}
onChange={v => { setSectionUuid(v ? String(v) : ''); setItemUuid(''); }}
placeholder="انتخاب بخش"
isLoading={sectionsQ.isLoading}
isClearable
height={38}
/>
</div>
</div>
<div>
<label style={label}>سرویس</label>
<select aria-label="سرویس" style={{ ...sel, marginTop: 6 }} value={itemUuid} onChange={e => setItemUuid(e.target.value)} disabled={!sectionUuid}>
<option value="">انتخاب سرویس</option>
{(itemsQ.data?.data ?? []).map(o => <option key={o.uuid} value={o.uuid}>{o.name}</option>)}
</select>
<div style={{ marginTop: 6 }}>
<SearchableSelect
options={(itemsQ.data?.data ?? []).map(o => ({ value: o.uuid, label: o.name ?? '' }))}
value={itemUuid || null}
onChange={v => setItemUuid(v ? String(v) : '')}
placeholder="انتخاب سرویس"
isDisabled={!sectionUuid}
isLoading={itemsQ.isLoading}
isClearable
height={38}
/>
</div>
</div>
</div>
)}
<label style={label}>انتخاب پرسنل</label>
<select aria-label="پرسنل" style={{ ...sel, margin: '6px 0 4px' }} value={staffUuid} onChange={e => setStaffUuid(e.target.value)}>
<option value="">انتخاب...</option>
{(staffQ.data?.data ?? []).map(o => <option key={o.uuid} value={o.uuid}>{o.full_name}</option>)}
</select>
<div style={{ margin: '6px 0 4px' }}>
<SearchableSelect
options={(staffQ.data?.data ?? []).map(o => ({ value: o.uuid, label: o.full_name ?? '' }))}
value={staffUuid || null}
onChange={v => setStaffUuid(v ? String(v) : '')}
placeholder="انتخاب..."
isLoading={staffQ.isLoading}
isClearable
height={38}
/>
</div>
{/* زمان نوبت */}
<div style={sectionTitle}>زمان نوبت:</div>
@@ -301,10 +322,15 @@ export default function AppointmentCreatePage() {
)}
<label style={label}>انتخاب وضعیت</label>
<select aria-label="وضعیت" style={{ ...sel, margin: '6px 0 12px' }} value={status} onChange={e => setStatus(e.target.value)}>
<option value="pending">ثبت شده</option>
<option value="confirmed">قطعی شده</option>
</select>
<div style={{ margin: '6px 0 12px' }}>
<SearchableSelect
options={[{ value: 'pending', label: 'ثبت شده' }, { value: 'confirmed', label: 'قطعی شده' }]}
value={status || null}
onChange={v => setStatus(v ? String(v) : '')}
placeholder="انتخاب وضعیت"
height={38}
/>
</div>
<label style={label}>توضیحات</label>
<div className="field" style={{ height: 'auto', margin: '6px 0 16px' }}>
@@ -46,7 +46,7 @@ describe('AppointmentEditPage (ویرایش نوبت)', () => {
renderEdit();
expect(await screen.findByText('مشخصات سرویس:')).toBeInTheDocument();
expect((screen.getByLabelText('ساعت شروع') as HTMLInputElement).value).toBe('15:00');
expect((screen.getByLabelText('وضعیت') as HTMLSelectElement).value).toBe('confirmed');
expect(screen.getByText('قطعی شده')).toBeInTheDocument(); // react-select single value = confirmed
expect(screen.getByDisplayValue('یادداشت')).toBeInTheDocument();
});
+44 -16
View File
@@ -7,6 +7,7 @@ import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
import PersianDateInput from '../components/ui/PersianDateInput';
import PriceInput from '../components/ui/PriceInput';
import SearchableSelect from '../components/ui/SearchableSelect';
import { WalletChargeLink } from '../components/AppointmentActions';
interface Option { uuid: string; name?: string; full_name?: string }
@@ -100,7 +101,6 @@ export default function AppointmentEditPage() {
});
const label = { fontSize: 12.5, color: 'var(--text-3)' } as const;
const sel = { width: '100%', height: 38, borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', fontSize: 13, fontFamily: 'inherit', padding: '0 10px' } as const;
if (isLoading || !a) return <div style={{ padding: 24, color: 'var(--text-3)', fontSize: 13 }}>در حال بارگذاری...</div>;
@@ -122,24 +122,46 @@ export default function AppointmentEditPage() {
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 12, marginBottom: 18 }}>
<div>
<label style={label}>بخش</label>
<select aria-label="بخش" style={{ ...sel, marginTop: 6 }} value={sectionUuid} onChange={e => { setSectionUuid(e.target.value); setItemUuid(''); }}>
<option value="">انتخاب بخش</option>
{(sectionsQ.data?.data ?? []).map(o => <option key={o.uuid} value={o.uuid}>{o.name}</option>)}
</select>
<div style={{ marginTop: 6 }}>
<SearchableSelect
options={(sectionsQ.data?.data ?? []).map(o => ({ value: o.uuid, label: o.name ?? '' }))}
value={sectionUuid || null}
onChange={v => { setSectionUuid(v ? String(v) : ''); setItemUuid(''); }}
placeholder="انتخاب بخش"
isLoading={sectionsQ.isLoading}
isClearable
height={38}
/>
</div>
</div>
<div>
<label style={label}>سرویس</label>
<select aria-label="سرویس" style={{ ...sel, marginTop: 6 }} value={itemUuid} onChange={e => setItemUuid(e.target.value)} disabled={!sectionUuid}>
<option value="">انتخاب سرویس</option>
{(itemsQ.data?.data ?? []).map(o => <option key={o.uuid} value={o.uuid}>{o.name}</option>)}
</select>
<div style={{ marginTop: 6 }}>
<SearchableSelect
options={(itemsQ.data?.data ?? []).map(o => ({ value: o.uuid, label: o.name ?? '' }))}
value={itemUuid || null}
onChange={v => setItemUuid(v ? String(v) : '')}
placeholder="انتخاب سرویس"
isDisabled={!sectionUuid}
isLoading={itemsQ.isLoading}
isClearable
height={38}
/>
</div>
</div>
<div>
<label style={label}>پرسنل</label>
<select aria-label="پرسنل" style={{ ...sel, marginTop: 6 }} value={staffUuid} onChange={e => setStaffUuid(e.target.value)}>
<option value="">انتخاب پرسنل</option>
{(staffQ.data?.data ?? []).map(o => <option key={o.uuid} value={o.uuid}>{o.full_name}</option>)}
</select>
<div style={{ marginTop: 6 }}>
<SearchableSelect
options={(staffQ.data?.data ?? []).map(o => ({ value: o.uuid, label: o.full_name ?? '' }))}
value={staffUuid || null}
onChange={v => setStaffUuid(v ? String(v) : '')}
placeholder="انتخاب پرسنل"
isLoading={staffQ.isLoading}
isClearable
height={38}
/>
</div>
</div>
</div>
@@ -178,9 +200,15 @@ export default function AppointmentEditPage() {
<div style={{ maxWidth: 320, marginBottom: 18 }}>
<label style={label}>انتخاب وضعیت</label>
<select aria-label="وضعیت" style={{ ...sel, marginTop: 6 }} value={status} onChange={e => setStatus(e.target.value)}>
{statusOptions.map(([v, l]) => <option key={v} value={v}>{l}</option>)}
</select>
<div style={{ marginTop: 6 }}>
<SearchableSelect
options={statusOptions.map(([v, l]) => ({ value: v, label: l }))}
value={status || null}
onChange={v => setStatus(v ? String(v) : '')}
placeholder="انتخاب وضعیت"
height={38}
/>
</div>
</div>
<label style={label}>توضیحات</label>
+11 -5
View File
@@ -15,6 +15,7 @@ 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';
@@ -650,10 +651,15 @@ 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: 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>
<div style={{ minWidth: 280 }}>
<SearchableSelect
options={options.map(s => ({ value: s.uuid, label: s.name }))}
value={value || null}
onChange={v => onChange(v ? String(v) : '')}
placeholder="سرویس مورد نظر را انتخاب کنید..."
isClearable
height={44}
/>
</div>
);
}
+27 -19
View File
@@ -1053,15 +1053,23 @@ function TimeSelect({ value, onChange }: { value: string; onChange: (v: string)
return (
<div className="cp-time-select" style={{ display: 'flex', alignItems: 'center', gap: 6 }} dir="ltr">
<select className="cp-input" style={{ height: 38, width: 72, textAlign: 'center' }}
value={h} onChange={e => onChange(`${e.target.value}:${m}`)}>
{HOUR_VALUES.map(hv => <option key={hv} value={hv}>{hv}</option>)}
</select>
<div style={{ width: 92 }}>
<GlobalSearchableSelect
options={HOUR_VALUES.map(hv => ({ value: hv, label: hv }))}
value={h}
onChange={v => onChange(`${v ? String(v) : h}:${m}`)}
height={38}
/>
</div>
<span style={{ fontWeight: 700, color: 'var(--text-2)' }}>:</span>
<select className="cp-input" style={{ height: 38, width: 72, textAlign: 'center' }}
value={m} onChange={e => onChange(`${h}:${e.target.value}`)}>
{MINUTE_VALUES.map(mv => <option key={mv} value={mv}>{mv}</option>)}
</select>
<div style={{ width: 92 }}>
<GlobalSearchableSelect
options={MINUTE_VALUES.map(mv => ({ value: mv, label: mv }))}
value={m}
onChange={v => onChange(`${h}:${v ? String(v) : m}`)}
height={38}
/>
</div>
</div>
);
}
@@ -1494,24 +1502,24 @@ export function WeeklyScheduleTab({ doctorUuid, addresses, readOnly = false }: {
<div className={`px-4 py-3 transition-opacity ${meta.online_booking_enabled ? '' : 'opacity-50 pointer-events-none'}`}>
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm text-slate-600 dark:text-slate-400">رزرو آنلاین تا</span>
<div className="flex items-stretch rounded-lg border border-slate-200 dark:border-gray-700 overflow-hidden bg-white dark:bg-gray-900">
<div className="flex items-stretch gap-2">
<input
type="number"
min={1}
value={meta.booking_window_value}
disabled={!meta.online_booking_enabled}
onChange={(e) => setMeta(m => ({ ...m, booking_window_value: Math.max(1, Number(e.target.value) || 1) }))}
className="w-14 text-center text-sm bg-transparent border-0 focus:outline-none focus:ring-0 px-2 py-1.5"
className="w-14 text-center text-sm rounded-lg border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-900 focus:outline-none focus:ring-0 px-2 py-1.5"
/>
<select
value={meta.booking_window_unit}
disabled={!meta.online_booking_enabled}
onChange={(e) => setMeta(m => ({ ...m, booking_window_unit: e.target.value as 'week' | 'month' }))}
className="text-sm bg-slate-50 dark:bg-gray-800 border-0 border-r border-slate-200 dark:border-gray-700 focus:outline-none focus:ring-0 px-2 py-1.5"
>
<option value="week">هفته</option>
<option value="month">ماه</option>
</select>
<div style={{ width: 110 }}>
<GlobalSearchableSelect
options={[{ value: 'week', label: 'هفته' }, { value: 'month', label: 'ماه' }]}
value={meta.booking_window_unit}
onChange={(v) => setMeta(m => ({ ...m, booking_window_unit: (v as 'week' | 'month') }))}
isDisabled={!meta.online_booking_enabled}
height={38}
/>
</div>
</div>
<span className="text-sm text-slate-600 dark:text-slate-400">آینده</span>
</div>
+10 -5
View File
@@ -1,6 +1,7 @@
import React, { useMemo, useState } from 'react';
import { PlusIcon, MagnifyingGlassIcon, ArchiveBoxIcon } from '@heroicons/react/24/outline';
import ConfirmDialog from '../components/ui/ConfirmDialog';
import SearchableSelect from '../components/ui/SearchableSelect';
import { useInventory } from '../hooks/useInventory';
import type { InventoryItem, InventoryPackage, ItemPayload, PackagePayload } from '../hooks/useInventory';
import InventoryStatCards from '../components/inventory/InventoryStatCards';
@@ -68,11 +69,15 @@ export default function InventoryPage() {
style={{ border: 'none', background: 'transparent', flex: 1, padding: 0 }}
/>
</div>
<div className="field" style={{ minWidth: 200, flex: '1 1 260px', maxWidth: 397 }}>
<select value={category} onChange={(e) => setCategory(e.target.value)}>
<option value="">دستهبندی کالا را انتخاب کنید...</option>
{categories.map((c) => <option key={c} value={c}>{c}</option>)}
</select>
<div style={{ minWidth: 200, flex: '1 1 260px', maxWidth: 397 }}>
<SearchableSelect
options={categories.map((c) => ({ value: c, label: c }))}
value={category || null}
onChange={(v) => setCategory(v ? String(v) : '')}
placeholder="دسته‌بندی کالا را انتخاب کنید..."
isClearable
height={38}
/>
</div>
</div>
<button className="btn primary" onClick={() => setItemModal({ open: true, editing: null })}>
+29 -40
View File
@@ -1393,29 +1393,25 @@ function MyPatientsPageInner() {
>
<div className="field" style={{ flexDirection: "column", alignItems: "stretch", height: "auto", gap: 5, padding: 0, border: "none", background: "none" }}>
<label style={{ fontSize: 12.5, fontWeight: 600 }}>بیمه پایه</label>
<select
className="input"
value={baseInsuranceId}
onChange={(e) => applyBaseInsurance(e.target.value)}
>
<option value="">بدون بیمه پایه</option>
{baseInsuranceOptions.map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
<SearchableSelect
options={baseInsuranceOptions}
value={baseInsuranceId || null}
onChange={(v) => applyBaseInsurance(v ? String(v) : "")}
placeholder="بدون بیمه پایه"
isClearable
height={38}
/>
</div>
<div className="field" style={{ flexDirection: "column", alignItems: "stretch", height: "auto", gap: 5, padding: 0, border: "none", background: "none" }}>
<label style={{ fontSize: 12.5, fontWeight: 600 }}>بیمه تکمیلی</label>
<select
className="input"
value={suppInsuranceId}
onChange={(e) => applySuppInsurance(e.target.value)}
>
<option value="">بدون بیمه تکمیلی</option>
{suppInsuranceOptions.map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
<SearchableSelect
options={suppInsuranceOptions}
value={suppInsuranceId || null}
onChange={(v) => applySuppInsurance(v ? String(v) : "")}
placeholder="بدون بیمه تکمیلی"
isClearable
height={38}
/>
</div>
</div>
<div
@@ -1460,17 +1456,14 @@ function MyPatientsPageInner() {
</div>
</div>
<div style={{ fontWeight: 700, fontSize: 13.5, color: "var(--text-2)", borderTop: "1px solid var(--border)", paddingTop: 12 }}>پرداخت و یادداشت</div>
<div className="field">
<div className="field" style={{ flexDirection: "column", alignItems: "stretch", height: "auto", gap: 5, padding: 0, border: "none", background: "none" }}>
<label>روش پرداخت</label>
<select {...form.register("payment_method")}>
{Object.entries(PAYMENT_LABELS).map(
([v, l]) => (
<option key={v} value={v}>
{l}
</option>
),
)}
</select>
<SearchableSelect
options={Object.entries(PAYMENT_LABELS).map(([v, l]) => ({ value: v, label: l }))}
value={form.watch("payment_method")}
onChange={(v) => form.setValue("payment_method", v as "cash" | "card" | "insurance" | "online" | "pending", { shouldDirty: true })}
height={38}
/>
</div>
<div className="field">
<label>یادداشت</label>
@@ -2026,18 +2019,14 @@ function EditSessionModal({
return (
<Modal open={!!session} onClose={onClose} title="ویرایش مراجعه">
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
<div className="field">
<div className="field" style={{ flexDirection: "column", alignItems: "stretch", height: "auto", gap: 5, padding: 0, border: "none", background: "none" }}>
<label>روش پرداخت</label>
<select
<SearchableSelect
options={Object.entries(PAYMENT_LABELS).map(([v, l]) => ({ value: v, label: l }))}
value={method}
onChange={(e) => setMethod(e.target.value)}
>
{Object.entries(PAYMENT_LABELS).map(([v, l]) => (
<option key={v} value={v}>
{l}
</option>
))}
</select>
onChange={(v) => setMethod(v ? String(v) : "cash")}
height={38}
/>
</div>
<div className="field">
<label>یادداشت</label>
+11 -11
View File
@@ -4,6 +4,7 @@ import { MagnifyingGlassIcon, EyeIcon, BanknotesIcon, UserPlusIcon } from '@hero
import PageHeader from '../components/ui/PageHeader';
import Pagination from '../components/ui/Pagination';
import PersianDateInput from '../components/ui/PersianDateInput';
import SearchableSelect from '../components/ui/SearchableSelect';
import { formatRial, formatDate, formatNumber, toDate } from '../lib/utils';
import {
usePayments,
@@ -92,17 +93,16 @@ export default function MyPaymentsPage() {
/>
</div>
<select
className="input"
style={{ flex: '0 0 auto', width: 160 }}
value={status}
onChange={(e) => { setStatus(e.target.value); reset(); }}
aria-label="وضعیت"
>
<option value="">همه وضعیتها</option>
<option value="paid">پرداخت شده</option>
<option value="unsettled">تسویه نشده</option>
</select>
<div style={{ flex: '0 0 auto', width: 160 }}>
<SearchableSelect
options={[{ value: 'paid', label: 'پرداخت شده' }, { value: 'unsettled', label: 'تسویه نشده' }]}
value={status || null}
onChange={(v) => { setStatus(v ? String(v) : ''); reset(); }}
placeholder="همه وضعیت‌ها"
isClearable
height={38}
/>
</div>
<div style={{ width: 150 }}>
<PersianDateInput value={from} onChange={(v) => { setFrom(v); reset(); }} placeholder="از تاریخ" />
@@ -11,6 +11,16 @@ vi.mock('../lib/api', () => ({
import { api } from '../lib/api';
import PatientRecordFormPage from './PatientRecordFormPage';
/** react-select (SearchableSelect) را با placeholder پیدا و گزینه را با متن انتخاب می‌کند. */
async function pickSelect(placeholder: string, optionLabel: string) {
const ph = await screen.findByText(placeholder);
const control = ph.closest('div[class*="control"]') as HTMLElement;
const input = control.querySelector('input') as HTMLInputElement;
fireEvent.focus(input);
fireEvent.keyDown(input, { key: 'ArrowDown' });
fireEvent.click(await screen.findByText(optionLabel));
}
const post = api.post as ReturnType<typeof vi.fn>;
const patch = api.patch as ReturnType<typeof vi.fn>;
@@ -26,7 +36,7 @@ describe('PatientRecordFormPage (تشکیل پرونده)', () => {
fireEvent.change(screen.getByPlaceholderText('نام و نام خانوادگی را وارد نمایید'), { target: { value: 'بیمار نمونه' } });
fireEvent.change(screen.getByPlaceholderText('شماره پرونده'), { target: { value: 'P-1001' } });
fireEvent.change(screen.getAllByRole('combobox')[0], { target: { value: 'female' } }); // gender
await pickSelect('انتخاب...', 'زن'); // gender = female
fireEvent.change(screen.getByPlaceholderText('کد ملی را وارد نمایید'), { target: { value: '1234567890' } });
fireEvent.change(screen.getByPlaceholderText('شماره تماس را وارد نمایید'), { target: { value: '09120000000' } });
fireEvent.click(screen.getByRole('button', { name: 'ثبت اطلاعات' }));
@@ -42,7 +52,7 @@ describe('PatientRecordFormPage (تشکیل پرونده)', () => {
fireEvent.change(screen.getByPlaceholderText('نام و نام خانوادگی را وارد نمایید'), { target: { value: 'ب' } });
fireEvent.change(screen.getByPlaceholderText('شماره پرونده'), { target: { value: 'P-1' } });
fireEvent.change(screen.getAllByRole('combobox')[0], { target: { value: 'male' } });
await pickSelect('انتخاب...', 'مرد');
fireEvent.change(screen.getByPlaceholderText('کد ملی را وارد نمایید'), { target: { value: '12' } });
fireEvent.change(screen.getByPlaceholderText('شماره تماس را وارد نمایید'), { target: { value: '09120000000' } });
fireEvent.click(screen.getByRole('button', { name: 'ثبت اطلاعات' }));
+16 -9
View File
@@ -10,6 +10,7 @@ import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
import type { PatientRecord } from '../types';
import PersianDateInput from '../components/ui/PersianDateInput';
import SearchableSelect from '../components/ui/SearchableSelect';
const REFERRAL_OPTIONS = ['اینستاگرام', 'معرفی دوستان و آشنایان', 'جستجوی اینترنتی', 'تابلو مطب', 'سایر'];
@@ -116,11 +117,13 @@ export default function PatientRecordFormPage() {
<div className="field"><input {...form.register('record_number')} placeholder="شماره پرونده" /></div>
</Field>
<Field label="جنسیت" required error={form.formState.errors.gender?.message}>
<div className="field"><select {...form.register('gender')} style={{ width: '100%', border: 'none', background: 'transparent', fontFamily: 'inherit' }}>
<option value="">انتخاب...</option>
<option value="female">زن</option>
<option value="male">مرد</option>
</select></div>
<SearchableSelect
options={[{ value: 'female', label: 'زن' }, { value: 'male', label: 'مرد' }]}
value={form.watch('gender') ?? null}
onChange={(v) => form.setValue('gender', v as Form['gender'], { shouldValidate: true, shouldDirty: true })}
placeholder="انتخاب..."
height={38}
/>
</Field>
<Field label="کد ملی" required error={form.formState.errors.national_code?.message}>
<div className="field"><input {...form.register('national_code')} inputMode="numeric" placeholder="کد ملی را وارد نمایید" /></div>
@@ -132,10 +135,14 @@ export default function PatientRecordFormPage() {
<PersianDateInput value={form.watch('birth_date') ?? ''} onChange={(v) => form.setValue('birth_date', v)} enableYearPicker />
</Field>
<Field label="نحوه آشنایی">
<div className="field"><select {...form.register('referral_source')} style={{ width: '100%', border: 'none', background: 'transparent', fontFamily: 'inherit' }}>
<option value="">انتخاب کنید...</option>
{REFERRAL_OPTIONS.map((o) => <option key={o} value={o}>{o}</option>)}
</select></div>
<SearchableSelect
options={REFERRAL_OPTIONS.map((o) => ({ value: o, label: o }))}
value={form.watch('referral_source') || null}
onChange={(v) => form.setValue('referral_source', v ? String(v) : '', { shouldDirty: true })}
placeholder="انتخاب کنید..."
isClearable
height={38}
/>
</Field>
</div>
@@ -5,6 +5,7 @@ import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
import { formatRial, formatDate, tomanToRial } from '../lib/utils';
import Pagination from '../components/ui/Pagination';
import SearchableSelect from '../components/ui/SearchableSelect';
interface WalletBalance { balance_rials: number }
interface RepSummary { income: { settlable_rials: number; settled_rials: number; pending_rials: number } }
@@ -130,15 +131,16 @@ export default function RepresentationSettlementPage() {
onChange={(e) => setAmount(e.target.value)}
style={{ width: 200, height: 38, padding: '0 12px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13.5, boxSizing: 'border-box' }}
/>
<select
value={ibanId} onChange={(e) => setIbanId(e.target.value)} dir="ltr"
style={{ width: 320, height: 38, padding: '0 10px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 12.5, boxSizing: 'border-box' }}
>
<option value="">انتخاب شماره شبا...</option>
{ibans.map((b) => (
<option key={b.id} value={b.id}>{b.iban}{b.bank_name ? `${b.bank_name}` : ''}</option>
))}
</select>
<div style={{ width: 320 }}>
<SearchableSelect
options={ibans.map((b) => ({ value: String(b.id), label: `${b.iban}${b.bank_name ? `${b.bank_name}` : ''}` }))}
value={ibanId || null}
onChange={(v) => setIbanId(v ? String(v) : '')}
placeholder="انتخاب شماره شبا..."
isClearable
height={38}
/>
</div>
<button className="btn primary" onClick={submit} disabled={createMut.isPending}>
{createMut.isPending ? 'در حال ثبت...' : 'ثبت درخواست'}
</button>
+11 -5
View File
@@ -12,6 +12,7 @@ import type { Appointment } from '../types';
import { formatDate } from '../lib/utils';
import { useAuthStore } from '../stores/authStore';
import AppointmentStatusDropdown from '../components/ui/AppointmentStatusDropdown';
import SearchableSelect from '../components/ui/SearchableSelect';
import Pagination from '../components/ui/Pagination';
import NewAppointmentDrawer from '../components/NewAppointmentDrawer';
import { AppointmentInfoModal, TransferReserveModal } from '../components/AppointmentActions';
@@ -118,11 +119,16 @@ export default function ReserveAppointmentsPage() {
<h1 style={{ fontSize: 17, fontWeight: 800 }}>نوبت های رزرو شده</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{isClinic && (
<select aria-label="پزشک" value={doctorUuid} onChange={e => setDoctorUuid(e.target.value)}
style={{ height: 34, borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', fontSize: 13, fontFamily: 'inherit', padding: '0 10px' }}>
<option value="">انتخاب پزشک...</option>
{clinicDoctors.map(d => <option key={d.uuid} value={d.uuid}>{d.name}</option>)}
</select>
<div style={{ minWidth: 180 }}>
<SearchableSelect
options={clinicDoctors.map(d => ({ value: d.uuid, label: d.name }))}
value={doctorUuid || null}
onChange={v => setDoctorUuid(v ? String(v) : '')}
placeholder="انتخاب پزشک..."
isClearable
height={34}
/>
</div>
)}
{primaryRole !== 'representation' && (
<button className="btn primary sm" disabled={!doctorUuid}