refactor: update UserDetailPage styles and introduce avatar gradient utility
- Refactored role metadata styles in UserDetailPage for consistency with design tokens. - Replaced hardcoded avatar colors with a utility function to generate gradients based on user ID. - Improved InfoCard component styles for better hover effects and accessibility. - Removed deprecated color classes and adjusted background gradients for various components. - Updated theme token tests to reflect the removal of deferred files and ensure compliance with design standards. - Added new avatarColors utility file to manage avatar gradient definitions.
This commit is contained in:
@@ -169,7 +169,7 @@ function PersianDateInput({ value, onChange, minDate, placeholder }: {
|
|||||||
<div>
|
<div>
|
||||||
<button ref={btnRef} type="button" onClick={openPicker}
|
<button ref={btnRef} type="button" onClick={openPicker}
|
||||||
className="cp-input h-11 flex items-center justify-between w-full cursor-pointer">
|
className="cp-input h-11 flex items-center justify-between w-full cursor-pointer">
|
||||||
<span className={value ? 'text-[var(--text)] dark:text-[var(--text)]' : 'text-[var(--text-3)] dark:text-[var(--text-2)]'}>
|
<span className={value ? 'text-[var(--text)]' : 'text-[var(--text-3)]'}>
|
||||||
{value ? formatPersianDate(value) : (placeholder ?? 'انتخاب تاریخ')}
|
{value ? formatPersianDate(value) : (placeholder ?? 'انتخاب تاریخ')}
|
||||||
</span>
|
</span>
|
||||||
<CalendarIcon className="w-4 h-4 text-[var(--text-3)] shrink-0 mr-2" />
|
<CalendarIcon className="w-4 h-4 text-[var(--text-3)] shrink-0 mr-2" />
|
||||||
@@ -205,10 +205,10 @@ function PersianDateInput({ value, onChange, minDate, placeholder }: {
|
|||||||
return (
|
return (
|
||||||
<button key={jd} type="button" disabled={dis} onClick={() => selectDay(jd)}
|
<button key={jd} type="button" disabled={dis} onClick={() => selectDay(jd)}
|
||||||
className={`w-full aspect-square flex items-center justify-center rounded-lg text-sm transition-colors
|
className={`w-full aspect-square flex items-center justify-center rounded-lg text-sm transition-colors
|
||||||
${dis ? 'text-[var(--text-3)] dark:text-[var(--text-2)] cursor-not-allowed'
|
${dis ? 'text-[var(--text-3)] cursor-not-allowed'
|
||||||
: sel ? 'bg-primary-500 text-[var(--on-primary)] font-semibold shadow-sm'
|
: sel ? 'bg-[var(--primary)] text-[var(--on-primary)] font-semibold shadow-sm'
|
||||||
: tod ? 'ring-1 ring-primary-400 text-primary-700 dark:text-primary-300 font-medium'
|
: tod ? 'ring-1 ring-[var(--primary)] text-[var(--primary-700)] dark:text-[var(--primary)] font-medium'
|
||||||
: 'text-[var(--text)] dark:text-[var(--text-3)] hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)]'}`}>
|
: 'text-[var(--text)] hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)]'}`}>
|
||||||
{toPersianNums(jd)}
|
{toPersianNums(jd)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@@ -352,7 +352,7 @@ function SlotEditor({ slots, onChange }: {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<button type="button" onClick={add}
|
<button type="button" onClick={add}
|
||||||
className="flex items-center gap-1.5 text-xs text-primary-600 dark:text-primary-400 hover:underline mt-1">
|
className="flex items-center gap-1.5 text-xs text-[var(--primary)] dark:text-[var(--primary)] hover:underline mt-1">
|
||||||
<PlusIcon className="w-3.5 h-3.5" />افزودن بازه زمانی
|
<PlusIcon className="w-3.5 h-3.5" />افزودن بازه زمانی
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -511,7 +511,7 @@ function NoLocationsNotice({ clinicUuid }: { clinicUuid?: string | null }) {
|
|||||||
: ['ابتدا آدرس مطب را ثبت کنید', 'برنامه کاری نیاز به حداقل یک مکان نوبت دارد'];
|
: ['ابتدا آدرس مطب را ثبت کنید', 'برنامه کاری نیاز به حداقل یک مکان نوبت دارد'];
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center gap-3 py-12 text-center">
|
<div className="flex flex-col items-center justify-center gap-3 py-12 text-center">
|
||||||
<div className="w-12 h-12 rounded-full bg-[var(--warning-bg)] dark:bg-amber-500/10 border border-[var(--warning)] dark:border-amber-500/30 flex items-center justify-center">
|
<div className="w-12 h-12 rounded-full bg-[var(--warning-bg)] border border-[var(--warning)] flex items-center justify-center">
|
||||||
<MapPinIcon className="w-6 h-6 text-[var(--warning)]" />
|
<MapPinIcon className="w-6 h-6 text-[var(--warning)]" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -627,9 +627,9 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{totalSlots > 0 && (
|
{totalSlots > 0 && (
|
||||||
<div className="flex items-center gap-2 mb-3 p-3 rounded-xl bg-[var(--success-bg)] dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/30">
|
<div className="flex items-center gap-2 mb-3 p-3 rounded-xl bg-[var(--success-bg)] border border-[var(--success)]">
|
||||||
<CalendarIcon className="w-4 h-4 text-[var(--success)] dark:text-emerald-400 shrink-0" />
|
<CalendarIcon className="w-4 h-4 text-[var(--success)] shrink-0" />
|
||||||
<span className="text-sm text-emerald-700 dark:text-emerald-300">
|
<span className="text-sm text-[var(--success)]">
|
||||||
مجموع <strong>{totalSlots}</strong> نوبت در هفته
|
مجموع <strong>{totalSlots}</strong> نوبت در هفته
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -638,7 +638,7 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
|
|||||||
const activeSessions = (scheduleMap[day.key]?.sessions ?? []).filter(s => s.active);
|
const activeSessions = (scheduleMap[day.key]?.sessions ?? []).filter(s => s.active);
|
||||||
const daySlots = activeSessions.reduce((sum, s) => sum + calcSlotCount(s), 0);
|
const daySlots = activeSessions.reduce((sum, s) => sum + calcSlotCount(s), 0);
|
||||||
return (
|
return (
|
||||||
<div key={day.key} className="flex items-center gap-3 px-4 py-3 rounded-xl border border-[var(--border)] bg-[var(--surface)] dark:bg-[var(--surface)]/40">
|
<div key={day.key} className="flex items-center gap-3 px-4 py-3 rounded-xl border border-[var(--border)] bg-[var(--surface)]">
|
||||||
<b style={{ fontSize: 14, minWidth: 56, flexShrink: 0 }}>{day.label}</b>
|
<b style={{ fontSize: 14, minWidth: 56, flexShrink: 0 }}>{day.label}</b>
|
||||||
<div className="flex-1 flex gap-2 items-center flex-wrap min-w-0">
|
<div className="flex-1 flex gap-2 items-center flex-wrap min-w-0">
|
||||||
{activeSessions.length > 0 ? activeSessions.map((s, i) => (
|
{activeSessions.length > 0 ? activeSessions.map((s, i) => (
|
||||||
@@ -665,9 +665,9 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{/* ─ خلاصه کل هفته */}
|
{/* ─ خلاصه کل هفته */}
|
||||||
{totalSlots > 0 && (
|
{totalSlots > 0 && (
|
||||||
<div className="flex items-center gap-2 mb-3 p-3 rounded-xl bg-[var(--success-bg)] dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/30">
|
<div className="flex items-center gap-2 mb-3 p-3 rounded-xl bg-[var(--success-bg)] border border-[var(--success)]">
|
||||||
<CalendarIcon className="w-4 h-4 text-[var(--success)] dark:text-emerald-400 shrink-0" />
|
<CalendarIcon className="w-4 h-4 text-[var(--success)] shrink-0" />
|
||||||
<span className="text-sm text-emerald-700 dark:text-emerald-300">
|
<span className="text-sm text-[var(--success)]">
|
||||||
مجموع <strong>{totalSlots}</strong> نوبت در هفته
|
مجموع <strong>{totalSlots}</strong> نوبت در هفته
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -675,7 +675,7 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
|
|||||||
|
|
||||||
{/* ─ روش نوبتدهی */}
|
{/* ─ روش نوبتدهی */}
|
||||||
<div className="mb-3 rounded-xl border border-[var(--border)] overflow-hidden">
|
<div className="mb-3 rounded-xl border border-[var(--border)] overflow-hidden">
|
||||||
<div className="px-4 py-3 bg-[var(--surface-2)] dark:bg-[var(--surface)]/50">
|
<div className="px-4 py-3 bg-[var(--surface-2)]">
|
||||||
<span className="text-sm font-medium text-[var(--text)]">روش نوبتدهی</span>
|
<span className="text-sm font-medium text-[var(--text)]">روش نوبتدهی</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 py-3 space-y-3">
|
<div className="px-4 py-3 space-y-3">
|
||||||
@@ -694,11 +694,11 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
|
|||||||
className={`text-right p-3 rounded-lg border transition-colors ${
|
className={`text-right p-3 rounded-lg border transition-colors ${
|
||||||
selected
|
selected
|
||||||
? 'border-[var(--primary)] bg-[var(--primary)]/5'
|
? 'border-[var(--primary)] bg-[var(--primary)]/5'
|
||||||
: 'border-[var(--border)] dark:border-[var(--border)] bg-[var(--surface)] dark:bg-[var(--bg)]'
|
: 'border-[var(--border)] bg-[var(--surface)]'
|
||||||
} ${modeLocked ? 'opacity-70 cursor-not-allowed' : 'hover:border-[var(--primary)]'}`}
|
} ${modeLocked ? 'opacity-70 cursor-not-allowed' : 'hover:border-[var(--primary)]'}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<span className={`w-3.5 h-3.5 rounded-full border shrink-0 ${selected ? 'border-[var(--primary)] bg-[var(--primary)]' : 'border-[var(--border-2)] dark:border-gray-600'}`} />
|
<span className={`w-3.5 h-3.5 rounded-full border shrink-0 ${selected ? 'border-[var(--primary)] bg-[var(--primary)]' : 'border-[var(--border-2)]'}`} />
|
||||||
<span className="text-sm font-medium text-[var(--text)]">{lbl}</span>
|
<span className="text-sm font-medium text-[var(--text)]">{lbl}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-[var(--text-2)] leading-relaxed">{desc}</p>
|
<p className="text-xs text-[var(--text-2)] leading-relaxed">{desc}</p>
|
||||||
@@ -712,7 +712,7 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
|
|||||||
نوع نوبتدهی ثبت شده و دیگر قابل تغییر نیست.
|
نوع نوبتدهی ثبت شده و دیگر قابل تغییر نیست.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-[var(--warning)] dark:text-amber-400 leading-relaxed">
|
<p className="text-xs text-[var(--warning)] leading-relaxed">
|
||||||
⚠️ توجه: نوع نوبتدهی پس از اولین ثبت <span className="font-medium">بههیچعنوان قابل تغییر نیست</span>. پیش از ذخیره با دقت انتخاب کنید.
|
⚠️ توجه: نوع نوبتدهی پس از اولین ثبت <span className="font-medium">بههیچعنوان قابل تغییر نیست</span>. پیش از ذخیره با دقت انتخاب کنید.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -745,7 +745,7 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
|
|||||||
{/* ─ نوبتدهی آنلاین */}
|
{/* ─ نوبتدهی آنلاین */}
|
||||||
<div className="mb-3 rounded-xl border border-[var(--border)] overflow-hidden">
|
<div className="mb-3 rounded-xl border border-[var(--border)] overflow-hidden">
|
||||||
{/* header + toggle */}
|
{/* header + toggle */}
|
||||||
<label className="flex items-center justify-between gap-3 px-4 py-3 cursor-pointer bg-[var(--surface-2)] dark:bg-[var(--surface)]/50">
|
<label className="flex items-center justify-between gap-3 px-4 py-3 cursor-pointer bg-[var(--surface-2)]">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<GlobeAltIcon className="w-4 h-4 text-[var(--text-2)] shrink-0" />
|
<GlobeAltIcon className="w-4 h-4 text-[var(--text-2)] shrink-0" />
|
||||||
<span className="text-sm font-medium text-[var(--text)]">نوبتدهی آنلاین</span>
|
<span className="text-sm font-medium text-[var(--text)]">نوبتدهی آنلاین</span>
|
||||||
@@ -757,7 +757,7 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
|
|||||||
checked={meta.online_booking_enabled}
|
checked={meta.online_booking_enabled}
|
||||||
onChange={(e) => setMeta(m => ({ ...m, online_booking_enabled: e.target.checked }))}
|
onChange={(e) => setMeta(m => ({ ...m, online_booking_enabled: e.target.checked }))}
|
||||||
/>
|
/>
|
||||||
<span className="w-10 h-6 rounded-full bg-slate-300 dark:bg-gray-600 transition-colors peer-checked:bg-emerald-500" />
|
<span className="w-10 h-6 rounded-full bg-[var(--surface-3)] transition-colors peer-checked:bg-[var(--success)]" />
|
||||||
<span className="absolute top-0.5 right-0.5 w-5 h-5 rounded-full bg-[var(--surface)] shadow transition-transform peer-checked:-translate-x-4" />
|
<span className="absolute top-0.5 right-0.5 w-5 h-5 rounded-full bg-[var(--surface)] shadow transition-transform peer-checked:-translate-x-4" />
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -864,7 +864,7 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
|
|||||||
|
|
||||||
<div className="flex items-center justify-between pt-3">
|
<div className="flex items-center justify-between pt-3">
|
||||||
{(hasAnyOverlap || missingLocation) && (
|
{(hasAnyOverlap || missingLocation) && (
|
||||||
<p className="text-xs text-[var(--danger)] dark:text-red-400 flex items-center gap-1">
|
<p className="text-xs text-[var(--danger)] flex items-center gap-1">
|
||||||
<ExclamationTriangleIcon className="w-3.5 h-3.5" />
|
<ExclamationTriangleIcon className="w-3.5 h-3.5" />
|
||||||
{hasAnyOverlap ? 'تداخل زمانی در برنامه وجود دارد' : 'مکان مطب برای همه بازهها الزامی است'}
|
{hasAnyOverlap ? 'تداخل زمانی در برنامه وجود دارد' : 'مکان مطب برای همه بازهها الزامی است'}
|
||||||
</p>
|
</p>
|
||||||
@@ -966,7 +966,7 @@ function DateOverrideModal({ open, onClose, existing, doctorUuid, clinicUuid, on
|
|||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
onClick={() => setOverrideType('closed')}
|
onClick={() => setOverrideType('closed')}
|
||||||
className={`flex items-center gap-2 p-3 rounded-xl border-2 text-sm transition-colors text-right ${overrideType === 'closed' ? 'border-red-400 bg-[var(--danger-bg)] dark:bg-[var(--danger)]/10 text-[var(--danger)] dark:text-red-300' : 'border-[var(--border)] dark:border-[var(--border)] text-[var(--text-2)] dark:text-[var(--text-3)] hover:border-[var(--border-2)]'}`}>
|
className={`flex items-center gap-2 p-3 rounded-xl border-2 text-sm transition-colors text-right ${overrideType === 'closed' ? 'border-[var(--danger)] bg-[var(--danger-bg)] text-[var(--danger)]' : 'border-[var(--border)] text-[var(--text-2)] hover:border-[var(--border-2)]'}`}>
|
||||||
<XCircleIcon className="w-5 h-5 shrink-0" />
|
<XCircleIcon className="w-5 h-5 shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium leading-tight">تعطیل است</p>
|
<p className="font-medium leading-tight">تعطیل است</p>
|
||||||
@@ -975,7 +975,7 @@ function DateOverrideModal({ open, onClose, existing, doctorUuid, clinicUuid, on
|
|||||||
</button>
|
</button>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
onClick={() => setOverrideType('custom')}
|
onClick={() => setOverrideType('custom')}
|
||||||
className={`flex items-center gap-2 p-3 rounded-xl border-2 text-sm transition-colors text-right ${overrideType === 'custom' ? 'border-amber-400 bg-[var(--warning-bg)] dark:bg-amber-500/10 text-amber-700 dark:text-amber-300' : 'border-[var(--border)] dark:border-[var(--border)] text-[var(--text-2)] dark:text-[var(--text-3)] hover:border-[var(--border-2)]'}`}>
|
className={`flex items-center gap-2 p-3 rounded-xl border-2 text-sm transition-colors text-right ${overrideType === 'custom' ? 'border-[var(--warning)] bg-[var(--warning-bg)] text-[var(--warning)]' : 'border-[var(--border)] text-[var(--text-2)] hover:border-[var(--border-2)]'}`}>
|
||||||
<CalendarIcon className="w-5 h-5 shrink-0" />
|
<CalendarIcon className="w-5 h-5 shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium leading-tight">ساعات خاص</p>
|
<p className="font-medium leading-tight">ساعات خاص</p>
|
||||||
@@ -1020,9 +1020,9 @@ function DateOverrideModal({ open, onClose, existing, doctorUuid, clinicUuid, on
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{overrideType === 'closed' && (
|
{overrideType === 'closed' && (
|
||||||
<div className="flex items-start gap-2 p-3 rounded-xl bg-[var(--danger-bg)] dark:bg-[var(--danger)]/10 border border-[var(--danger)] dark:border-red-500/30">
|
<div className="flex items-start gap-2 p-3 rounded-xl bg-[var(--danger-bg)] border border-[var(--danger)]">
|
||||||
<ExclamationTriangleIcon className="w-4 h-4 text-[var(--danger)] shrink-0 mt-0.5" />
|
<ExclamationTriangleIcon className="w-4 h-4 text-[var(--danger)] shrink-0 mt-0.5" />
|
||||||
<p className="text-xs text-[var(--danger)] dark:text-red-400">
|
<p className="text-xs text-[var(--danger)]">
|
||||||
در این روز هیچ نوبتی داده نخواهد شد، حتی اگر در برنامه هفتگی ساعات کاری داشته باشید.
|
در این روز هیچ نوبتی داده نخواهد شد، حتی اگر در برنامه هفتگی ساعات کاری داشته باشید.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1079,16 +1079,16 @@ function DateOverridesTab({ doctorUuid, clinicUuid, addresses, readOnly = false
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{overrides.map(ov => (
|
{overrides.map(ov => (
|
||||||
<div key={ov.uuid} className="flex items-center gap-3 p-3.5 rounded-xl border border-[var(--border)] bg-[var(--surface)] dark:bg-[var(--surface)]/60">
|
<div key={ov.uuid} className="flex items-center gap-3 p-3.5 rounded-xl border border-[var(--border)] bg-[var(--surface)]">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<span className="text-sm font-medium text-[var(--text)]">{formatPersianDate(tsToDate(ov.date))}</span>
|
<span className="text-sm font-medium text-[var(--text)]">{formatPersianDate(tsToDate(ov.date))}</span>
|
||||||
{ov.active ? (
|
{ov.active ? (
|
||||||
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-[var(--warning-bg)] dark:bg-amber-500/20 text-amber-700 dark:text-amber-300">
|
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-[var(--warning-bg)] text-[var(--warning)]">
|
||||||
{ov.custom_slots.length > 0 ? `${ov.custom_slots.length} بازه سفارشی` : 'ساعات خاص'}
|
{ov.custom_slots.length > 0 ? `${ov.custom_slots.length} بازه سفارشی` : 'ساعات خاص'}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-[var(--danger-bg)] dark:bg-[var(--danger)]/20 text-[var(--danger)] dark:text-red-400">
|
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-[var(--danger-bg)] text-[var(--danger)]">
|
||||||
تعطیل
|
تعطیل
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -1098,7 +1098,7 @@ function DateOverridesTab({ doctorUuid, clinicUuid, addresses, readOnly = false
|
|||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<div className="flex items-center gap-1 shrink-0">
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
<button onClick={() => { setEditing(ov); setModalOpen(true); }}
|
<button onClick={() => { setEditing(ov); setModalOpen(true); }}
|
||||||
className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--info)] hover:bg-[var(--info-bg)] dark:hover:bg-blue-500/10 transition-colors">
|
className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--info)] hover:bg-[var(--info-bg)] dark:hover:bg-[var(--info)]/10 transition-colors">
|
||||||
<PencilIcon className="w-3.5 h-3.5" />
|
<PencilIcon className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => setDeletingUuid(ov.uuid)}
|
<button onClick={() => setDeletingUuid(ov.uuid)}
|
||||||
@@ -1250,17 +1250,17 @@ function HolidaysTab({ doctorUuid, clinicUuid, readOnly = false }: { doctorUuid:
|
|||||||
const isCurrent = h.start_date <= now && h.end_date >= now;
|
const isCurrent = h.start_date <= now && h.end_date >= now;
|
||||||
const sameDay = tsToDate(h.start_date) === tsToDate(h.end_date);
|
const sameDay = tsToDate(h.start_date) === tsToDate(h.end_date);
|
||||||
return (
|
return (
|
||||||
<div key={h.uuid} className={`flex items-center gap-3 p-3.5 rounded-xl border transition-colors ${isCurrent ? 'border-[var(--danger)] dark:border-red-500/30 bg-[var(--danger-bg)]/40 dark:bg-[var(--danger)]/5' : isPast ? 'border-[var(--border)] dark:border-[var(--border)] bg-[var(--surface-2)]/40 dark:bg-[var(--surface)]/30 opacity-70' : 'border-orange-200 dark:border-orange-500/30 bg-orange-50/30 dark:bg-orange-500/5'}`}>
|
<div key={h.uuid} className={`flex items-center gap-3 p-3.5 rounded-xl border transition-colors ${isCurrent ? 'border-[var(--danger)] bg-[var(--danger-bg)]/40' : isPast ? 'border-[var(--border)] bg-[var(--surface-2)]/40 opacity-70' : 'border-[var(--accent)] bg-[var(--accent-bg)]/30'}`}>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<span className={`text-sm font-medium ${isPast ? 'text-[var(--text-2)] dark:text-[var(--text-3)]' : 'text-[var(--text)] dark:text-[var(--text)]'}`}>
|
<span className={`text-sm font-medium ${isPast ? 'text-[var(--text-2)]' : 'text-[var(--text)]'}`}>
|
||||||
{sameDay ? formatPersianDate(tsToDate(h.start_date)) : `${formatPersianDate(tsToDate(h.start_date))} تا ${formatPersianDate(tsToDate(h.end_date))}`}
|
{sameDay ? formatPersianDate(tsToDate(h.start_date)) : `${formatPersianDate(tsToDate(h.start_date))} تا ${formatPersianDate(tsToDate(h.end_date))}`}
|
||||||
</span>
|
</span>
|
||||||
{isCurrent && h.active && (
|
{isCurrent && h.active && (
|
||||||
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-[var(--danger-bg)] dark:bg-[var(--danger)]/20 text-[var(--danger)] dark:text-red-400 animate-pulse">در جریان</span>
|
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-[var(--danger-bg)] text-[var(--danger)] animate-pulse">در جریان</span>
|
||||||
)}
|
)}
|
||||||
{!isCurrent && !isPast && h.active && (
|
{!isCurrent && !isPast && h.active && (
|
||||||
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-orange-100 dark:bg-orange-500/20 text-orange-600 dark:text-orange-400">آینده</span>
|
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-[var(--accent-bg)] text-[var(--accent)]">آینده</span>
|
||||||
)}
|
)}
|
||||||
{isPast && (
|
{isPast && (
|
||||||
<span className="text-xs px-2 py-0.5 rounded-full bg-[var(--surface-2)] text-[var(--text-3)]">گذشته</span>
|
<span className="text-xs px-2 py-0.5 rounded-full bg-[var(--surface-2)] text-[var(--text-3)]">گذشته</span>
|
||||||
@@ -1274,11 +1274,11 @@ function HolidaysTab({ doctorUuid, clinicUuid, readOnly = false }: { doctorUuid:
|
|||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<div className="flex items-center gap-1 shrink-0">
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
<button onClick={() => toggleMut.mutate(h)} disabled={toggleMut.isPending}
|
<button onClick={() => toggleMut.mutate(h)} disabled={toggleMut.isPending}
|
||||||
className={`text-xs px-2.5 h-7 rounded-lg border transition-colors disabled:opacity-50 ${h.active ? 'border-[var(--border)] dark:border-gray-600 text-[var(--text-2)] dark:text-[var(--text-3)] hover:border-[var(--border-2)] hover:text-[var(--text)] dark:hover:text-[var(--text)]' : 'border-emerald-200 dark:border-emerald-500/40 text-[var(--success)] dark:text-emerald-400 hover:bg-[var(--success-bg)] dark:hover:bg-emerald-500/10'}`}>
|
className={`text-xs px-2.5 h-7 rounded-lg border transition-colors disabled:opacity-50 ${h.active ? 'border-[var(--border)] text-[var(--text-2)] hover:border-[var(--border-2)] hover:text-[var(--text)]' : 'border-[var(--success)] text-[var(--success)] hover:bg-[var(--success-bg)] dark:hover:bg-[var(--success)]/10'}`}>
|
||||||
{h.active ? 'غیرفعال' : 'فعال'}
|
{h.active ? 'غیرفعال' : 'فعال'}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => { setEditing(h); setModalOpen(true); }}
|
<button onClick={() => { setEditing(h); setModalOpen(true); }}
|
||||||
className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--info)] hover:bg-[var(--info-bg)] dark:hover:bg-blue-500/10 transition-colors">
|
className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--info)] hover:bg-[var(--info-bg)] dark:hover:bg-[var(--info)]/10 transition-colors">
|
||||||
<PencilIcon className="w-3.5 h-3.5" />
|
<PencilIcon className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => setDeletingUuid(h.uuid)}
|
<button onClick={() => setDeletingUuid(h.uuid)}
|
||||||
@@ -1329,8 +1329,8 @@ export function ScheduleSection({ doctorUuid, clinicUuid, readOnly = false }: {
|
|||||||
<button key={t.id} type="button" onClick={() => setTab(t.id)}
|
<button key={t.id} type="button" onClick={() => setTab(t.id)}
|
||||||
className={`flex-1 text-xs sm:text-sm font-medium px-2 py-1.5 rounded-lg transition-colors ${
|
className={`flex-1 text-xs sm:text-sm font-medium px-2 py-1.5 rounded-lg transition-colors ${
|
||||||
tab === t.id
|
tab === t.id
|
||||||
? 'bg-[var(--surface)] dark:bg-[var(--surface-2)] text-[var(--text)] dark:text-[var(--text)] shadow-sm'
|
? 'bg-[var(--surface)] text-[var(--text)] shadow-sm'
|
||||||
: 'text-[var(--text-2)] dark:text-[var(--text-3)] hover:text-[var(--text)] dark:hover:text-[var(--text)]'
|
: 'text-[var(--text-2)] hover:text-[var(--text)]'
|
||||||
}`}>
|
}`}>
|
||||||
{t.label}
|
{t.label}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* گرادیان آواتارهای بدون عکس — تزئینی و عمداً چندرنگ، در هر دو تم یکسان
|
||||||
|
* (مثل پالت برچسبها). تنها جایی در پنل که پالت ثابت Tailwind مجاز است.
|
||||||
|
*/
|
||||||
|
export const AVATAR_GRADIENTS = [
|
||||||
|
'from-blue-500 to-cyan-500',
|
||||||
|
'from-violet-500 to-purple-600',
|
||||||
|
'from-emerald-500 to-teal-600',
|
||||||
|
'from-rose-500 to-pink-600',
|
||||||
|
'from-amber-500 to-orange-600',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** گرادیان پایدار برای یک آواتار بر اساس شناسه/اندیس. */
|
||||||
|
export const avatarGradient = (seed: number) =>
|
||||||
|
AVATAR_GRADIENTS[Math.abs(seed) % AVATAR_GRADIENTS.length];
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react';
|
||||||
|
import { avatarGradient } from '../lib/avatarColors';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
@@ -84,18 +85,13 @@ const DEGREE_OPTIONS = [
|
|||||||
{ value: 'subspecialistplus', label: 'فلوشیپ' },
|
{ value: 'subspecialistplus', label: 'فلوشیپ' },
|
||||||
];
|
];
|
||||||
const GENDER_OPTIONS = [{ value: 'man', label: 'مرد' }, { value: 'woman', label: 'زن' }];
|
const GENDER_OPTIONS = [{ value: 'man', label: 'مرد' }, { value: 'woman', label: 'زن' }];
|
||||||
const AVATAR_COLORS = [
|
|
||||||
'from-blue-500 to-cyan-500', 'from-violet-500 to-purple-600',
|
|
||||||
'from-emerald-500 to-teal-600', 'from-rose-500 to-pink-600',
|
|
||||||
'from-amber-500 to-orange-600',
|
|
||||||
];
|
|
||||||
const SPECIALTY_COLORS = [
|
const SPECIALTY_COLORS = [
|
||||||
'bg-[var(--danger-bg)] dark:bg-red-400/10 text-[var(--danger)] dark:text-red-300',
|
'bg-[var(--danger-bg)] text-[var(--danger)]',
|
||||||
'bg-[var(--info-bg)] dark:bg-blue-400/10 text-[var(--info)] dark:text-blue-300',
|
'bg-[var(--info-bg)] text-[var(--info)]',
|
||||||
'bg-emerald-100 dark:bg-emerald-400/10 text-emerald-700 dark:text-emerald-300',
|
'bg-[var(--success-bg)] text-[var(--success)]',
|
||||||
'bg-violet-100 dark:bg-violet-400/10 text-violet-700 dark:text-violet-300',
|
'bg-[var(--violet-bg)] text-[var(--violet)]',
|
||||||
'bg-orange-100 dark:bg-orange-400/10 text-orange-700 dark:text-orange-300',
|
'bg-[var(--accent-bg)] text-[var(--accent)]',
|
||||||
'bg-pink-100 dark:bg-pink-400/10 text-pink-700 dark:text-pink-300',
|
'bg-[var(--violet-bg)] text-[var(--violet)]',
|
||||||
];
|
];
|
||||||
|
|
||||||
// ── Image upload ───────────────────────────────────────────────────────────
|
// ── Image upload ───────────────────────────────────────────────────────────
|
||||||
@@ -135,8 +131,8 @@ function DoctorAvatar({ name, img, idx, onUpload, uploading }: {
|
|||||||
onClick={() => onUpload && ref.current?.click()}
|
onClick={() => onUpload && ref.current?.click()}
|
||||||
>
|
>
|
||||||
{img
|
{img
|
||||||
? <img src={img} alt={name} className="w-24 h-24 rounded-2xl object-cover shadow-xl ring-4 ring-white dark:ring-gray-900" />
|
? <img src={img} alt={name} className="w-24 h-24 rounded-2xl object-cover shadow-xl ring-4 ring-[var(--surface)]" />
|
||||||
: <div className={`w-24 h-24 rounded-2xl bg-gradient-to-br ${AVATAR_COLORS[idx % AVATAR_COLORS.length]} flex items-center justify-center text-[var(--on-primary)] text-3xl font-bold shadow-xl ring-4 ring-white dark:ring-gray-900`}>{initials}</div>
|
: <div className={`w-24 h-24 rounded-2xl bg-gradient-to-br ${avatarGradient(idx)} flex items-center justify-center text-[var(--on-primary)] text-3xl font-bold shadow-xl ring-4 ring-[var(--surface)]`}>{initials}</div>
|
||||||
}
|
}
|
||||||
{onUpload && (
|
{onUpload && (
|
||||||
<div className="absolute inset-0 rounded-2xl bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
<div className="absolute inset-0 rounded-2xl bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
||||||
@@ -161,13 +157,13 @@ function InfoCard({ icon: Icon, label, value, mono = false, copyable = false }:
|
|||||||
mono?: boolean; copyable?: boolean;
|
mono?: boolean; copyable?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start gap-3 p-4 rounded-xl bg-[var(--surface-2)] dark:bg-[var(--surface)]/60 hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] transition-colors group">
|
<div className="flex items-start gap-3 p-4 rounded-xl bg-[var(--surface-2)] hover:bg-[var(--surface-2)] transition-colors group">
|
||||||
<div className="w-9 h-9 rounded-lg bg-[var(--surface)] shadow-sm flex items-center justify-center shrink-0">
|
<div className="w-9 h-9 rounded-lg bg-[var(--surface)] shadow-sm flex items-center justify-center shrink-0">
|
||||||
<Icon className="w-4 h-4 text-[var(--text-2)]" />
|
<Icon className="w-4 h-4 text-[var(--text-2)]" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-[11px] font-medium text-[var(--text-3)] uppercase tracking-wide mb-0.5">{label}</p>
|
<p className="text-[11px] font-medium text-[var(--text-3)] uppercase tracking-wide mb-0.5">{label}</p>
|
||||||
<p className={`text-sm font-medium text-[var(--text)] dark:text-[var(--text)] ${mono ? 'font-mono' : ''}`} dir={mono ? 'ltr' : undefined}>
|
<p className={`text-sm font-medium text-[var(--text)] ${mono ? 'font-mono' : ''}`} dir={mono ? 'ltr' : undefined}>
|
||||||
{value ?? <span className="text-[var(--text-3)] font-normal">ثبت نشده</span>}
|
{value ?? <span className="text-[var(--text-3)] font-normal">ثبت نشده</span>}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -186,7 +182,7 @@ function StarRating({ rate }: { rate: number }) {
|
|||||||
<div className="flex items-center gap-1" dir="ltr">
|
<div className="flex items-center gap-1" dir="ltr">
|
||||||
{Array.from({ length: 5 }).map((_, i) =>
|
{Array.from({ length: 5 }).map((_, i) =>
|
||||||
i < Math.round(rate)
|
i < Math.round(rate)
|
||||||
? <StarSolid key={i} className="w-4 h-4 text-amber-400" />
|
? <StarSolid key={i} className="w-4 h-4 text-[var(--warning)]" />
|
||||||
: <StarIcon key={i} className="w-4 h-4 text-[var(--text-3)]" />
|
: <StarIcon key={i} className="w-4 h-4 text-[var(--text-3)]" />
|
||||||
)}
|
)}
|
||||||
<span className="text-sm font-medium text-[var(--text-2)] mr-1">{rate.toFixed(1)}</span>
|
<span className="text-sm font-medium text-[var(--text-2)] mr-1">{rate.toFixed(1)}</span>
|
||||||
@@ -306,7 +302,7 @@ function SearchableSelect({ options, value, onChange, placeholder, disabled = fa
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={openDropdown}
|
onClick={openDropdown}
|
||||||
className={`cp-input h-11 flex items-center justify-between text-right w-full ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'} ${!selected ? 'text-[var(--text-3)] dark:text-[var(--text-2)]' : ''}`}
|
className={`cp-input h-11 flex items-center justify-between text-right w-full ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'} ${!selected ? 'text-[var(--text-3)]' : ''}`}
|
||||||
>
|
>
|
||||||
<span className="truncate flex-1">{selected?.label ?? placeholder ?? 'انتخاب کنید'}</span>
|
<span className="truncate flex-1">{selected?.label ?? placeholder ?? 'انتخاب کنید'}</span>
|
||||||
<ChevronDownIcon className={`w-4 h-4 shrink-0 mr-2 transition-transform ${open ? 'rotate-180' : ''}`} />
|
<ChevronDownIcon className={`w-4 h-4 shrink-0 mr-2 transition-transform ${open ? 'rotate-180' : ''}`} />
|
||||||
@@ -328,7 +324,7 @@ function SearchableSelect({ options, value, onChange, placeholder, disabled = fa
|
|||||||
{filtered.map(o => (
|
{filtered.map(o => (
|
||||||
<button key={o.value} type="button"
|
<button key={o.value} type="button"
|
||||||
onClick={() => { onChange(o.value, o.label); setOpen(false); setQ(''); }}
|
onClick={() => { onChange(o.value, o.label); setOpen(false); setQ(''); }}
|
||||||
className={`w-full text-right px-3 py-2 text-sm hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] ${value === o.value ? 'text-primary-600 dark:text-primary-400 font-medium bg-primary-50 dark:bg-primary-500/10' : 'text-[var(--text)] dark:text-[var(--text-3)]'}`}>
|
className={`w-full text-right px-3 py-2 text-sm hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] ${value === o.value ? 'text-[var(--primary)] dark:text-[var(--primary)] font-medium bg-[var(--primary-soft)] dark:bg-[color-mix(in_srgb,var(--primary)_10%,transparent)]' : 'text-[var(--text)]'}`}>
|
||||||
{o.label}
|
{o.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -380,16 +376,16 @@ function HierarchicalSpecialtyPicker({ selected, onChange, specialties }: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border border-[var(--border)] rounded-xl overflow-hidden">
|
<div className="border border-[var(--border)] rounded-xl overflow-hidden">
|
||||||
<div className="p-2 border-b border-[var(--border)] bg-[var(--surface-2)] dark:bg-[var(--surface)]/60">
|
<div className="p-2 border-b border-[var(--border)] bg-[var(--surface-2)]">
|
||||||
<input type="text" value={q} onChange={e => setQ(e.target.value)} placeholder="جستجوی تخصص..."
|
<input type="text" value={q} onChange={e => setQ(e.target.value)} placeholder="جستجوی تخصص..."
|
||||||
className="cp-input text-sm h-8" />
|
className="cp-input text-sm h-8" />
|
||||||
</div>
|
</div>
|
||||||
{selectedLabels.length > 0 && (
|
{selectedLabels.length > 0 && (
|
||||||
<div className="px-3 py-2 border-b border-[var(--border)] flex flex-wrap gap-1">
|
<div className="px-3 py-2 border-b border-[var(--border)] flex flex-wrap gap-1">
|
||||||
{selectedLabels.map(({ id, name }) => (
|
{selectedLabels.map(({ id, name }) => (
|
||||||
<span key={id} className="inline-flex items-center gap-1 text-xs px-2.5 py-0.5 rounded-full bg-primary-100 dark:bg-primary-500/20 text-primary-700 dark:text-primary-300">
|
<span key={id} className="inline-flex items-center gap-1 text-xs px-2.5 py-0.5 rounded-full bg-[var(--primary-soft2)] dark:bg-[color-mix(in_srgb,var(--primary)_20%,transparent)] text-[var(--primary-700)] dark:text-[var(--primary)]">
|
||||||
{name}
|
{name}
|
||||||
<button type="button" onClick={() => toggleSelect(id)} className="hover:text-primary-900 dark:hover:text-primary-100 leading-none">×</button>
|
<button type="button" onClick={() => toggleSelect(id)} className="hover:text-[var(--primary-700)] dark:hover:text-[var(--primary-soft2)] leading-none">×</button>
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -400,7 +396,7 @@ function HierarchicalSpecialtyPicker({ selected, onChange, specialties }: {
|
|||||||
? filteredFlat.map(s => (
|
? filteredFlat.map(s => (
|
||||||
<label key={s.id} className="flex items-center gap-2.5 px-3 py-2 hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] cursor-pointer">
|
<label key={s.id} className="flex items-center gap-2.5 px-3 py-2 hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] cursor-pointer">
|
||||||
<input type="checkbox" checked={selected.includes(s.id)} onChange={() => toggleSelect(s.id)}
|
<input type="checkbox" checked={selected.includes(s.id)} onChange={() => toggleSelect(s.id)}
|
||||||
className="rounded border-[var(--border-2)] dark:border-gray-600 text-primary-600 shrink-0" />
|
className="rounded border-[var(--border-2)] text-[var(--primary)] shrink-0" />
|
||||||
<span className="text-sm text-[var(--text)]">{s.name}</span>
|
<span className="text-sm text-[var(--text)]">{s.name}</span>
|
||||||
{s.parent_id !== null && (
|
{s.parent_id !== null && (
|
||||||
<span className="text-[10px] text-[var(--text-3)] mr-auto truncate max-w-[100px]">
|
<span className="text-[10px] text-[var(--text-3)] mr-auto truncate max-w-[100px]">
|
||||||
@@ -424,13 +420,13 @@ function HierarchicalSpecialtyPicker({ selected, onChange, specialties }: {
|
|||||||
? <ChevronDownIcon className="w-4 h-4 text-[var(--text-3)] shrink-0" />
|
? <ChevronDownIcon className="w-4 h-4 text-[var(--text-3)] shrink-0" />
|
||||||
: <ChevronRightIcon className="w-4 h-4 text-[var(--text-3)] shrink-0" />)
|
: <ChevronRightIcon className="w-4 h-4 text-[var(--text-3)] shrink-0" />)
|
||||||
: <input type="checkbox" checked={selected.includes(parent.id)} readOnly
|
: <input type="checkbox" checked={selected.includes(parent.id)} readOnly
|
||||||
className="rounded border-[var(--border-2)] dark:border-gray-600 text-primary-600 pointer-events-none shrink-0" />
|
className="rounded border-[var(--border-2)] text-[var(--primary)] pointer-events-none shrink-0" />
|
||||||
}
|
}
|
||||||
<span className={`text-sm font-medium ${numSel > 0 ? 'text-primary-600 dark:text-primary-400' : 'text-[var(--text)] dark:text-[var(--text-3)]'}`}>
|
<span className={`text-sm font-medium ${numSel > 0 ? 'text-[var(--primary)] dark:text-[var(--primary)]' : 'text-[var(--text)]'}`}>
|
||||||
{parent.name}
|
{parent.name}
|
||||||
</span>
|
</span>
|
||||||
{numSel > 0 && (
|
{numSel > 0 && (
|
||||||
<span className="mr-auto text-[10px] bg-primary-100 dark:bg-primary-500/20 text-primary-700 dark:text-primary-300 px-1.5 py-0.5 rounded-full">
|
<span className="mr-auto text-[10px] bg-[var(--primary-soft2)] dark:bg-[color-mix(in_srgb,var(--primary)_20%,transparent)] text-[var(--primary-700)] dark:text-[var(--primary)] px-1.5 py-0.5 rounded-full">
|
||||||
{numSel} انتخاب
|
{numSel} انتخاب
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -438,7 +434,7 @@ function HierarchicalSpecialtyPicker({ selected, onChange, specialties }: {
|
|||||||
{isOpen && children.map(child => (
|
{isOpen && children.map(child => (
|
||||||
<label key={child.id} className="flex items-center gap-2.5 px-3 py-2 pr-9 hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] cursor-pointer">
|
<label key={child.id} className="flex items-center gap-2.5 px-3 py-2 pr-9 hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] cursor-pointer">
|
||||||
<input type="checkbox" checked={selected.includes(child.id)} onChange={() => toggleSelect(child.id)}
|
<input type="checkbox" checked={selected.includes(child.id)} onChange={() => toggleSelect(child.id)}
|
||||||
className="rounded border-[var(--border-2)] dark:border-gray-600 text-primary-600 shrink-0" />
|
className="rounded border-[var(--border-2)] text-[var(--primary)] shrink-0" />
|
||||||
<span className="text-sm text-[var(--text-2)]">{child.name}</span>
|
<span className="text-sm text-[var(--text-2)]">{child.name}</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
@@ -487,7 +483,7 @@ function ServicesPicker({ selected, onChange, services, specialties, selectedSpe
|
|||||||
|
|
||||||
if (selectedSpecialtyIds.length === 0) {
|
if (selectedSpecialtyIds.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-dashed border-[var(--border-2)] dark:border-gray-600 py-8 text-center text-[var(--text-3)] text-sm">
|
<div className="rounded-xl border border-dashed border-[var(--border-2)] py-8 text-center text-[var(--text-3)] text-sm">
|
||||||
ابتدا تخصصهایی را انتخاب کنید تا خدمات نمایش داده شوند
|
ابتدا تخصصهایی را انتخاب کنید تا خدمات نمایش داده شوند
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -495,7 +491,7 @@ function ServicesPicker({ selected, onChange, services, specialties, selectedSpe
|
|||||||
|
|
||||||
if (relevantServices.length === 0) {
|
if (relevantServices.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-dashed border-[var(--border-2)] dark:border-gray-600 py-8 text-center text-[var(--text-3)] text-sm">
|
<div className="rounded-xl border border-dashed border-[var(--border-2)] py-8 text-center text-[var(--text-3)] text-sm">
|
||||||
خدمتی برای تخصصهای انتخابشده یافت نشد
|
خدمتی برای تخصصهای انتخابشده یافت نشد
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -504,11 +500,11 @@ function ServicesPicker({ selected, onChange, services, specialties, selectedSpe
|
|||||||
return (
|
return (
|
||||||
<div className="border border-[var(--border)] rounded-xl overflow-hidden">
|
<div className="border border-[var(--border)] rounded-xl overflow-hidden">
|
||||||
{tabs.length > 1 && (
|
{tabs.length > 1 && (
|
||||||
<div className="flex gap-1 px-2 py-2 border-b border-[var(--border)] bg-[var(--surface-2)] dark:bg-[var(--surface)]/60 overflow-x-auto">
|
<div className="flex gap-1 px-2 py-2 border-b border-[var(--border)] bg-[var(--surface-2)] overflow-x-auto">
|
||||||
{[{ id: 'all' as const, name: 'همه' }, ...tabs].map(t => (
|
{[{ id: 'all' as const, name: 'همه' }, ...tabs].map(t => (
|
||||||
<button key={String(t.id)} type="button"
|
<button key={String(t.id)} type="button"
|
||||||
onClick={() => setActiveTab(t.id as number | 'all')}
|
onClick={() => setActiveTab(t.id as number | 'all')}
|
||||||
className={`px-2.5 py-1 rounded-lg text-xs font-medium whitespace-nowrap transition-colors ${activeTab === t.id ? 'bg-primary-600 text-[var(--on-primary)]' : 'text-[var(--text-2)] dark:text-[var(--text-3)] hover:bg-[var(--surface)] dark:hover:bg-[var(--surface-2)]'}`}>
|
className={`px-2.5 py-1 rounded-lg text-xs font-medium whitespace-nowrap transition-colors ${activeTab === t.id ? 'bg-[var(--primary)] text-[var(--on-primary)]' : 'text-[var(--text-2)] hover:bg-[var(--surface)] dark:hover:bg-[var(--surface-2)]'}`}>
|
||||||
{t.name}
|
{t.name}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -521,9 +517,9 @@ function ServicesPicker({ selected, onChange, services, specialties, selectedSpe
|
|||||||
{selectedLabels.length > 0 && (
|
{selectedLabels.length > 0 && (
|
||||||
<div className="px-3 py-2 border-b border-[var(--border)] flex flex-wrap gap-1">
|
<div className="px-3 py-2 border-b border-[var(--border)] flex flex-wrap gap-1">
|
||||||
{selectedLabels.map(({ id, name }) => (
|
{selectedLabels.map(({ id, name }) => (
|
||||||
<span key={id} className="inline-flex items-center gap-1 text-xs px-2.5 py-0.5 rounded-full bg-emerald-100 dark:bg-emerald-500/20 text-emerald-700 dark:text-emerald-300">
|
<span key={id} className="inline-flex items-center gap-1 text-xs px-2.5 py-0.5 rounded-full bg-[var(--success-bg)] text-[var(--success)]">
|
||||||
{name}
|
{name}
|
||||||
<button type="button" onClick={() => toggle(id)} className="hover:text-emerald-900 dark:hover:text-emerald-100 leading-none">×</button>
|
<button type="button" onClick={() => toggle(id)} className="hover:text-[var(--success)] dark:hover:text-[var(--success)] leading-none">×</button>
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -532,7 +528,7 @@ function ServicesPicker({ selected, onChange, services, specialties, selectedSpe
|
|||||||
{visibleServices.map(svc => (
|
{visibleServices.map(svc => (
|
||||||
<label key={svc.id} className="flex items-center gap-2.5 px-3 py-2 hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] cursor-pointer">
|
<label key={svc.id} className="flex items-center gap-2.5 px-3 py-2 hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] cursor-pointer">
|
||||||
<input type="checkbox" checked={selected.includes(svc.id)} onChange={() => toggle(svc.id)}
|
<input type="checkbox" checked={selected.includes(svc.id)} onChange={() => toggle(svc.id)}
|
||||||
className="rounded border-[var(--border-2)] dark:border-gray-600 text-[var(--success)] shrink-0" />
|
className="rounded border-[var(--border-2)] text-[var(--success)] shrink-0" />
|
||||||
<span className="text-sm text-[var(--text)]">{svc.name}</span>
|
<span className="text-sm text-[var(--text)]">{svc.name}</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
@@ -741,7 +737,7 @@ function AddressCard({ addr, onEdit, onDelete, readOnly = false }: {
|
|||||||
addr: AddressData; onEdit: () => void; onDelete: () => void; readOnly?: boolean;
|
addr: AddressData; onEdit: () => void; onDelete: () => void; readOnly?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start gap-3 p-4 rounded-xl bg-[var(--surface-2)] dark:bg-[var(--surface)]/60 border border-[var(--border)] group">
|
<div className="flex items-start gap-3 p-4 rounded-xl bg-[var(--surface-2)] border border-[var(--border)] group">
|
||||||
<div className="w-9 h-9 rounded-lg bg-[var(--surface)] shadow-sm flex items-center justify-center shrink-0 mt-0.5">
|
<div className="w-9 h-9 rounded-lg bg-[var(--surface)] shadow-sm flex items-center justify-center shrink-0 mt-0.5">
|
||||||
<MapPinIcon className="w-4 h-4 text-[var(--text-3)]" />
|
<MapPinIcon className="w-4 h-4 text-[var(--text-3)]" />
|
||||||
</div>
|
</div>
|
||||||
@@ -769,7 +765,7 @@ function AddressCard({ addr, onEdit, onDelete, readOnly = false }: {
|
|||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<div className="opacity-0 group-hover:opacity-100 flex items-center gap-1 transition-opacity shrink-0">
|
<div className="opacity-0 group-hover:opacity-100 flex items-center gap-1 transition-opacity shrink-0">
|
||||||
<button onClick={onEdit}
|
<button onClick={onEdit}
|
||||||
className="w-7 h-7 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--info)] hover:bg-[var(--info-bg)] dark:hover:bg-blue-500/10 transition-colors">
|
className="w-7 h-7 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--info)] hover:bg-[var(--info-bg)] dark:hover:bg-[var(--info)]/10 transition-colors">
|
||||||
<PencilIcon className="w-3.5 h-3.5" />
|
<PencilIcon className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<button onClick={onDelete}
|
<button onClick={onDelete}
|
||||||
@@ -1362,7 +1358,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
|||||||
if (isError || !doctor) {
|
if (isError || !doctor) {
|
||||||
return (
|
return (
|
||||||
<div className="animate-slide-up cp-card p-16 flex flex-col items-center gap-4 text-center">
|
<div className="animate-slide-up cp-card p-16 flex flex-col items-center gap-4 text-center">
|
||||||
<div className="w-16 h-16 rounded-2xl bg-[var(--danger-bg)] dark:bg-red-400/10 flex items-center justify-center">
|
<div className="w-16 h-16 rounded-2xl bg-[var(--danger-bg)] flex items-center justify-center">
|
||||||
<XCircleIcon className="w-8 h-8 text-[var(--danger)]" />
|
<XCircleIcon className="w-8 h-8 text-[var(--danger)]" />
|
||||||
</div>
|
</div>
|
||||||
<p className="font-semibold text-[var(--text)]">پزشک یافت نشد</p>
|
<p className="font-semibold text-[var(--text)]">پزشک یافت نشد</p>
|
||||||
@@ -1407,7 +1403,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
|||||||
|
|
||||||
{/* Profile Card */}
|
{/* Profile Card */}
|
||||||
<div className="cp-card overflow-hidden">
|
<div className="cp-card overflow-hidden">
|
||||||
<div className="h-28 bg-gradient-to-l from-blue-600/20 via-blue-500/10 to-transparent dark:from-blue-500/20" />
|
<div className="h-28 bg-gradient-to-l from-[color-mix(in_srgb,var(--info)_20%,transparent)] via-[color-mix(in_srgb,var(--info)_10%,transparent)] to-transparent dark:from-[color-mix(in_srgb,var(--info)_20%,transparent)]" />
|
||||||
<div className="px-6 pb-6 -mt-12 flex flex-col sm:flex-row sm:items-end gap-4">
|
<div className="px-6 pb-6 -mt-12 flex flex-col sm:flex-row sm:items-end gap-4">
|
||||||
|
|
||||||
<div className="flex flex-col items-center gap-1.5">
|
<div className="flex flex-col items-center gap-1.5">
|
||||||
@@ -1427,11 +1423,11 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 min-w-0 sm:mb-1">
|
<div className="flex-1 min-w-0 sm:mb-1">
|
||||||
<h1 className="text-xl font-bold text-[var(--text)] dark:text-slate-50">{displayDoctorName(doctor.name)}</h1>
|
<h1 className="text-xl font-bold text-[var(--text)]">{displayDoctorName(doctor.name)}</h1>
|
||||||
<div className="flex items-center gap-2 mt-1.5 flex-wrap">
|
<div className="flex items-center gap-2 mt-1.5 flex-wrap">
|
||||||
{/* Active status badge */}
|
{/* Active status badge */}
|
||||||
{doctor.active
|
{doctor.active
|
||||||
? <span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-emerald-100 dark:bg-emerald-400/10 text-emerald-700 dark:text-emerald-300">
|
? <span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-[var(--success-bg)] text-[var(--success)]">
|
||||||
<CheckCircleIcon className="w-3 h-3" />فعال
|
<CheckCircleIcon className="w-3 h-3" />فعال
|
||||||
</span>
|
</span>
|
||||||
: <span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-[var(--surface-2)] text-[var(--text-2)]">
|
: <span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-[var(--surface-2)] text-[var(--text-2)]">
|
||||||
@@ -1439,7 +1435,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
{doctor.degree && (
|
{doctor.degree && (
|
||||||
<span className="inline-flex text-xs font-medium px-2.5 py-0.5 rounded-full bg-[var(--info-bg)] dark:bg-blue-400/10 text-[var(--info)] dark:text-blue-300">{degreeLabel}</span>
|
<span className="inline-flex text-xs font-medium px-2.5 py-0.5 rounded-full bg-[var(--info-bg)] text-[var(--info)]">{degreeLabel}</span>
|
||||||
)}
|
)}
|
||||||
{doctor.gender && (
|
{doctor.gender && (
|
||||||
<span className="text-xs text-[var(--text-2)]">{doctor.gender === 'man' ? '♂ مرد' : '♀ زن'}</span>
|
<span className="text-xs text-[var(--text-2)]">{doctor.gender === 'man' ? '♂ مرد' : '♀ زن'}</span>
|
||||||
@@ -1455,7 +1451,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
|||||||
{doctor.expertise.length > 0 && (
|
{doctor.expertise.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1 mt-1">
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
{doctor.expertise.map(e => (
|
{doctor.expertise.map(e => (
|
||||||
<span key={e.id} className="text-[11px] px-2 py-0.5 rounded-full bg-[var(--surface-2)] text-[var(--text-2)] border border-[var(--border)] dark:border-gray-600">{e.name}</span>
|
<span key={e.id} className="text-[11px] px-2 py-0.5 rounded-full bg-[var(--surface-2)] text-[var(--text-2)] border border-[var(--border)]">{e.name}</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1476,8 +1472,8 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
|||||||
disabled={toggleMut.isPending}
|
disabled={toggleMut.isPending}
|
||||||
className={`inline-flex items-center gap-1.5 text-sm font-medium px-3 h-10 rounded-xl border transition-colors disabled:opacity-50 ${
|
className={`inline-flex items-center gap-1.5 text-sm font-medium px-3 h-10 rounded-xl border transition-colors disabled:opacity-50 ${
|
||||||
doctor.active
|
doctor.active
|
||||||
? 'border-orange-300 dark:border-orange-500/40 text-orange-600 dark:text-orange-400 bg-orange-50 dark:bg-orange-500/10 hover:bg-orange-100 dark:hover:bg-orange-500/20'
|
? 'border-[var(--accent)] text-[var(--accent)] bg-[var(--accent-bg)] hover:bg-[var(--accent-bg)]'
|
||||||
: 'border-emerald-300 dark:border-emerald-500/40 text-[var(--success)] dark:text-emerald-400 bg-[var(--success-bg)] dark:bg-emerald-500/10 hover:bg-emerald-100 dark:hover:bg-emerald-500/20'
|
: 'border-[var(--success)] text-[var(--success)] bg-[var(--success-bg)] hover:bg-[var(--success-bg)]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{toggleMut.isPending
|
{toggleMut.isPending
|
||||||
@@ -1497,7 +1493,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
|||||||
{menuOpen && (
|
{menuOpen && (
|
||||||
<div className="absolute left-0 top-12 z-30 w-52 cp-card shadow-2xl border border-[var(--border)] py-1.5 animate-scale-in" data-dmenu>
|
<div className="absolute left-0 top-12 z-30 w-52 cp-card shadow-2xl border border-[var(--border)] py-1.5 animate-scale-in" data-dmenu>
|
||||||
<button onClick={() => { setDeleteOpen(true); setMenuOpen(false); }}
|
<button onClick={() => { setDeleteOpen(true); setMenuOpen(false); }}
|
||||||
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-[var(--danger)] dark:text-red-400 hover:bg-[var(--danger-bg)] dark:hover:bg-[var(--danger)]/10">
|
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-[var(--danger)] hover:bg-[var(--danger-bg)] dark:hover:bg-[var(--danger)]/10">
|
||||||
<TrashIcon className="w-4 h-4 shrink-0" />حذف این پزشک
|
<TrashIcon className="w-4 h-4 shrink-0" />حذف این پزشک
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1556,7 +1552,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
|||||||
</h2>
|
</h2>
|
||||||
{!isReadOnly && (
|
{!isReadOnly && (
|
||||||
<button onClick={() => { setEditingAddr(null); setAddrModalOpen(true); }}
|
<button onClick={() => { setEditingAddr(null); setAddrModalOpen(true); }}
|
||||||
className="flex items-center gap-1.5 text-xs font-medium text-primary-600 dark:text-primary-400 hover:underline">
|
className="flex items-center gap-1.5 text-xs font-medium text-[var(--primary)] dark:text-[var(--primary)] hover:underline">
|
||||||
<PlusIcon className="w-4 h-4" />افزودن آدرس
|
<PlusIcon className="w-4 h-4" />افزودن آدرس
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -1576,7 +1572,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
|||||||
<p className="text-sm">هنوز آدرسی ثبت نشده است</p>
|
<p className="text-sm">هنوز آدرسی ثبت نشده است</p>
|
||||||
{!isReadOnly && (
|
{!isReadOnly && (
|
||||||
<button onClick={() => { setEditingAddr(null); setAddrModalOpen(true); }}
|
<button onClick={() => { setEditingAddr(null); setAddrModalOpen(true); }}
|
||||||
className="mt-3 text-xs text-primary-600 dark:text-primary-400 hover:underline">
|
className="mt-3 text-xs text-[var(--primary)] dark:text-[var(--primary)] hover:underline">
|
||||||
اولین آدرس را اضافه کنید
|
اولین آدرس را اضافه کنید
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -1595,7 +1591,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
|||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{doctor.clinics.map(c => (
|
{doctor.clinics.map(c => (
|
||||||
<div key={c.uuid} className="flex items-start gap-3 p-3 rounded-xl bg-[var(--surface-2)] dark:bg-[var(--surface)]/60">
|
<div key={c.uuid} className="flex items-start gap-3 p-3 rounded-xl bg-[var(--surface-2)]">
|
||||||
<div className="w-8 h-8 rounded-lg bg-[var(--surface)] shadow-sm flex items-center justify-center shrink-0">
|
<div className="w-8 h-8 rounded-lg bg-[var(--surface)] shadow-sm flex items-center justify-center shrink-0">
|
||||||
<BuildingOfficeIcon className="w-4 h-4 text-[var(--text-3)]" />
|
<BuildingOfficeIcon className="w-4 h-4 text-[var(--text-3)]" />
|
||||||
</div>
|
</div>
|
||||||
@@ -1625,7 +1621,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
|||||||
: <p className="text-sm text-[var(--text-3)]">تخصصی ثبت نشده</p>
|
: <p className="text-sm text-[var(--text-3)]">تخصصی ثبت نشده</p>
|
||||||
}
|
}
|
||||||
{!isReadOnly && (
|
{!isReadOnly && (
|
||||||
<button onClick={() => setEditOpen(true)} className="w-full mt-3 text-xs text-primary-600 dark:text-primary-400 hover:underline text-right">
|
<button onClick={() => setEditOpen(true)} className="w-full mt-3 text-xs text-[var(--primary)] dark:text-[var(--primary)] hover:underline text-right">
|
||||||
ویرایش تخصصها ←
|
ویرایش تخصصها ←
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -1644,7 +1640,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
|||||||
: <p className="text-sm text-[var(--text-3)]">خدمتی ثبت نشده</p>
|
: <p className="text-sm text-[var(--text-3)]">خدمتی ثبت نشده</p>
|
||||||
}
|
}
|
||||||
{!isReadOnly && (
|
{!isReadOnly && (
|
||||||
<button onClick={() => setEditOpen(true)} className="w-full mt-3 text-xs text-primary-600 dark:text-primary-400 hover:underline text-right">
|
<button onClick={() => setEditOpen(true)} className="w-full mt-3 text-xs text-[var(--primary)] dark:text-[var(--primary)] hover:underline text-right">
|
||||||
ویرایش خدمات ←
|
ویرایش خدمات ←
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -1661,7 +1657,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
|||||||
<p className="text-xs text-[var(--text-2)] mb-1">درصد رضایت</p>
|
<p className="text-xs text-[var(--text-2)] mb-1">درصد رضایت</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex-1 h-2 rounded-full bg-[var(--surface-3)]">
|
<div className="flex-1 h-2 rounded-full bg-[var(--surface-3)]">
|
||||||
<div className="h-full rounded-full bg-amber-400 transition-all" style={{ width: `${doctor.satisfaction}%` }} />
|
<div className="h-full rounded-full bg-[var(--warning)] transition-all" style={{ width: `${doctor.satisfaction}%` }} />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium text-[var(--text)]">{doctor.satisfaction}%</span>
|
<span className="text-sm font-medium text-[var(--text)]">{doctor.satisfaction}%</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { avatarGradient } from '../lib/avatarColors';
|
||||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
@@ -71,11 +72,11 @@ const MEDICAL_SECTIONS: {
|
|||||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const ROLE_META: Record<string, { label: string; cls: string; dot: string }> = {
|
const ROLE_META: Record<string, { label: string; cls: string; dot: string }> = {
|
||||||
admin: { label: 'ادمین', cls: 'bg-violet-100 dark:bg-violet-400/10 text-violet-700 dark:text-violet-300', dot: '#8b5cf6' },
|
admin: { label: 'ادمین', cls: 'bg-[var(--violet-bg)] text-[var(--violet)]', dot: 'var(--violet)' },
|
||||||
doctor: { label: 'پزشک', cls: 'bg-[var(--info-bg)] dark:bg-blue-400/10 text-[var(--info)] dark:text-blue-300', dot: '#3b82f6' },
|
doctor: { label: 'پزشک', cls: 'bg-[var(--info-bg)] text-[var(--info)]', dot: 'var(--info)' },
|
||||||
secretary: { label: 'منشی', cls: 'bg-orange-100 dark:bg-orange-400/10 text-orange-700 dark:text-orange-300', dot: '#f97316' },
|
secretary: { label: 'منشی', cls: 'bg-[var(--accent-bg)] text-[var(--accent)]', dot: 'var(--accent)' },
|
||||||
clinic: { label: 'کلینیک', cls: 'bg-emerald-100 dark:bg-emerald-400/10 text-emerald-700 dark:text-emerald-300', dot: '#10b981' },
|
clinic: { label: 'کلینیک', cls: 'bg-[var(--success-bg)] text-[var(--success)]', dot: 'var(--success)' },
|
||||||
patient: { label: 'بیمار', cls: 'bg-[var(--surface-2)] dark:bg-slate-400/10 text-[var(--text-2)] dark:text-[var(--text-3)]', dot: '#94a3b8' },
|
patient: { label: 'بیمار', cls: 'bg-[var(--surface-2)] text-[var(--text-2)]', dot: 'var(--text-3)' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const ROLE_MAP: Record<string, string[]> = {
|
const ROLE_MAP: Record<string, string[]> = {
|
||||||
@@ -94,20 +95,13 @@ function getPrimaryRole(roles: string[]): string {
|
|||||||
return 'patient';
|
return 'patient';
|
||||||
}
|
}
|
||||||
|
|
||||||
const AVATAR_COLORS = [
|
|
||||||
'from-violet-500 to-purple-600',
|
|
||||||
'from-blue-500 to-cyan-600',
|
|
||||||
'from-emerald-500 to-teal-600',
|
|
||||||
'from-pink-500 to-rose-600',
|
|
||||||
'from-amber-500 to-orange-600',
|
|
||||||
];
|
|
||||||
|
|
||||||
function BigAvatar({ name, id }: { name: string | null; id: number }) {
|
function BigAvatar({ name, id }: { name: string | null; id: number }) {
|
||||||
const initials = name
|
const initials = name
|
||||||
? name.split(' ').filter(Boolean).map(w => w[0]).join('').toUpperCase().slice(0, 2)
|
? name.split(' ').filter(Boolean).map(w => w[0]).join('').toUpperCase().slice(0, 2)
|
||||||
: '؟';
|
: '؟';
|
||||||
return (
|
return (
|
||||||
<div className={`w-20 h-20 rounded-2xl bg-gradient-to-br ${AVATAR_COLORS[id % AVATAR_COLORS.length]} flex items-center justify-center text-[var(--on-primary)] text-2xl font-bold shadow-lg shrink-0`}>
|
<div className={`w-20 h-20 rounded-2xl bg-gradient-to-br ${avatarGradient(id)} flex items-center justify-center text-[var(--on-primary)] text-2xl font-bold shadow-lg shrink-0`}>
|
||||||
{initials}
|
{initials}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -124,13 +118,13 @@ function InfoCard({ icon: Icon, label, value, mono = false, copyable = false }:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start gap-3 p-4 rounded-xl bg-[var(--surface-2)] dark:bg-[var(--surface)]/60 hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] transition-colors group">
|
<div className="flex items-start gap-3 p-4 rounded-xl bg-[var(--surface-2)] hover:bg-[var(--surface-2)] transition-colors group">
|
||||||
<div className="w-9 h-9 rounded-lg bg-[var(--surface)] shadow-sm flex items-center justify-center shrink-0">
|
<div className="w-9 h-9 rounded-lg bg-[var(--surface)] shadow-sm flex items-center justify-center shrink-0">
|
||||||
<Icon className="w-4.5 h-4.5 text-[var(--text-2)]" />
|
<Icon className="w-4.5 h-4.5 text-[var(--text-2)]" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-[11px] font-medium text-[var(--text-3)] uppercase tracking-wide mb-0.5">{label}</p>
|
<p className="text-[11px] font-medium text-[var(--text-3)] uppercase tracking-wide mb-0.5">{label}</p>
|
||||||
<p className={`text-sm font-medium text-[var(--text)] dark:text-[var(--text)] truncate ${mono ? 'font-mono' : ''}`}
|
<p className={`text-sm font-medium text-[var(--text)] truncate ${mono ? 'font-mono' : ''}`}
|
||||||
dir={mono ? 'ltr' : undefined}>
|
dir={mono ? 'ltr' : undefined}>
|
||||||
{value ?? <span className="text-[var(--text-3)] font-normal">ثبت نشده</span>}
|
{value ?? <span className="text-[var(--text-3)] font-normal">ثبت نشده</span>}
|
||||||
</p>
|
</p>
|
||||||
@@ -138,7 +132,7 @@ function InfoCard({ icon: Icon, label, value, mono = false, copyable = false }:
|
|||||||
{copyable && typeof value === 'string' && (
|
{copyable && typeof value === 'string' && (
|
||||||
<button
|
<button
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
className="opacity-0 group-hover:opacity-100 w-7 h-7 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--text-2)] dark:hover:text-[var(--text-3)] hover:bg-[var(--surface)] dark:hover:bg-[var(--surface-2)] transition-all shrink-0"
|
className="opacity-0 group-hover:opacity-100 w-7 h-7 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--text-2)] hover:bg-[var(--surface)] dark:hover:bg-[var(--surface-2)] transition-all shrink-0"
|
||||||
>
|
>
|
||||||
<ClipboardDocumentIcon className="w-3.5 h-3.5" />
|
<ClipboardDocumentIcon className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
@@ -180,15 +174,15 @@ function ChangeRoleModal({ current, loading, onSave, onClose }: {
|
|||||||
key={key}
|
key={key}
|
||||||
className={`flex items-center gap-3 p-3 rounded-xl cursor-pointer border-2 transition-all ${
|
className={`flex items-center gap-3 p-3 rounded-xl cursor-pointer border-2 transition-all ${
|
||||||
selected === key
|
selected === key
|
||||||
? 'border-primary-500 bg-primary-50 dark:bg-primary-500/10'
|
? 'border-[var(--primary)] bg-[var(--primary-soft)] dark:bg-[color-mix(in_srgb,var(--primary)_10%,transparent)]'
|
||||||
: 'border-transparent hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] border border-[var(--border)] dark:border-[var(--border)]'
|
: 'border-transparent hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] border border-[var(--border)]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<input type="radio" name="role" value={key} checked={selected === key}
|
<input type="radio" name="role" value={key} checked={selected === key}
|
||||||
onChange={() => setSelected(key)} className="sr-only" />
|
onChange={() => setSelected(key)} className="sr-only" />
|
||||||
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: meta.dot }} />
|
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: meta.dot }} />
|
||||||
<span className="text-sm font-medium text-[var(--text)] flex-1">{meta.label}</span>
|
<span className="text-sm font-medium text-[var(--text)] flex-1">{meta.label}</span>
|
||||||
{selected === key && <CheckCircleIcon className="w-4 h-4 text-primary-600 dark:text-primary-400" />}
|
{selected === key && <CheckCircleIcon className="w-4 h-4 text-[var(--primary)] dark:text-[var(--primary)]" />}
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -320,7 +314,7 @@ export default function UserDetailPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="animate-slide-up">
|
<div className="animate-slide-up">
|
||||||
<div className="cp-card p-16 flex flex-col items-center gap-4 text-center">
|
<div className="cp-card p-16 flex flex-col items-center gap-4 text-center">
|
||||||
<div className="w-16 h-16 rounded-2xl bg-[var(--danger-bg)] dark:bg-red-400/10 flex items-center justify-center">
|
<div className="w-16 h-16 rounded-2xl bg-[var(--danger-bg)] flex items-center justify-center">
|
||||||
<XCircleIcon className="w-8 h-8 text-[var(--danger)]" />
|
<XCircleIcon className="w-8 h-8 text-[var(--danger)]" />
|
||||||
</div>
|
</div>
|
||||||
<p className="font-semibold text-[var(--text)]">کاربر یافت نشد</p>
|
<p className="font-semibold text-[var(--text)]">کاربر یافت نشد</p>
|
||||||
@@ -356,14 +350,14 @@ export default function UserDetailPage() {
|
|||||||
<div className="cp-card overflow-hidden">
|
<div className="cp-card overflow-hidden">
|
||||||
|
|
||||||
{/* Cover strip */}
|
{/* Cover strip */}
|
||||||
<div className="h-24 bg-gradient-to-l from-primary-600/20 via-primary-500/10 to-transparent dark:from-primary-500/20" />
|
<div className="h-24 bg-gradient-to-l from-[color-mix(in_srgb,var(--primary)_20%,transparent)] via-[color-mix(in_srgb,var(--primary)_10%,transparent)] to-transparent dark:from-[color-mix(in_srgb,var(--primary)_20%,transparent)]" />
|
||||||
|
|
||||||
{/* Profile header */}
|
{/* Profile header */}
|
||||||
<div className="px-6 pb-6 -mt-10 flex flex-col sm:flex-row sm:items-end gap-4">
|
<div className="px-6 pb-6 -mt-10 flex flex-col sm:flex-row sm:items-end gap-4">
|
||||||
<BigAvatar name={user.name} id={user.id} />
|
<BigAvatar name={user.name} id={user.id} />
|
||||||
|
|
||||||
<div className="flex-1 min-w-0 sm:mb-1">
|
<div className="flex-1 min-w-0 sm:mb-1">
|
||||||
<h1 className="text-xl font-bold text-[var(--text)] dark:text-slate-50 leading-tight">
|
<h1 className="text-xl font-bold text-[var(--text)] leading-tight">
|
||||||
{user.name || <span className="text-[var(--text-3)] font-normal">نام ثبت نشده</span>}
|
{user.name || <span className="text-[var(--text-3)] font-normal">نام ثبت نشده</span>}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex items-center gap-2 mt-1.5 flex-wrap">
|
<div className="flex items-center gap-2 mt-1.5 flex-wrap">
|
||||||
@@ -372,12 +366,12 @@ export default function UserDetailPage() {
|
|||||||
{roleMeta.label}
|
{roleMeta.label}
|
||||||
</span>
|
</span>
|
||||||
{user.is_active ? (
|
{user.is_active ? (
|
||||||
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-[var(--success-bg)] dark:bg-green-400/10 text-[var(--success)] dark:text-green-300">
|
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-[var(--success-bg)] text-[var(--success)]">
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />فعال
|
<span className="w-1.5 h-1.5 rounded-full bg-[var(--success)] animate-pulse" />فعال
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-[var(--surface-2)] dark:bg-slate-400/10 text-[var(--text-2)]">
|
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-[var(--surface-2)] text-[var(--text-2)]">
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-slate-400" />غیرفعال
|
<span className="w-1.5 h-1.5 rounded-full bg-[var(--text-3)]" />غیرفعال
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="text-xs text-[var(--text-3)]">#{user.id}</span>
|
<span className="text-xs text-[var(--text-3)]">#{user.id}</span>
|
||||||
@@ -411,7 +405,7 @@ export default function UserDetailPage() {
|
|||||||
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-[var(--text)] hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] transition-colors disabled:opacity-50"
|
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-[var(--text)] hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{user.is_active
|
{user.is_active
|
||||||
? <XCircleIcon className="w-4 h-4 text-orange-400 shrink-0" />
|
? <XCircleIcon className="w-4 h-4 text-[var(--accent)] shrink-0" />
|
||||||
: <CheckCircleIcon className="w-4 h-4 text-[var(--success)] shrink-0" />
|
: <CheckCircleIcon className="w-4 h-4 text-[var(--success)] shrink-0" />
|
||||||
}
|
}
|
||||||
{user.is_active ? 'غیرفعال کردن حساب' : 'فعالسازی حساب'}
|
{user.is_active ? 'غیرفعال کردن حساب' : 'فعالسازی حساب'}
|
||||||
@@ -420,13 +414,13 @@ export default function UserDetailPage() {
|
|||||||
onClick={() => { setRoleOpen(true); setMenuOpen(false); }}
|
onClick={() => { setRoleOpen(true); setMenuOpen(false); }}
|
||||||
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-[var(--text)] hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] transition-colors"
|
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-[var(--text)] hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] transition-colors"
|
||||||
>
|
>
|
||||||
<ShieldCheckIcon className="w-4 h-4 text-violet-400 shrink-0" />
|
<ShieldCheckIcon className="w-4 h-4 text-[var(--violet)] shrink-0" />
|
||||||
تغییر نقش کاربر
|
تغییر نقش کاربر
|
||||||
</button>
|
</button>
|
||||||
<div className="border-t border-[var(--border)] my-1" />
|
<div className="border-t border-[var(--border)] my-1" />
|
||||||
<button
|
<button
|
||||||
onClick={() => { setDeleteOpen(true); setMenuOpen(false); }}
|
onClick={() => { setDeleteOpen(true); setMenuOpen(false); }}
|
||||||
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-[var(--danger)] dark:text-red-400 hover:bg-[var(--danger-bg)] dark:hover:bg-[var(--danger)]/10 transition-colors"
|
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-[var(--danger)] hover:bg-[var(--danger-bg)] dark:hover:bg-[var(--danger)]/10 transition-colors"
|
||||||
>
|
>
|
||||||
<TrashIcon className="w-4 h-4 shrink-0" />
|
<TrashIcon className="w-4 h-4 shrink-0" />
|
||||||
حذف این کاربر
|
حذف این کاربر
|
||||||
@@ -469,7 +463,7 @@ export default function UserDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setRoleOpen(true)}
|
onClick={() => setRoleOpen(true)}
|
||||||
className="w-full mt-1 text-xs text-primary-600 dark:text-primary-400 hover:underline text-right"
|
className="w-full mt-1 text-xs text-[var(--primary)] dark:text-[var(--primary)] hover:underline text-right"
|
||||||
>
|
>
|
||||||
تغییر نقش ←
|
تغییر نقش ←
|
||||||
</button>
|
</button>
|
||||||
@@ -484,7 +478,7 @@ export default function UserDetailPage() {
|
|||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => { navigator.clipboard.writeText(user.uuid); toast.success('UUID کپی شد'); }}
|
onClick={() => { navigator.clipboard.writeText(user.uuid); toast.success('UUID کپی شد'); }}
|
||||||
className="w-7 h-7 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--text-2)] dark:hover:text-[var(--text-3)] hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface-2)] transition-colors shrink-0"
|
className="w-7 h-7 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--text-2)] hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface-2)] transition-colors shrink-0"
|
||||||
>
|
>
|
||||||
<ClipboardDocumentIcon className="w-3.5 h-3.5" />
|
<ClipboardDocumentIcon className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
@@ -496,20 +490,20 @@ export default function UserDetailPage() {
|
|||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h2 className="text-sm font-semibold text-[var(--text)]">وضعیت حساب</h2>
|
<h2 className="text-sm font-semibold text-[var(--text)]">وضعیت حساب</h2>
|
||||||
{user.is_active
|
{user.is_active
|
||||||
? <span className="text-xs font-medium text-[var(--success)] dark:text-green-400">فعال</span>
|
? <span className="text-xs font-medium text-[var(--success)]">فعال</span>
|
||||||
: <span className="text-xs font-medium text-[var(--text-2)]">غیرفعال</span>
|
: <span className="text-xs font-medium text-[var(--text-2)]">غیرفعال</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className={`w-full h-2 rounded-full ${user.is_active ? 'bg-green-200 dark:bg-green-400/20' : 'bg-[var(--surface-3)] dark:bg-[var(--surface-2)]'}`}>
|
<div className={`w-full h-2 rounded-full ${user.is_active ? 'bg-[var(--success-bg)]' : 'bg-[var(--surface-3)]'}`}>
|
||||||
<div className={`h-full rounded-full transition-all ${user.is_active ? 'w-full bg-green-500' : 'w-0'}`} />
|
<div className={`h-full rounded-full transition-all ${user.is_active ? 'w-full bg-[var(--success)]' : 'w-0'}`} />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleStatusMut.mutate()}
|
onClick={() => toggleStatusMut.mutate()}
|
||||||
disabled={toggleStatusMut.isPending}
|
disabled={toggleStatusMut.isPending}
|
||||||
className={`w-full mt-4 text-sm font-medium py-2 rounded-xl transition-colors disabled:opacity-50 ${
|
className={`w-full mt-4 text-sm font-medium py-2 rounded-xl transition-colors disabled:opacity-50 ${
|
||||||
user.is_active
|
user.is_active
|
||||||
? 'bg-orange-50 dark:bg-orange-400/10 text-orange-600 dark:text-orange-400 hover:bg-orange-100 dark:hover:bg-orange-400/20'
|
? 'bg-[var(--accent-bg)] text-[var(--accent)] hover:bg-[var(--accent-bg)]'
|
||||||
: 'bg-[var(--success-bg)] dark:bg-green-400/10 text-[var(--success)] dark:text-green-400 hover:bg-[var(--success-bg)] dark:hover:bg-green-400/20'
|
: 'bg-[var(--success-bg)] text-[var(--success)] hover:bg-[var(--success-bg)]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{toggleStatusMut.isPending
|
{toggleStatusMut.isPending
|
||||||
@@ -561,14 +555,14 @@ export default function UserDetailPage() {
|
|||||||
.map(format)
|
.map(format)
|
||||||
.filter((s) => s && s.trim());
|
.filter((s) => s && s.trim());
|
||||||
return (
|
return (
|
||||||
<div key={key} className="p-4 rounded-xl bg-[var(--surface-2)] dark:bg-[var(--surface)]/60">
|
<div key={key} className="p-4 rounded-xl bg-[var(--surface-2)]">
|
||||||
<p className="text-[11px] font-medium text-[var(--text-3)] uppercase tracking-wide mb-1.5">{label}</p>
|
<p className="text-[11px] font-medium text-[var(--text-3)] uppercase tracking-wide mb-1.5">{label}</p>
|
||||||
{items.length === 0 ? (
|
{items.length === 0 ? (
|
||||||
<p className="text-sm text-[var(--text-3)]">موردی ثبت نشده</p>
|
<p className="text-sm text-[var(--text-3)]">موردی ثبت نشده</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="flex flex-wrap gap-1.5">
|
<ul className="flex flex-wrap gap-1.5">
|
||||||
{items.map((text, i) => (
|
{items.map((text, i) => (
|
||||||
<li key={i} className="text-xs px-2 py-0.5 rounded-lg bg-[var(--surface)] text-[var(--text-2)] border border-[var(--border)] dark:border-gray-600">
|
<li key={i} className="text-xs px-2 py-0.5 rounded-lg bg-[var(--surface)] text-[var(--text-2)] border border-[var(--border)]">
|
||||||
{text}
|
{text}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -624,10 +618,10 @@ export default function UserDetailPage() {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="مثال: علی احمدی"
|
placeholder="مثال: علی احمدی"
|
||||||
className={`cp-input ${errors.name ? 'border-red-400 dark:border-red-500' : ''}`}
|
className={`cp-input ${errors.name ? 'border-[var(--danger)]' : ''}`}
|
||||||
{...register('name')}
|
{...register('name')}
|
||||||
/>
|
/>
|
||||||
{errors.name && <p className="text-xs text-[var(--danger)] dark:text-red-400 mt-1">{errors.name.message}</p>}
|
{errors.name && <p className="text-xs text-[var(--danger)] mt-1">{errors.name.message}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Email */}
|
{/* Email */}
|
||||||
@@ -639,10 +633,10 @@ export default function UserDetailPage() {
|
|||||||
type="email"
|
type="email"
|
||||||
placeholder="example@email.com"
|
placeholder="example@email.com"
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
className={`cp-input text-left ${errors.email ? 'border-red-400 dark:border-red-500' : ''}`}
|
className={`cp-input text-left ${errors.email ? 'border-[var(--danger)]' : ''}`}
|
||||||
{...register('email')}
|
{...register('email')}
|
||||||
/>
|
/>
|
||||||
{errors.email && <p className="text-xs text-[var(--danger)] dark:text-red-400 mt-1">{errors.email.message}</p>}
|
{errors.email && <p className="text-xs text-[var(--danger)] mt-1">{errors.email.message}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Password */}
|
{/* Password */}
|
||||||
@@ -657,11 +651,11 @@ export default function UserDetailPage() {
|
|||||||
type="password"
|
type="password"
|
||||||
placeholder="حداقل ۶ کاراکتر"
|
placeholder="حداقل ۶ کاراکتر"
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
className={`cp-input pr-9 text-left ${errors.password ? 'border-red-400 dark:border-red-500' : ''}`}
|
className={`cp-input pr-9 text-left ${errors.password ? 'border-[var(--danger)]' : ''}`}
|
||||||
{...register('password')}
|
{...register('password')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{errors.password && <p className="text-xs text-[var(--danger)] dark:text-red-400 mt-1">{errors.password.message}</p>}
|
{errors.password && <p className="text-xs text-[var(--danger)] mt-1">{errors.password.message}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile (read-only) */}
|
{/* Mobile (read-only) */}
|
||||||
@@ -675,7 +669,7 @@ export default function UserDetailPage() {
|
|||||||
value={user.mobile_number}
|
value={user.mobile_number}
|
||||||
readOnly
|
readOnly
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
className="cp-input text-left bg-[var(--surface-2)] dark:bg-[var(--surface)]/60 text-[var(--text-3)] cursor-not-allowed"
|
className="cp-input text-left bg-[var(--surface-2)] text-[var(--text-3)] cursor-not-allowed"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -9,18 +9,11 @@ import { describe, expect, it } from 'vitest';
|
|||||||
|
|
||||||
const ROOT = join(__dirname, '..');
|
const ROOT = join(__dirname, '..');
|
||||||
|
|
||||||
/** فاز ۳ — هنوز جفت slate/gray + dark: دارند؛ کار میکنند ولی از design-system منحرفاند. */
|
|
||||||
const DEFERRED = [
|
|
||||||
'pages/DoctorDetailPage.tsx',
|
|
||||||
'pages/UserDetailPage.tsx',
|
|
||||||
'components/schedule/ScheduleSection.tsx',
|
|
||||||
];
|
|
||||||
|
|
||||||
/** رنگهای عمداً ثابت: دادهٔ کاربر یا پوششی که در هر دو تم یکسان است. */
|
/** رنگهای عمداً ثابت: دادهٔ کاربر یا پوششی که در هر دو تم یکسان است. */
|
||||||
const INTENTIONAL = [
|
const INTENTIONAL = [
|
||||||
'pages/TagsSettingsPage.tsx', // TAG_COLORS — پالت انتخابی کاربر برای برچسب
|
'pages/TagsSettingsPage.tsx', // TAG_COLORS — پالت انتخابی کاربر برای برچسب
|
||||||
'components/ImageCropModal.tsx', // bg-black/50 — اسکریم مودال، مثل .overlay
|
|
||||||
'stores/uiStore.ts', // BrandFixed — هیوی نارنجی عمداً hex ثابت است
|
'stores/uiStore.ts', // BrandFixed — هیوی نارنجی عمداً hex ثابت است
|
||||||
|
'lib/avatarColors.ts', // گرادیان تزئینی آواتار، در هر دو تم یکسان
|
||||||
];
|
];
|
||||||
|
|
||||||
const HEX = /#[0-9a-fA-F]{3,8}\b/;
|
const HEX = /#[0-9a-fA-F]{3,8}\b/;
|
||||||
@@ -42,12 +35,16 @@ function sourceFiles(): string[] {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** کامنتها و fallbackهای `var(--x, #hex)` رنگِ رندرشده نیستند. */
|
/**
|
||||||
|
* کامنتها و fallbackهای `var(--x, #hex)` رنگِ رندرشده نیستند؛ `bg-black/NN` هم
|
||||||
|
* اسکریم است و عمداً در هر دو تم یکسان میماند (مثل `.overlay` در styles.css).
|
||||||
|
*/
|
||||||
function strip(src: string): string {
|
function strip(src: string): string {
|
||||||
return src
|
return src
|
||||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||||
.replace(/(^|[^:])\/\/.*$/gm, '$1')
|
.replace(/(^|[^:])\/\/.*$/gm, '$1')
|
||||||
.replace(/var\(\s*--[a-z0-9-]+\s*,[^)]*\)/gi, 'var(--token)');
|
.replace(/var\(\s*--[a-z0-9-]+\s*,[^)]*\)/gi, 'var(--token)')
|
||||||
|
.replace(/\bbg-black\/\d+/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('theme tokens', () => {
|
describe('theme tokens', () => {
|
||||||
@@ -69,14 +66,14 @@ describe('theme tokens', () => {
|
|||||||
|
|
||||||
it('فایلهای پاکشده hex ثابت ندارند', () => {
|
it('فایلهای پاکشده hex ثابت ندارند', () => {
|
||||||
const offenders = files
|
const offenders = files
|
||||||
.filter((f) => ![...DEFERRED, ...INTENTIONAL].includes(f))
|
.filter((f) => !INTENTIONAL.includes(f))
|
||||||
.filter((f) => HEX.test(strip(readFileSync(join(ROOT, f), 'utf8'))));
|
.filter((f) => HEX.test(strip(readFileSync(join(ROOT, f), 'utf8'))));
|
||||||
expect(offenders).toEqual([]);
|
expect(offenders).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('فایلهای پاکشده کلاس پالت Tailwind ندارند', () => {
|
it('فایلهای پاکشده کلاس پالت Tailwind ندارند', () => {
|
||||||
const offenders = files
|
const offenders = files
|
||||||
.filter((f) => ![...DEFERRED, ...INTENTIONAL].includes(f))
|
.filter((f) => !INTENTIONAL.includes(f))
|
||||||
.filter((f) => PALETTE_UTIL.test(strip(readFileSync(join(ROOT, f), 'utf8'))));
|
.filter((f) => PALETTE_UTIL.test(strip(readFileSync(join(ROOT, f), 'utf8'))));
|
||||||
expect(offenders).toEqual([]);
|
expect(offenders).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -478,9 +478,13 @@ Row 3: [Bar Chart — آمار ماهانه نمایندگان (60%)] | [لیس
|
|||||||
رنگ را از والد بگیر، یا `style={{ stroke: 'var(--…)' }}`.
|
رنگ را از والد بگیر، یا `style={{ stroke: 'var(--…)' }}`.
|
||||||
- الفای هگزی (`${color}30`) روی توکن بیمعنی است — `color-mix(in srgb, var(--x) 19%, transparent)`.
|
- الفای هگزی (`${color}30`) روی توکن بیمعنی است — `color-mix(in srgb, var(--x) 19%, transparent)`.
|
||||||
|
|
||||||
فاز باقیمانده: `pages/DoctorDetailPage.tsx`، `pages/UserDetailPage.tsx` و
|
کل `assets/admin` مهاجرت کرده است؛ تست گارد `DEFERRED` ندارد و فقط سه استثنای عمدی
|
||||||
`components/schedule/ScheduleSection.tsx` هنوز جفت `slate/gray` + `dark:` دارند
|
دارد: پالت برچسبها، `BrandFixed` در `uiStore`، و گرادیان تزئینی آواتار
|
||||||
(کار میکنند ولی با brand hue هماهنگ نمیشوند) — در تست گارد بهعنوان `DEFERRED` علامت خوردهاند.
|
(`lib/avatarColors.ts`). اسکریم `bg-black/NN` هم مجاز است.
|
||||||
|
|
||||||
|
> نکتهٔ تاریخی: کلاسهایی مثل `text-primary-600` / `bg-primary-100` هیچوقت رنگی تولید
|
||||||
|
> نمیکردند — بلوک `@theme` فقط `--font-sans` را تعریف میکند و مقیاس `primary-*` در
|
||||||
|
> Tailwind وجود ندارد. ۴۳ نمونه از این کلاسهای مرده در آدیت به توکن تبدیل شدند.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user