refactor: simplify Topbar component and remove unused code
- Removed breadcrumb functionality and related code from Topbar. - Consolidated mobile and desktop sidebar toggle logic. - Adjusted icon sizes and styles for consistency. - Cleaned up unused imports and props in Topbar. refactor: enhance DoctorDetailPage session editor UI - Improved layout and styling of session editor for better usability. - Removed advanced settings toggle and integrated relevant fields directly. - Added visual indicators for session slots and rest periods. refactor: streamline DoctorsPage component - Removed bulk selection functionality and related state management. - Simplified rendering logic for doctor list and adjusted table structure. - Cleaned up unused imports and components for better readability. refactor: optimize UsersPage component - Removed bulk selection and deletion functionality. - Simplified user table structure and adjusted column spans. - Cleaned up unused state and imports for improved clarity.
This commit is contained in:
@@ -1017,110 +1017,126 @@ function SessionEditor({ session, onChange, onRemove, addresses }: {
|
||||
}) {
|
||||
const upd = <K extends keyof SessionConfig>(k: K, v: SessionConfig[K]) =>
|
||||
onChange({ ...session, [k]: v });
|
||||
const hasAdvanced = session.has_rest || session.patient_limit !== null;
|
||||
const [showAdvanced, setShowAdvanced] = useState(hasAdvanced);
|
||||
const slotCount = session.active ? calcSlotCount(session) : 0;
|
||||
const slotCount = calcSlotCount(session);
|
||||
|
||||
return (
|
||||
<div className={`rounded-xl border transition-colors ${session.active ? 'border-primary-200 dark:border-primary-500/30 bg-white dark:bg-gray-800/60' : 'border-slate-200 dark:border-gray-700 bg-slate-50/40 dark:bg-gray-800/30'}`}>
|
||||
{/* ─ row اصلی */}
|
||||
<div className="flex items-center gap-2 px-3 py-2.5 flex-wrap">
|
||||
<button type="button"
|
||||
onClick={() => upd('active', !session.active)}
|
||||
className={`relative w-10 h-5 rounded-full transition-colors shrink-0 focus:outline-none ${session.active ? 'bg-primary-500' : 'bg-slate-300 dark:bg-gray-600'}`}>
|
||||
<span className={`absolute top-0.5 w-4 h-4 rounded-full bg-white shadow-sm transition-transform duration-150 ${session.active ? 'translate-x-5' : 'translate-x-0.5'}`} />
|
||||
</button>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs text-slate-400 shrink-0">از</span>
|
||||
<input type="time" value={session.start_time} onChange={e => upd('start_time', e.target.value)}
|
||||
className="cp-input h-8 w-28 text-sm font-mono text-center px-2" />
|
||||
<span className="text-xs text-slate-400 shrink-0">تا</span>
|
||||
<input type="time" value={session.end_time} onChange={e => upd('end_time', e.target.value)}
|
||||
className="cp-input h-8 w-28 text-sm font-mono text-center px-2" />
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 'var(--r)', background: 'var(--surface)', overflow: 'hidden' }}>
|
||||
|
||||
{/* Time inputs */}
|
||||
<div style={{ padding: '14px 16px 12px', display: 'grid', gridTemplateColumns: '1fr 1fr auto', gap: 12, alignItems: 'end' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-2)', marginBottom: 5 }}>از ساعت</div>
|
||||
<div className="field">
|
||||
<input type="time" dir="ltr" value={session.start_time}
|
||||
onChange={e => upd('start_time', e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<select value={session.duration_per_patient} onChange={e => upd('duration_per_patient', Number(e.target.value))}
|
||||
className="cp-select h-8 text-sm">
|
||||
{DURATION_OPTS.map(d => <option key={d} value={d}>{d} دقیقه</option>)}
|
||||
</select>
|
||||
{session.active && slotCount > 0 && (
|
||||
<span className="text-xs font-medium bg-emerald-100 dark:bg-emerald-500/20 text-emerald-700 dark:text-emerald-300 px-2 py-0.5 rounded-full">
|
||||
{slotCount} نوبت
|
||||
</span>
|
||||
)}
|
||||
<button type="button" onClick={onRemove}
|
||||
className="mr-auto w-7 h-7 flex items-center justify-center rounded-lg text-slate-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors shrink-0">
|
||||
<TrashIcon className="w-3.5 h-3.5" />
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-2)', marginBottom: 5 }}>تا ساعت</div>
|
||||
<div className="field">
|
||||
<input type="time" dir="ltr" value={session.end_time}
|
||||
onChange={e => upd('end_time', e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" className="mini-btn danger" onClick={onRemove} style={{ marginBottom: 1 }}>
|
||||
<TrashIcon style={{ width: 15, height: 15 }} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{session.active && (
|
||||
<div className="border-t border-slate-100 dark:border-gray-700 px-3 pb-3 pt-2.5 space-y-2">
|
||||
{/* ─ مکان مطب — اجباری */}
|
||||
{addresses.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-xs text-slate-500 dark:text-slate-400 shrink-0 w-16">
|
||||
مکان مطب <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="flex-1">
|
||||
<select value={session.location_id ?? ''}
|
||||
onChange={e => upd('location_id', e.target.value ? Number(e.target.value) : null)}
|
||||
className={`cp-select h-8 text-sm w-full ${!session.location_id ? 'border-red-300 dark:border-red-500/60 focus:ring-red-400' : ''}`}>
|
||||
<option value="" disabled>انتخاب مطب...</option>
|
||||
{addresses.map(a => (
|
||||
<option key={a.id} value={a.id}>{a.name ?? a.address ?? `مطب ${a.id}`}</option>
|
||||
))}
|
||||
</select>
|
||||
{!session.location_id && (
|
||||
<p className="text-xs text-red-500 mt-0.5">انتخاب مکان مطب الزامی است</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Duration + Location + Limit */}
|
||||
<div style={{ padding: '0 16px 14px', display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-2)', marginBottom: 5 }}>بازه هر نوبت</div>
|
||||
<div className="field">
|
||||
<select value={session.duration_per_patient}
|
||||
onChange={e => upd('duration_per_patient', Number(e.target.value))}>
|
||||
{DURATION_OPTS.map(d => <option key={d} value={d}>{d} دقیقه</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-2)', marginBottom: 5 }}>مکان نوبت</div>
|
||||
<div className="field">
|
||||
<select value={session.location_id ?? ''}
|
||||
onChange={e => upd('location_id', e.target.value ? Number(e.target.value) : null)}>
|
||||
<option value="">انتخاب کنید</option>
|
||||
{addresses.map(a => (
|
||||
<option key={a.id} value={a.id}>{a.name ?? a.address ?? `مطب ${a.id}`}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-2)', marginBottom: 5 }}>حداکثر نوبت</div>
|
||||
<div className="field">
|
||||
<input type="number" min={1} placeholder="—"
|
||||
value={session.patient_limit ?? ''}
|
||||
onChange={e => upd('patient_limit', e.target.value ? Number(e.target.value) : null)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─ تنظیمات پیشرفته collapse */}
|
||||
<button type="button" onClick={() => setShowAdvanced(v => !v)}
|
||||
className="flex items-center gap-1 text-xs text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors">
|
||||
<ChevronDownIcon className={`w-3.5 h-3.5 transition-transform ${showAdvanced ? 'rotate-180' : ''}`} />
|
||||
{showAdvanced ? 'پنهان کردن' : 'تنظیمات پیشرفته (استراحت · محدودیت نوبت)'}
|
||||
</button>
|
||||
{showAdvanced && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-1">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 dark:text-slate-400 block mb-1">حداکثر نوبت</label>
|
||||
<input type="number" min={1} placeholder="بدون محدودیت"
|
||||
value={session.patient_limit ?? ''}
|
||||
onChange={e => upd('patient_limit', e.target.value ? Number(e.target.value) : null)}
|
||||
className="cp-input h-8 text-sm w-full" />
|
||||
</div>
|
||||
<div className="sm:col-span-2 space-y-2">
|
||||
<label className="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input type="checkbox" checked={session.has_rest} onChange={e => upd('has_rest', e.target.checked)}
|
||||
className="w-3.5 h-3.5 rounded accent-primary-500" />
|
||||
<span className="text-xs text-slate-600 dark:text-slate-400">استراحت دورهای بین نوبتها</span>
|
||||
</label>
|
||||
{session.has_rest && (
|
||||
<div className="flex items-center gap-4 flex-wrap pr-5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs text-slate-500">هر</span>
|
||||
<input type="number" min={1} value={session.rest_interval}
|
||||
onChange={e => upd('rest_interval', Number(e.target.value))}
|
||||
className="cp-input h-7 w-16 text-sm text-center px-1" />
|
||||
<span className="text-xs text-slate-500">دقیقه کار</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs text-slate-500">استراحت</span>
|
||||
<input type="number" min={1} value={session.time_to_rest}
|
||||
onChange={e => upd('time_to_rest', Number(e.target.value))}
|
||||
className="cp-input h-7 w-16 text-sm text-center px-1" />
|
||||
<span className="text-xs text-slate-500">دقیقه</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Rest toggle */}
|
||||
<div style={{
|
||||
padding: '12px 16px', display: 'flex', alignItems: 'center',
|
||||
justifyContent: 'space-between', borderTop: '1px solid var(--border)',
|
||||
}}>
|
||||
<div>
|
||||
<b style={{ fontSize: 13 }}>استراحت دورهای بین نوبتها</b>
|
||||
<div className="muted" style={{ fontSize: 12, marginTop: 2 }}>توقف خودکار پس از مدت کاری مشخص</div>
|
||||
</div>
|
||||
<button type="button" onClick={() => upd('has_rest', !session.has_rest)}
|
||||
style={{
|
||||
position: 'relative', width: 40, height: 22, borderRadius: 99,
|
||||
border: 'none', cursor: 'pointer', flexShrink: 0,
|
||||
background: session.has_rest ? 'var(--primary)' : 'var(--surface-3)',
|
||||
transition: 'background .2s',
|
||||
}}>
|
||||
<span style={{
|
||||
position: 'absolute', top: 3, borderRadius: '50%',
|
||||
width: 16, height: 16, background: '#fff',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,.25)',
|
||||
insetInlineStart: session.has_rest ? 20 : 3,
|
||||
}} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Rest params */}
|
||||
{session.has_rest && (
|
||||
<div style={{ padding: '0 16px 14px', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-2)', marginBottom: 5 }}>هر (دقیقه کار)</div>
|
||||
<div className="field">
|
||||
<input type="number" min={1} value={session.rest_interval}
|
||||
onChange={e => upd('rest_interval', Number(e.target.value))} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-2)', marginBottom: 5 }}>استراحت (دقیقه)</div>
|
||||
<div className="field">
|
||||
<input type="number" min={1} value={session.time_to_rest}
|
||||
onChange={e => upd('time_to_rest', Number(e.target.value))} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Slot count footer */}
|
||||
<div style={{
|
||||
padding: '10px 16px', display: 'flex', alignItems: 'center',
|
||||
justifyContent: 'space-between', borderTop: '1px solid var(--border)',
|
||||
background: 'var(--surface-2)',
|
||||
}}>
|
||||
<span className="muted" style={{ fontSize: 12 }}>تعداد نوبت محاسبهشده در این بازه</span>
|
||||
{slotCount > 0 ? (
|
||||
<span className="badge green">
|
||||
<CheckCircleIcon style={{ width: 13, height: 13 }} />
|
||||
{slotCount} نوبت
|
||||
</span>
|
||||
) : (
|
||||
<span className="muted" style={{ fontSize: 12 }}>—</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1228,65 +1244,59 @@ function WeeklyScheduleTab({ doctorUuid, addresses }: { doctorUuid: string; addr
|
||||
)}
|
||||
|
||||
{SCHEDULE_DAYS.map(day => {
|
||||
const sessions = scheduleMap[day.key]?.sessions ?? [];
|
||||
const isOverlap = overlapDays[day.key];
|
||||
const isExpanded = expandedDay === day.key;
|
||||
const sessions = scheduleMap[day.key]?.sessions ?? [];
|
||||
const isOverlap = overlapDays[day.key];
|
||||
const isExpanded = expandedDay === day.key;
|
||||
const activeSessions = sessions.filter(s => s.active);
|
||||
const daySlots = activeSessions.reduce((sum, s) => sum + calcSlotCount(s), 0);
|
||||
const hasAny = sessions.length > 0;
|
||||
const daySlots = activeSessions.reduce((sum, s) => sum + calcSlotCount(s), 0);
|
||||
|
||||
return (
|
||||
<div key={day.key} className={`rounded-xl border transition-all ${hasAny ? 'border-primary-200 dark:border-primary-500/30' : 'border-slate-200 dark:border-gray-700'} ${isOverlap ? 'border-red-300 dark:border-red-500/50' : ''}`}>
|
||||
{/* ─ header هر روز */}
|
||||
<div key={day.key} className="card" style={{ overflow: 'hidden' }}>
|
||||
{/* Day header */}
|
||||
<div
|
||||
className={`flex items-center gap-2 px-4 py-3 cursor-pointer select-none rounded-xl transition-colors ${hasAny ? 'bg-primary-50/40 dark:bg-primary-500/5 hover:bg-primary-50/70 dark:hover:bg-primary-500/10' : 'bg-slate-50/60 dark:bg-gray-800/30 hover:bg-slate-100/60 dark:hover:bg-gray-800/50'}`}
|
||||
style={{ padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer', userSelect: 'none' }}
|
||||
onClick={() => setExpandedDay(isExpanded ? null : day.key)}
|
||||
>
|
||||
<span className={`text-sm font-semibold w-16 shrink-0 ${hasAny ? 'text-primary-700 dark:text-primary-300' : 'text-slate-500 dark:text-slate-400'}`}>
|
||||
{day.label}
|
||||
</span>
|
||||
|
||||
{/* chips بازههای active */}
|
||||
<div className="flex items-center gap-1.5 flex-wrap flex-1 min-w-0">
|
||||
{sessions.length === 0 && (
|
||||
<span className="text-xs text-slate-400 dark:text-slate-500">تعطیل</span>
|
||||
)}
|
||||
<b style={{ fontSize: 14, minWidth: 56, flexShrink: 0 }}>{day.label}</b>
|
||||
<div style={{ flex: 1, display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', minWidth: 0 }}>
|
||||
{activeSessions.map((s, i) => (
|
||||
<span key={i} className="text-xs bg-primary-100 dark:bg-primary-500/20 text-primary-700 dark:text-primary-300 px-2 py-0.5 rounded-full font-mono whitespace-nowrap">
|
||||
{s.start_time}–{s.end_time}
|
||||
<span key={i} style={{ fontSize: 13, fontFamily: 'monospace', color: 'var(--text-2)', direction: 'ltr', whiteSpace: 'nowrap' }}>
|
||||
{s.start_time} — {s.end_time}
|
||||
</span>
|
||||
))}
|
||||
{daySlots > 0 && (
|
||||
<span className="text-xs text-emerald-600 dark:text-emerald-400 font-medium">
|
||||
({daySlots} نوبت)
|
||||
</span>
|
||||
)}
|
||||
{isOverlap && (
|
||||
<span className="flex items-center gap-0.5 text-xs text-red-500 dark:text-red-400">
|
||||
<ExclamationTriangleIcon className="w-3.5 h-3.5" />تداخل
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<button type="button"
|
||||
onClick={e => { e.stopPropagation(); addSession(day.key); }}
|
||||
className="flex items-center gap-0.5 text-xs text-primary-600 dark:text-primary-400 hover:bg-primary-100 dark:hover:bg-primary-500/20 px-2 py-1 rounded-lg transition-colors">
|
||||
<PlusIcon className="w-3.5 h-3.5" />بازه
|
||||
</button>
|
||||
<ChevronDownIcon className={`w-4 h-4 text-slate-400 transition-transform ${isExpanded ? 'rotate-180' : ''}`} />
|
||||
{sessions.length === 0 && <span className="muted" style={{ fontSize: 12 }}>تعطیل</span>}
|
||||
</div>
|
||||
{daySlots > 0 && (
|
||||
<span className="badge green" style={{ flexShrink: 0 }}>
|
||||
<span className="bdot" />{daySlots} نوبت
|
||||
</span>
|
||||
)}
|
||||
{isOverlap && (
|
||||
<span className="badge red" style={{ flexShrink: 0 }}>
|
||||
<ExclamationTriangleIcon style={{ width: 12, height: 12 }} />تداخل
|
||||
</span>
|
||||
)}
|
||||
<button type="button" className="btn ghost sm" style={{ flexShrink: 0 }}
|
||||
onClick={e => { e.stopPropagation(); addSession(day.key); }}>
|
||||
<PlusIcon style={{ width: 13, height: 13 }} />
|
||||
بازه
|
||||
</button>
|
||||
<ChevronDownIcon style={{
|
||||
width: 16, height: 16, flexShrink: 0, color: 'var(--text-3)',
|
||||
transform: isExpanded ? 'rotate(180deg)' : 'none',
|
||||
transition: 'transform .2s',
|
||||
}} />
|
||||
</div>
|
||||
|
||||
{/* ─ محتوای expanded */}
|
||||
{/* Expanded sessions */}
|
||||
{isExpanded && (
|
||||
<div className="border-t border-slate-100 dark:border-gray-700 px-4 pb-4 pt-3 space-y-2">
|
||||
<div style={{ borderTop: '1px solid var(--border)', padding: '12px 16px 16px', display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{sessions.length === 0 ? (
|
||||
<div className="text-center py-4">
|
||||
<p className="text-xs text-slate-400 dark:text-slate-500 mb-2">هیچ بازهای برای این روز تنظیم نشده</p>
|
||||
<button type="button" onClick={() => addSession(day.key)}
|
||||
className="text-xs text-primary-600 dark:text-primary-400 hover:underline flex items-center gap-1 mx-auto">
|
||||
<PlusIcon className="w-3.5 h-3.5" />افزودن اولین بازه
|
||||
<div style={{ textAlign: 'center', padding: '16px 0' }}>
|
||||
<p className="muted" style={{ fontSize: 13, marginBottom: 10 }}>هیچ بازهای برای این روز تنظیم نشده</p>
|
||||
<button type="button" onClick={() => addSession(day.key)} className="btn ghost sm">
|
||||
<PlusIcon style={{ width: 14, height: 14 }} />
|
||||
افزودن اولین بازه
|
||||
</button>
|
||||
</div>
|
||||
) : sessions.map((session, idx) => (
|
||||
@@ -1309,11 +1319,8 @@ function WeeklyScheduleTab({ doctorUuid, addresses }: { doctorUuid: string; addr
|
||||
)}
|
||||
<button type="button" onClick={() => saveMut.mutate()}
|
||||
disabled={saveMut.isPending || hasAnyOverlap || missingLocation}
|
||||
className="mr-auto cp-btn-primary disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
{saveMut.isPending
|
||||
? <><span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />در حال ذخیره...</>
|
||||
: 'ذخیره برنامه هفتگی'
|
||||
}
|
||||
className="btn primary sm" style={{ marginInlineStart: 'auto', opacity: (saveMut.isPending || hasAnyOverlap || missingLocation) ? 0.5 : 1 }}>
|
||||
{saveMut.isPending ? 'در حال ذخیره...' : 'ذخیره برنامه هفتگی'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user