Implement comprehensive dark/light mode overhaul for admin panel
- Refactor color palette in `ui-design-spec.md` to utilize CSS variables exclusively, eliminating fixed hex values and Tailwind utility classes. - Complete dark mode implementation in `uiStore.ts`, ensuring proper theme application via `applyTheme()` and `applyBrand()`. - Create `admin-theme-dark-light-audit.md` to document the transition process, outlining issues with inline styles and fixed colors. - Introduce `theme-tokens.test.ts` to enforce rules against fixed hex colors and ensure compliance with the design system. - Update various components and styles to replace inline styles and fixed colors with CSS variables, ensuring consistent theming across light and dark modes. - Ensure all changes maintain visual integrity in both light and dark modes, with a focus on accessibility and contrast standards.
This commit is contained in:
@@ -352,7 +352,7 @@ export default function AppointmentCreatePage() {
|
||||
border: active ? '1px solid var(--primary)' : '1px solid var(--border-2)',
|
||||
background: active ? 'var(--primary)' : 'transparent',
|
||||
}}>
|
||||
{active && <span style={{ width: 8, height: 8, background: '#fff', borderRadius: 2 }} />}
|
||||
{active && <span style={{ width: 8, height: 8, background: 'var(--surface)', borderRadius: 2 }} />}
|
||||
</span>
|
||||
{o.name}
|
||||
</button>
|
||||
@@ -378,7 +378,7 @@ export default function AppointmentCreatePage() {
|
||||
<button type="button" aria-label={`حذف ${s.name}`} onClick={() => removeService(s.uuid)}
|
||||
style={{
|
||||
display: 'grid', placeItems: 'center', width: 16, height: 16, borderRadius: 999,
|
||||
border: 'none', cursor: 'pointer', background: 'var(--primary)', color: '#fff',
|
||||
border: 'none', cursor: 'pointer', background: 'var(--primary)', color: 'var(--on-primary)',
|
||||
fontSize: 12, lineHeight: 1, fontFamily: 'inherit',
|
||||
}}>×</button>
|
||||
</span>
|
||||
@@ -451,13 +451,13 @@ export default function AppointmentCreatePage() {
|
||||
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 10, fontSize: 14, cursor: 'pointer' }}>
|
||||
<span style={{
|
||||
position: 'relative', width: 42, height: 22, borderRadius: 999, flexShrink: 0,
|
||||
background: depositRequired ? 'var(--primary)' : '#c4c4c4', transition: 'background .2s',
|
||||
background: depositRequired ? 'var(--primary)' : 'var(--border-2)', transition: 'background .2s',
|
||||
}}>
|
||||
<input type="checkbox" checked={depositRequired} onChange={e => setDepositRequired(e.target.checked)}
|
||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', margin: 0, opacity: 0, cursor: 'pointer' }} />
|
||||
<span style={{
|
||||
position: 'absolute', top: 2, insetInlineStart: depositRequired ? 22 : 2, width: 18, height: 18,
|
||||
borderRadius: 999, background: '#fff', transition: 'inset-inline-start .2s', boxShadow: '0 1px 2px rgba(0,0,0,.2)',
|
||||
borderRadius: 999, background: 'var(--surface)', transition: 'inset-inline-start .2s', boxShadow: '0 1px 2px rgba(0,0,0,.2)',
|
||||
}} />
|
||||
</span>
|
||||
بیعانه مورد نیاز است.
|
||||
|
||||
@@ -123,7 +123,7 @@ export default function AppointmentDetailPage() {
|
||||
]}
|
||||
action={
|
||||
<button onClick={() => navigate(backTo)}
|
||||
className="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-100 transition-colors">
|
||||
className="flex items-center gap-2 text-sm text-[var(--text-2)] hover:text-[var(--text)] transition-colors">
|
||||
<ArrowRightIcon className="w-4 h-4" />
|
||||
بازگشت
|
||||
</button>
|
||||
@@ -138,8 +138,8 @@ export default function AppointmentDetailPage() {
|
||||
</div>
|
||||
) : appt ? (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6">
|
||||
<h3 className="font-semibold text-gray-800 mb-4">اطلاعات بیمار</h3>
|
||||
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm p-6">
|
||||
<h3 className="font-semibold text-[var(--text)] mb-4">اطلاعات بیمار</h3>
|
||||
<InfoRow label="نام بیمار" value={appt.patient_name} />
|
||||
<InfoRow label="موبایل" value={<span dir="ltr">{appt.patient_mobile}</span>} />
|
||||
<InfoRow label="پزشک" value={appt.doctor?.name ?? null} />
|
||||
@@ -151,10 +151,10 @@ export default function AppointmentDetailPage() {
|
||||
<InfoRow label="تاریخ ثبت" value={formatDateTime(appt.created_at)} />
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6">
|
||||
<h3 className="font-semibold text-gray-800 mb-4">وضعیت و اقدامات</h3>
|
||||
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm p-6">
|
||||
<h3 className="font-semibold text-[var(--text)] mb-4">وضعیت و اقدامات</h3>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-gray-500 mb-2">وضعیت فعلی:</p>
|
||||
<p className="text-sm text-[var(--text-2)] mb-2">وضعیت فعلی:</p>
|
||||
<StatusBadge type="appointment" value={appt.status} />
|
||||
</div>
|
||||
|
||||
@@ -189,37 +189,37 @@ export default function AppointmentDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-gray-100">
|
||||
<div className="mt-4 pt-4 border-t border-[var(--border)]">
|
||||
<button
|
||||
onClick={() => setCancelOpen(true)}
|
||||
className="w-full py-2 border border-red-300 text-red-600 text-sm rounded-[10px] hover:bg-red-50 transition-colors"
|
||||
className="w-full py-2 border border-[var(--danger)] text-[var(--danger)] text-sm rounded-[10px] hover:bg-[var(--danger-bg)] transition-colors"
|
||||
>
|
||||
لغو نوبت
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6 lg:col-span-2">
|
||||
<h3 className="font-semibold text-gray-800 mb-4">تاریخچه رویدادها</h3>
|
||||
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm p-6 lg:col-span-2">
|
||||
<h3 className="font-semibold text-[var(--text)] mb-4">تاریخچه رویدادها</h3>
|
||||
{eventsQuery.isLoading ? (
|
||||
<div className="h-6 w-40 rounded skeleton" />
|
||||
) : events.length === 0 ? (
|
||||
<p className="text-sm text-gray-400">رویدادی برای این نوبت ثبت نشده است.</p>
|
||||
<p className="text-sm text-[var(--text-3)]">رویدادی برای این نوبت ثبت نشده است.</p>
|
||||
) : (
|
||||
<ol className="space-y-4">
|
||||
{events.map((ev, i) => (
|
||||
<li key={i} className="flex gap-3">
|
||||
<span className="mt-1.5 w-2.5 h-2.5 rounded-full bg-red-500 shrink-0" />
|
||||
<span className="mt-1.5 w-2.5 h-2.5 rounded-full bg-[var(--danger)] shrink-0" />
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium text-gray-800">{ev.title}</span>
|
||||
<span className="text-xs text-gray-400">{formatDateTime(ev.created_at)}</span>
|
||||
<span className="text-sm font-medium text-[var(--text)]">{ev.title}</span>
|
||||
<span className="text-xs text-[var(--text-3)]">{formatDateTime(ev.created_at)}</span>
|
||||
</div>
|
||||
{ev.actor_name && (
|
||||
<div className="text-xs text-gray-500 mt-0.5">توسط: {ev.actor_name}</div>
|
||||
<div className="text-xs text-[var(--text-2)] mt-0.5">توسط: {ev.actor_name}</div>
|
||||
)}
|
||||
{ev.reason && (
|
||||
<div className="text-xs text-gray-600 mt-0.5">دلیل: {ev.reason}</div>
|
||||
<div className="text-xs text-[var(--text-2)] mt-0.5">دلیل: {ev.reason}</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
@@ -229,7 +229,7 @@ export default function AppointmentDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white rounded-2xl border border-gray-100 p-16 text-center text-gray-400">
|
||||
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] p-16 text-center text-[var(--text-3)]">
|
||||
نوبتی یافت نشد
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -75,7 +75,7 @@ function DateNavigator({ date, onChange }: { date: string; onChange: (d: string)
|
||||
background: 'var(--surface)', border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r-sm)', minWidth: 130, gap: 1,
|
||||
}}>
|
||||
<span style={{ fontSize: 13, fontWeight: 700, lineHeight: 1.2, color: isFriday ? '#ef4444' : 'var(--text)' }}>
|
||||
<span style={{ fontSize: 13, fontWeight: 700, lineHeight: 1.2, color: isFriday ? 'var(--danger)' : 'var(--text)' }}>
|
||||
{weekDay}
|
||||
</span>
|
||||
<span style={{ fontSize: 11, color: 'var(--text-2)', lineHeight: 1.2 }}>
|
||||
|
||||
@@ -142,7 +142,7 @@ export default function BlogFormPage() {
|
||||
<div>
|
||||
<label>عنوان مقاله *</label>
|
||||
<input {...register('title')} placeholder="عنوان جذاب بنویسید..." className="cp-input h-11" />
|
||||
{errors.title && <p className="text-red-500 text-xs mt-1">{errors.title.message}</p>}
|
||||
{errors.title && <p className="text-[var(--danger)] text-xs mt-1">{errors.title.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -170,7 +170,7 @@ export default function BlogFormPage() {
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{errors.body && <p className="text-red-500 text-xs mt-1">{errors.body.message}</p>}
|
||||
{errors.body && <p className="text-[var(--danger)] text-xs mt-1">{errors.body.message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ function LogoUploadField({ value, onChange, uploadUrl }: {
|
||||
<img src={value} alt="logo" style={{ width: 64, height: 64, objectFit: 'contain', borderRadius: 12, border: '1px solid var(--border)', background: 'var(--surface)', padding: 4 }} />
|
||||
<button type="button" onClick={() => onChange(null)} style={{
|
||||
position: 'absolute', top: -6, right: -6, width: 20, height: 20, borderRadius: 10,
|
||||
background: 'var(--danger)', color: '#fff', border: 'none', cursor: 'pointer',
|
||||
background: 'var(--danger)', color: 'var(--on-primary)', border: 'none', cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<XMarkIcon style={{ width: 12, height: 12 }} />
|
||||
|
||||
@@ -631,7 +631,7 @@ export default function ClinicDetailPage() {
|
||||
borderRadius: '50%', background: 'var(--primary)', border: '2px solid var(--surface)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
|
||||
}}>
|
||||
<CameraIcon style={{ width: 12, height: 12, color: '#fff' }} />
|
||||
<CameraIcon style={{ width: 12, height: 12, color: 'var(--on-primary)' }} />
|
||||
</button>
|
||||
<input ref={logoInputRef} type="file" accept="image/*" style={{ display: 'none' }}
|
||||
onChange={e => e.target.files?.[0] && handleLogoUpload(e.target.files[0])} />
|
||||
|
||||
@@ -35,7 +35,7 @@ function Avatar({ name }: { name: string }) {
|
||||
width: 26, height: 26, borderRadius: '50%',
|
||||
background: 'linear-gradient(145deg, oklch(0.62 0.15 162), oklch(0.48 0.16 162))',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 11, fontWeight: 700, color: '#fff', flexShrink: 0,
|
||||
fontSize: 11, fontWeight: 700, color: 'var(--on-primary)', flexShrink: 0,
|
||||
}}>
|
||||
{name.charAt(0)}
|
||||
</div>
|
||||
|
||||
@@ -66,7 +66,7 @@ const PAY_LABEL: Record<string, string> = {
|
||||
pending: 'در انتظار', received: 'موفق', canceled: 'لغو شده', refund: 'استرداد',
|
||||
};
|
||||
const PAY_COLOR: Record<string, string> = {
|
||||
pending: '#f59e0b', received: '#22c55e', canceled: '#ef4444', refund: '#3b82f6',
|
||||
pending: 'var(--warning)', received: 'var(--success)', canceled: 'var(--danger)', refund: 'var(--info)',
|
||||
};
|
||||
const PAY_CLS: Record<string, string> = {
|
||||
pending: 'amber', received: 'green', canceled: 'red', refund: 'blue',
|
||||
@@ -373,7 +373,7 @@ function AdminDashboard() {
|
||||
const revSeries = useMemo(() => charts?.revenue_by_day?.map(d => d.amount_rials) ?? [], [charts]);
|
||||
const donutData = useMemo(() =>
|
||||
(charts?.appointment_status ?? []).slice(0, 7).map(s => ({
|
||||
label: APPT_LABEL[s.status] ?? s.status, value: s.count, color: APPT_COLOR[s.status] ?? '#94a3b8',
|
||||
label: APPT_LABEL[s.status] ?? s.status, value: s.count, color: APPT_COLOR[s.status] ?? 'var(--text-3)',
|
||||
})), [charts]);
|
||||
const hbarsData = useMemo(() => (charts?.top_specialties ?? []).map(s => ({ label: s.name, value: s.count })), [charts]);
|
||||
|
||||
@@ -400,13 +400,13 @@ function AdminDashboard() {
|
||||
if (!recent) return [];
|
||||
const evs: { title: string; sub: string; time: string; color: string }[] = [];
|
||||
(recent.appointments ?? []).slice(0, 4).forEach(a => {
|
||||
evs.push({ title: `نوبت ${APPT_LABEL[a.status] ?? a.status}`, sub: `${a.user_name || a.user_mobile} · ${a.doctor_name}`, time: a.created_at, color: APPT_COLOR[a.status] ?? '#94a3b8' });
|
||||
evs.push({ title: `نوبت ${APPT_LABEL[a.status] ?? a.status}`, sub: `${a.user_name || a.user_mobile} · ${a.doctor_name}`, time: a.created_at, color: APPT_COLOR[a.status] ?? 'var(--text-3)' });
|
||||
});
|
||||
(recent.payments ?? []).slice(0, 3).forEach(p => {
|
||||
evs.push({ title: `پرداخت ${PAY_LABEL[p.status] ?? p.status}`, sub: `${p.user_name || p.user_mobile} · ${formatRial(p.amount)}`, time: p.created_at, color: PAY_COLOR[p.status] ?? '#94a3b8' });
|
||||
evs.push({ title: `پرداخت ${PAY_LABEL[p.status] ?? p.status}`, sub: `${p.user_name || p.user_mobile} · ${formatRial(p.amount)}`, time: p.created_at, color: PAY_COLOR[p.status] ?? 'var(--text-3)' });
|
||||
});
|
||||
(recent.users ?? []).slice(0, 3).forEach(u => {
|
||||
evs.push({ title: 'ثبتنام کاربر جدید', sub: u.name || u.mobile, time: u.created_at, color: '#8b5cf6' });
|
||||
evs.push({ title: 'ثبتنام کاربر جدید', sub: u.name || u.mobile, time: u.created_at, color: 'var(--violet)' });
|
||||
});
|
||||
return evs.sort((a, b) => new Date(b.time).getTime() - new Date(a.time).getTime()).slice(0, 8);
|
||||
}, [recent]);
|
||||
|
||||
@@ -90,8 +90,8 @@ const AVATAR_COLORS = [
|
||||
'from-amber-500 to-orange-600',
|
||||
];
|
||||
const SPECIALTY_COLORS = [
|
||||
'bg-red-100 dark:bg-red-400/10 text-red-700 dark:text-red-300',
|
||||
'bg-blue-100 dark:bg-blue-400/10 text-blue-700 dark:text-blue-300',
|
||||
'bg-[var(--danger-bg)] dark:bg-red-400/10 text-[var(--danger)] dark:text-red-300',
|
||||
'bg-[var(--info-bg)] dark:bg-blue-400/10 text-[var(--info)] dark:text-blue-300',
|
||||
'bg-emerald-100 dark:bg-emerald-400/10 text-emerald-700 dark:text-emerald-300',
|
||||
'bg-violet-100 dark:bg-violet-400/10 text-violet-700 dark:text-violet-300',
|
||||
'bg-orange-100 dark:bg-orange-400/10 text-orange-700 dark:text-orange-300',
|
||||
@@ -136,13 +136,13 @@ function DoctorAvatar({ name, img, idx, onUpload, uploading }: {
|
||||
>
|
||||
{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" />
|
||||
: <div className={`w-24 h-24 rounded-2xl bg-gradient-to-br ${AVATAR_COLORS[idx % AVATAR_COLORS.length]} flex items-center justify-center text-white 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 ${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>
|
||||
}
|
||||
{onUpload && (
|
||||
<div className="absolute inset-0 rounded-2xl bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
||||
{uploading
|
||||
? <span className="w-6 h-6 border-2 border-white/40 border-t-white rounded-full animate-spin" />
|
||||
: <CameraIcon className="w-7 h-7 text-white" />
|
||||
? <span className="w-6 h-6 border-2 border-[var(--surface)]/40 border-t-white rounded-full animate-spin" />
|
||||
: <CameraIcon className="w-7 h-7 text-[var(--on-primary)]" />
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
@@ -161,19 +161,19 @@ function InfoCard({ icon: Icon, label, value, mono = false, copyable = false }:
|
||||
mono?: boolean; copyable?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-start gap-3 p-4 rounded-xl bg-slate-50 dark:bg-gray-800/60 hover:bg-slate-100 dark:hover:bg-gray-800 transition-colors group">
|
||||
<div className="w-9 h-9 rounded-lg bg-white dark:bg-gray-700 shadow-sm flex items-center justify-center shrink-0">
|
||||
<Icon className="w-4 h-4 text-slate-500 dark:text-slate-400" />
|
||||
<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="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)]" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[11px] font-medium text-slate-400 dark:text-slate-500 uppercase tracking-wide mb-0.5">{label}</p>
|
||||
<p className={`text-sm font-medium text-slate-800 dark:text-slate-200 ${mono ? 'font-mono' : ''}`} dir={mono ? 'ltr' : undefined}>
|
||||
{value ?? <span className="text-slate-400 font-normal">ثبت نشده</span>}
|
||||
<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}>
|
||||
{value ?? <span className="text-[var(--text-3)] font-normal">ثبت نشده</span>}
|
||||
</p>
|
||||
</div>
|
||||
{copyable && typeof value === 'string' && (
|
||||
<button onClick={() => { navigator.clipboard.writeText(value); toast.success('کپی شد'); }}
|
||||
className="opacity-0 group-hover:opacity-100 w-7 h-7 flex items-center justify-center rounded-lg text-slate-400 hover:text-slate-600 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)] transition-all shrink-0">
|
||||
<ClipboardDocumentIcon className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
@@ -187,9 +187,9 @@ function StarRating({ rate }: { rate: number }) {
|
||||
{Array.from({ length: 5 }).map((_, i) =>
|
||||
i < Math.round(rate)
|
||||
? <StarSolid key={i} className="w-4 h-4 text-amber-400" />
|
||||
: <StarIcon key={i} className="w-4 h-4 text-slate-300 dark:text-slate-600" />
|
||||
: <StarIcon key={i} className="w-4 h-4 text-[var(--text-3)]" />
|
||||
)}
|
||||
<span className="text-sm font-medium text-slate-600 dark:text-slate-400 mr-1">{rate.toFixed(1)}</span>
|
||||
<span className="text-sm font-medium text-[var(--text-2)] mr-1">{rate.toFixed(1)}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -241,7 +241,7 @@ function MapPicker({ lat, lng, onChange, flyTarget }: {
|
||||
const pos: [number, number] | null = lat !== null && lng !== null ? [lat, lng] : null;
|
||||
const center: [number, number] = pos ?? IRAN_CENTER;
|
||||
return (
|
||||
<div className="rounded-xl overflow-hidden border border-slate-200 dark:border-gray-700" style={{ height: 300 }}>
|
||||
<div className="rounded-xl overflow-hidden border border-[var(--border)]" style={{ height: 300 }}>
|
||||
<MapContainer center={center} zoom={pos ? 13 : 5} style={{ height: '100%', width: '100%' }}>
|
||||
<TileLayer
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
@@ -306,7 +306,7 @@ function SearchableSelect({ options, value, onChange, placeholder, disabled = fa
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
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-slate-400 dark:text-slate-500' : ''}`}
|
||||
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)]' : ''}`}
|
||||
>
|
||||
<span className="truncate flex-1">{selected?.label ?? placeholder ?? 'انتخاب کنید'}</span>
|
||||
<ChevronDownIcon className={`w-4 h-4 shrink-0 mr-2 transition-transform ${open ? 'rotate-180' : ''}`} />
|
||||
@@ -314,26 +314,26 @@ function SearchableSelect({ options, value, onChange, placeholder, disabled = fa
|
||||
|
||||
{open && createPortal(
|
||||
<div ref={dropRef} style={dropStyle}
|
||||
className="bg-white dark:bg-gray-900 border border-slate-200 dark:border-gray-700 rounded-xl shadow-2xl overflow-hidden">
|
||||
<div className="p-2 border-b border-slate-100 dark:border-gray-700">
|
||||
className="bg-[var(--surface)] border border-[var(--border)] rounded-xl shadow-2xl overflow-hidden">
|
||||
<div className="p-2 border-b border-[var(--border)]">
|
||||
<input autoFocus type="text" value={q} onChange={e => setQ(e.target.value)}
|
||||
placeholder="جستجو..." className="cp-input text-sm h-8" />
|
||||
</div>
|
||||
<div className="max-h-52 overflow-y-auto">
|
||||
<button type="button"
|
||||
onClick={() => { onChange(null, null); setOpen(false); setQ(''); }}
|
||||
className="w-full text-right px-3 py-2 text-sm text-slate-400 hover:bg-slate-50 dark:hover:bg-gray-800">
|
||||
className="w-full text-right px-3 py-2 text-sm text-[var(--text-3)] hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)]">
|
||||
{placeholder ?? 'انتخاب کنید'}
|
||||
</button>
|
||||
{filtered.map(o => (
|
||||
<button key={o.value} type="button"
|
||||
onClick={() => { onChange(o.value, o.label); setOpen(false); setQ(''); }}
|
||||
className={`w-full text-right px-3 py-2 text-sm hover:bg-slate-50 dark:hover:bg-gray-800 ${value === o.value ? 'text-primary-600 dark:text-primary-400 font-medium bg-primary-50 dark:bg-primary-500/10' : 'text-slate-700 dark:text-slate-300'}`}>
|
||||
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)]'}`}>
|
||||
{o.label}
|
||||
</button>
|
||||
))}
|
||||
{filtered.length === 0 && (
|
||||
<p className="text-xs text-slate-400 text-center py-3">نتیجهای یافت نشد</p>
|
||||
<p className="text-xs text-[var(--text-3)] text-center py-3">نتیجهای یافت نشد</p>
|
||||
)}
|
||||
</div>
|
||||
</div>,
|
||||
@@ -379,13 +379,13 @@ function HierarchicalSpecialtyPicker({ selected, onChange, specialties }: {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="border border-slate-200 dark:border-gray-700 rounded-xl overflow-hidden">
|
||||
<div className="p-2 border-b border-slate-100 dark:border-gray-700 bg-slate-50 dark:bg-gray-800/60">
|
||||
<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">
|
||||
<input type="text" value={q} onChange={e => setQ(e.target.value)} placeholder="جستجوی تخصص..."
|
||||
className="cp-input text-sm h-8" />
|
||||
</div>
|
||||
{selectedLabels.length > 0 && (
|
||||
<div className="px-3 py-2 border-b border-slate-100 dark:border-gray-700 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 }) => (
|
||||
<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">
|
||||
{name}
|
||||
@@ -398,18 +398,18 @@ function HierarchicalSpecialtyPicker({ selected, onChange, specialties }: {
|
||||
{q ? (
|
||||
filteredFlat.length > 0
|
||||
? filteredFlat.map(s => (
|
||||
<label key={s.id} className="flex items-center gap-2.5 px-3 py-2 hover:bg-slate-50 dark:hover:bg-gray-800 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)}
|
||||
className="rounded border-slate-300 dark:border-gray-600 text-primary-600 shrink-0" />
|
||||
<span className="text-sm text-slate-700 dark:text-slate-300">{s.name}</span>
|
||||
className="rounded border-[var(--border-2)] dark:border-gray-600 text-primary-600 shrink-0" />
|
||||
<span className="text-sm text-[var(--text)]">{s.name}</span>
|
||||
{s.parent_id !== null && (
|
||||
<span className="text-[10px] text-slate-400 mr-auto truncate max-w-[100px]">
|
||||
<span className="text-[10px] text-[var(--text-3)] mr-auto truncate max-w-[100px]">
|
||||
{specialties.find(p => p.id === s.parent_id)?.name}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
))
|
||||
: <p className="text-xs text-slate-400 text-center py-4">نتیجهای یافت نشد</p>
|
||||
: <p className="text-xs text-[var(--text-3)] text-center py-4">نتیجهای یافت نشد</p>
|
||||
) : (
|
||||
parents.map(parent => {
|
||||
const children = childMap.get(parent.id) ?? [];
|
||||
@@ -417,16 +417,16 @@ function HierarchicalSpecialtyPicker({ selected, onChange, specialties }: {
|
||||
const numSel = children.filter(c => selected.includes(c.id)).length;
|
||||
return (
|
||||
<div key={parent.id}>
|
||||
<div className="flex items-center gap-2 px-3 py-2.5 hover:bg-slate-50 dark:hover:bg-gray-800 cursor-pointer select-none"
|
||||
<div className="flex items-center gap-2 px-3 py-2.5 hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] cursor-pointer select-none"
|
||||
onClick={() => children.length > 0 ? toggleParent(parent.id) : toggleSelect(parent.id)}>
|
||||
{children.length > 0
|
||||
? (isOpen
|
||||
? <ChevronDownIcon className="w-4 h-4 text-slate-400 shrink-0" />
|
||||
: <ChevronRightIcon className="w-4 h-4 text-slate-400 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" />)
|
||||
: <input type="checkbox" checked={selected.includes(parent.id)} readOnly
|
||||
className="rounded border-slate-300 dark:border-gray-600 text-primary-600 pointer-events-none shrink-0" />
|
||||
className="rounded border-[var(--border-2)] dark:border-gray-600 text-primary-600 pointer-events-none shrink-0" />
|
||||
}
|
||||
<span className={`text-sm font-medium ${numSel > 0 ? 'text-primary-600 dark:text-primary-400' : 'text-slate-700 dark:text-slate-300'}`}>
|
||||
<span className={`text-sm font-medium ${numSel > 0 ? 'text-primary-600 dark:text-primary-400' : 'text-[var(--text)] dark:text-[var(--text-3)]'}`}>
|
||||
{parent.name}
|
||||
</span>
|
||||
{numSel > 0 && (
|
||||
@@ -436,10 +436,10 @@ function HierarchicalSpecialtyPicker({ selected, onChange, specialties }: {
|
||||
)}
|
||||
</div>
|
||||
{isOpen && children.map(child => (
|
||||
<label key={child.id} className="flex items-center gap-2.5 px-3 py-2 pr-9 hover:bg-slate-50 dark:hover:bg-gray-800 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)}
|
||||
className="rounded border-slate-300 dark:border-gray-600 text-primary-600 shrink-0" />
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">{child.name}</span>
|
||||
className="rounded border-[var(--border-2)] dark:border-gray-600 text-primary-600 shrink-0" />
|
||||
<span className="text-sm text-[var(--text-2)]">{child.name}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
@@ -487,7 +487,7 @@ function ServicesPicker({ selected, onChange, services, specialties, selectedSpe
|
||||
|
||||
if (selectedSpecialtyIds.length === 0) {
|
||||
return (
|
||||
<div className="rounded-xl border border-dashed border-slate-300 dark:border-gray-600 py-8 text-center text-slate-400 dark:text-slate-500 text-sm">
|
||||
<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>
|
||||
);
|
||||
@@ -495,31 +495,31 @@ function ServicesPicker({ selected, onChange, services, specialties, selectedSpe
|
||||
|
||||
if (relevantServices.length === 0) {
|
||||
return (
|
||||
<div className="rounded-xl border border-dashed border-slate-300 dark:border-gray-600 py-8 text-center text-slate-400 dark:text-slate-500 text-sm">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border border-slate-200 dark:border-gray-700 rounded-xl overflow-hidden">
|
||||
<div className="border border-[var(--border)] rounded-xl overflow-hidden">
|
||||
{tabs.length > 1 && (
|
||||
<div className="flex gap-1 px-2 py-2 border-b border-slate-100 dark:border-gray-700 bg-slate-50 dark:bg-gray-800/60 overflow-x-auto">
|
||||
<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">
|
||||
{[{ id: 'all' as const, name: 'همه' }, ...tabs].map(t => (
|
||||
<button key={String(t.id)} type="button"
|
||||
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-white' : 'text-slate-600 dark:text-slate-400 hover:bg-white dark:hover:bg-gray-700'}`}>
|
||||
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)]'}`}>
|
||||
{t.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="p-2 border-b border-slate-100 dark:border-gray-700">
|
||||
<div className="p-2 border-b border-[var(--border)]">
|
||||
<input type="text" value={q} onChange={e => setQ(e.target.value)} placeholder="جستجوی خدمت..."
|
||||
className="cp-input text-sm h-8" />
|
||||
</div>
|
||||
{selectedLabels.length > 0 && (
|
||||
<div className="px-3 py-2 border-b border-slate-100 dark:border-gray-700 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 }) => (
|
||||
<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">
|
||||
{name}
|
||||
@@ -530,14 +530,14 @@ function ServicesPicker({ selected, onChange, services, specialties, selectedSpe
|
||||
)}
|
||||
<div className="max-h-44 overflow-y-auto">
|
||||
{visibleServices.map(svc => (
|
||||
<label key={svc.id} className="flex items-center gap-2.5 px-3 py-2 hover:bg-slate-50 dark:hover:bg-gray-800 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)}
|
||||
className="rounded border-slate-300 dark:border-gray-600 text-emerald-600 shrink-0" />
|
||||
<span className="text-sm text-slate-700 dark:text-slate-300">{svc.name}</span>
|
||||
className="rounded border-[var(--border-2)] dark:border-gray-600 text-[var(--success)] shrink-0" />
|
||||
<span className="text-sm text-[var(--text)]">{svc.name}</span>
|
||||
</label>
|
||||
))}
|
||||
{visibleServices.length === 0 && q && (
|
||||
<p className="text-xs text-slate-400 text-center py-4">نتیجهای یافت نشد</p>
|
||||
<p className="text-xs text-[var(--text-3)] text-center py-4">نتیجهای یافت نشد</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -644,32 +644,32 @@ function AddressModal({ open, onClose, existing, doctorUuid, onSaved }: {
|
||||
{/* Row 1: Name + Phone */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">نام مطب / کلینیک</label>
|
||||
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">نام مطب / کلینیک</label>
|
||||
<input type="text" className="input" placeholder="مثال: کلینیک مهر" {...register('name')} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
تلفن <span className="text-red-500">*</span>
|
||||
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">
|
||||
تلفن <span className="text-[var(--danger)]">*</span>
|
||||
</label>
|
||||
<input className="cp-input text-left" placeholder="021..." {...latinDigitsField(register('telephone'))} />
|
||||
{errors.telephone && <p className="text-xs text-red-500 mt-1">{errors.telephone.message}</p>}
|
||||
{errors.telephone && <p className="text-xs text-[var(--danger)] mt-1">{errors.telephone.message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Full address */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
آدرس کامل <span className="text-red-500">*</span>
|
||||
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">
|
||||
آدرس کامل <span className="text-[var(--danger)]">*</span>
|
||||
</label>
|
||||
<textarea rows={2} className="cp-input resize-none" placeholder="خیابان، کوچه، پلاک..." {...register('address')} />
|
||||
{errors.address && <p className="text-xs text-red-500 mt-1">{errors.address.message}</p>}
|
||||
{errors.address && <p className="text-xs text-[var(--danger)] mt-1">{errors.address.message}</p>}
|
||||
</div>
|
||||
|
||||
{/* Row 3: Province + City side by side */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
استان <span className="text-red-500">*</span>
|
||||
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">
|
||||
استان <span className="text-[var(--danger)]">*</span>
|
||||
</label>
|
||||
<SearchableSelect
|
||||
options={provinces.map(p => ({ value: p.id, label: p.name }))}
|
||||
@@ -681,11 +681,11 @@ function AddressModal({ open, onClose, existing, doctorUuid, onSaved }: {
|
||||
setMapFlyTarget(null);
|
||||
}}
|
||||
/>
|
||||
{errors.province_id && <p className="text-xs text-red-500 mt-1">{errors.province_id.message}</p>}
|
||||
{errors.province_id && <p className="text-xs text-[var(--danger)] mt-1">{errors.province_id.message}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
شهر <span className="text-red-500">*</span>
|
||||
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">
|
||||
شهر <span className="text-[var(--danger)]">*</span>
|
||||
</label>
|
||||
<SearchableSelect
|
||||
options={cities.map(c => ({ value: c.id, label: c.name }))}
|
||||
@@ -701,23 +701,23 @@ function AddressModal({ open, onClose, existing, doctorUuid, onSaved }: {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{errors.city_id && <p className="text-xs text-red-500 mt-1">{errors.city_id.message}</p>}
|
||||
{errors.city_id && <p className="text-xs text-[var(--danger)] mt-1">{errors.city_id.message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 4: Map */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
<label className="block text-sm font-medium text-[var(--text)]">
|
||||
موقعیت روی نقشه
|
||||
</label>
|
||||
{lat && lng && (
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-mono" dir="ltr">
|
||||
<span className="text-xs text-[var(--text-2)] font-mono" dir="ltr">
|
||||
{lat.toFixed(5)}, {lng.toFixed(5)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 dark:text-slate-500 mb-2">
|
||||
<p className="text-xs text-[var(--text-3)] mb-2">
|
||||
برای دقت بیشتر روی نقشه کلیک کنید
|
||||
</p>
|
||||
<MapPicker lat={lat} lng={lng} flyTarget={mapFlyTarget}
|
||||
@@ -725,7 +725,7 @@ function AddressModal({ open, onClose, existing, doctorUuid, onSaved }: {
|
||||
{lat && lng && (
|
||||
<button type="button"
|
||||
onClick={() => { setValue('latitude', ''); setValue('longitude', ''); }}
|
||||
className="mt-2 text-xs text-red-500 hover:underline flex items-center gap-1">
|
||||
className="mt-2 text-xs text-[var(--danger)] hover:underline flex items-center gap-1">
|
||||
<XMarkIcon className="w-3 h-3" />حذف موقعیت
|
||||
</button>
|
||||
)}
|
||||
@@ -741,26 +741,26 @@ function AddressCard({ addr, onEdit, onDelete, readOnly = false }: {
|
||||
addr: AddressData; onEdit: () => void; onDelete: () => void; readOnly?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-start gap-3 p-4 rounded-xl bg-slate-50 dark:bg-gray-800/60 border border-slate-100 dark:border-gray-700 group">
|
||||
<div className="w-9 h-9 rounded-lg bg-white dark:bg-gray-700 shadow-sm flex items-center justify-center shrink-0 mt-0.5">
|
||||
<MapPinIcon className="w-4 h-4 text-slate-400" />
|
||||
<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="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)]" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
{addr.name && <p className="text-sm font-semibold text-slate-800 dark:text-slate-200 mb-0.5">{addr.name}</p>}
|
||||
{addr.address && <p className="text-xs text-slate-600 dark:text-slate-400 leading-relaxed">{addr.address}</p>}
|
||||
{addr.name && <p className="text-sm font-semibold text-[var(--text)] mb-0.5">{addr.name}</p>}
|
||||
{addr.address && <p className="text-xs text-[var(--text-2)] leading-relaxed">{addr.address}</p>}
|
||||
<div className="flex flex-wrap gap-x-3 gap-y-1 mt-1.5">
|
||||
{addr.telephone && (
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400 flex items-center gap-1 font-mono" dir="ltr">
|
||||
<span className="text-xs text-[var(--text-2)] flex items-center gap-1 font-mono" dir="ltr">
|
||||
<PhoneIcon className="w-3 h-3" />{addr.telephone}
|
||||
</span>
|
||||
)}
|
||||
{addr.province && (
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">
|
||||
<span className="text-xs text-[var(--text-2)]">
|
||||
{addr.province.name}{addr.city ? ` — ${addr.city.name}` : ''}
|
||||
</span>
|
||||
)}
|
||||
{addr.map.latitude && addr.map.longitude && (
|
||||
<span className="text-xs text-slate-400 dark:text-slate-500 flex items-center gap-1" dir="ltr">
|
||||
<span className="text-xs text-[var(--text-3)] flex items-center gap-1" dir="ltr">
|
||||
<MapPinIcon className="w-3 h-3" />{parseFloat(addr.map.latitude).toFixed(4)}, {parseFloat(addr.map.longitude).toFixed(4)}
|
||||
</span>
|
||||
)}
|
||||
@@ -769,11 +769,11 @@ function AddressCard({ addr, onEdit, onDelete, readOnly = false }: {
|
||||
{!readOnly && (
|
||||
<div className="opacity-0 group-hover:opacity-100 flex items-center gap-1 transition-opacity shrink-0">
|
||||
<button onClick={onEdit}
|
||||
className="w-7 h-7 flex items-center justify-center rounded-lg text-slate-400 hover:text-blue-600 hover:bg-blue-50 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-blue-500/10 transition-colors">
|
||||
<PencilIcon className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button onClick={onDelete}
|
||||
className="w-7 h-7 flex items-center justify-center rounded-lg text-slate-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors">
|
||||
className="w-7 h-7 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--danger)] hover:bg-[var(--danger-bg)] dark:hover:bg-[var(--danger)]/10 transition-colors">
|
||||
<TrashIcon className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -1362,10 +1362,10 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
if (isError || !doctor) {
|
||||
return (
|
||||
<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-red-100 dark:bg-red-400/10 flex items-center justify-center">
|
||||
<XCircleIcon className="w-8 h-8 text-red-500" />
|
||||
<div className="w-16 h-16 rounded-2xl bg-[var(--danger-bg)] dark:bg-red-400/10 flex items-center justify-center">
|
||||
<XCircleIcon className="w-8 h-8 text-[var(--danger)]" />
|
||||
</div>
|
||||
<p className="font-semibold text-slate-800 dark:text-slate-200">پزشک یافت نشد</p>
|
||||
<p className="font-semibold text-[var(--text)]">پزشک یافت نشد</p>
|
||||
<button onClick={() => navigate('/admin/doctors')} className="cp-btn-secondary mt-2">
|
||||
بازگشت به لیست پزشکان
|
||||
</button>
|
||||
@@ -1383,24 +1383,24 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
<div className="animate-slide-up space-y-5">
|
||||
|
||||
{/* Breadcrumb */}
|
||||
<div className="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400">
|
||||
<div className="flex items-center gap-2 text-sm text-[var(--text-2)]">
|
||||
{isOwnProfile ? (
|
||||
<>
|
||||
<button onClick={() => navigate('/admin/dashboard')}
|
||||
className="flex items-center gap-1.5 hover:text-slate-800 dark:hover:text-slate-100 transition-colors">
|
||||
className="flex items-center gap-1.5 hover:text-[var(--text)] dark:hover:text-[var(--text)] transition-colors">
|
||||
<ArrowRightIcon className="w-4 h-4" />داشبورد
|
||||
</button>
|
||||
<span>/</span>
|
||||
<span className="text-slate-700 dark:text-slate-300 font-medium">پروفایل من</span>
|
||||
<span className="text-[var(--text)] font-medium">پروفایل من</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button onClick={() => navigate(primaryRole === 'clinic' ? '/admin/dashboard' : '/admin/doctors')}
|
||||
className="flex items-center gap-1.5 hover:text-slate-800 dark:hover:text-slate-100 transition-colors">
|
||||
className="flex items-center gap-1.5 hover:text-[var(--text)] dark:hover:text-[var(--text)] transition-colors">
|
||||
<ArrowRightIcon className="w-4 h-4" />{primaryRole === 'clinic' ? 'داشبورد' : 'پزشکان'}
|
||||
</button>
|
||||
<span>/</span>
|
||||
<span className="text-slate-700 dark:text-slate-300 font-medium">پروفایل پزشک</span>
|
||||
<span className="text-[var(--text)] font-medium">پروفایل پزشک</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -1422,27 +1422,27 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
/>
|
||||
)}
|
||||
{primaryRole !== 'clinic' && !isReadOnly && (
|
||||
<span className="text-[10px] text-slate-400 dark:text-slate-500">کلیک برای تغییر عکس</span>
|
||||
<span className="text-[10px] text-[var(--text-3)]">کلیک برای تغییر عکس</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 sm:mb-1">
|
||||
<h1 className="text-xl font-bold text-slate-900 dark:text-slate-50">{displayDoctorName(doctor.name)}</h1>
|
||||
<h1 className="text-xl font-bold text-[var(--text)] dark:text-slate-50">{displayDoctorName(doctor.name)}</h1>
|
||||
<div className="flex items-center gap-2 mt-1.5 flex-wrap">
|
||||
{/* Active status badge */}
|
||||
{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">
|
||||
<CheckCircleIcon className="w-3 h-3" />فعال
|
||||
</span>
|
||||
: <span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-slate-100 dark:bg-gray-700 text-slate-500 dark:text-slate-400">
|
||||
: <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)]">
|
||||
<XCircleIcon className="w-3 h-3" />غیرفعال
|
||||
</span>
|
||||
}
|
||||
{doctor.degree && (
|
||||
<span className="inline-flex text-xs font-medium px-2.5 py-0.5 rounded-full bg-blue-100 dark:bg-blue-400/10 text-blue-700 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)] dark:bg-blue-400/10 text-[var(--info)] dark:text-blue-300">{degreeLabel}</span>
|
||||
)}
|
||||
{doctor.gender && (
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">{doctor.gender === 'man' ? '♂ مرد' : '♀ زن'}</span>
|
||||
<span className="text-xs text-[var(--text-2)]">{doctor.gender === 'man' ? '♂ مرد' : '♀ زن'}</span>
|
||||
)}
|
||||
</div>
|
||||
{doctor.specialties.length > 0 && (
|
||||
@@ -1455,7 +1455,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
{doctor.expertise.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{doctor.expertise.map(e => (
|
||||
<span key={e.id} className="text-[11px] px-2 py-0.5 rounded-full bg-slate-100 dark:bg-gray-700 text-slate-600 dark:text-slate-400 border border-slate-200 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)] dark:border-gray-600">{e.name}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -1477,7 +1477,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
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
|
||||
? '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-emerald-300 dark:border-emerald-500/40 text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10 hover:bg-emerald-100 dark:hover:bg-emerald-500/20'
|
||||
: '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'
|
||||
}`}
|
||||
>
|
||||
{toggleMut.isPending
|
||||
@@ -1495,9 +1495,9 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
<EllipsisVerticalIcon className="w-4 h-4" />
|
||||
</button>
|
||||
{menuOpen && (
|
||||
<div className="absolute left-0 top-12 z-30 w-52 cp-card shadow-2xl border border-slate-200 dark:border-gray-700 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); }}
|
||||
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10">
|
||||
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">
|
||||
<TrashIcon className="w-4 h-4 shrink-0" />حذف این پزشک
|
||||
</button>
|
||||
</div>
|
||||
@@ -1515,7 +1515,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
<div className="lg:col-span-2 space-y-5">
|
||||
|
||||
<div className="cp-card p-6 space-y-4">
|
||||
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300">اطلاعات پایه</h2>
|
||||
<h2 className="text-sm font-semibold text-[var(--text)]">اطلاعات پایه</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<InfoCard icon={UserIcon} label="نام کامل" value={doctor.name} />
|
||||
<InfoCard icon={AcademicCapIcon} label="کد نظام پزشکی" value={doctor.medical_system_code} mono copyable />
|
||||
@@ -1527,8 +1527,8 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
value={
|
||||
<span className="flex items-center gap-2">
|
||||
{doctor.representation
|
||||
? <button type="button" style={{ color: '#2563eb', textDecoration: 'underline', background: 'none', border: 0, padding: 0, cursor: 'pointer', font: 'inherit' }} onClick={() => navigate(`/admin/representations/${doctor.representation!.uuid}`)}>{doctor.representation.full_name ?? 'نماینده'}</button>
|
||||
: <span className="text-slate-400">بدون نماینده</span>}
|
||||
? <button type="button" style={{ color: 'var(--info)', textDecoration: 'underline', background: 'none', border: 0, padding: 0, cursor: 'pointer', font: 'inherit' }} onClick={() => navigate(`/admin/representations/${doctor.representation!.uuid}`)}>{doctor.representation.full_name ?? 'نماینده'}</button>
|
||||
: <span className="text-[var(--text-3)]">بدون نماینده</span>}
|
||||
{isAdmin && (
|
||||
<button type="button" className="btn ghost sm"
|
||||
onClick={() => { setSelRepId(doctor.representation?.id ?? null); setRepModalOpen(true); }}>
|
||||
@@ -1543,15 +1543,15 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
|
||||
{doctor.detail && (
|
||||
<div className="cp-card p-6">
|
||||
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">بیوگرافی</h2>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400 leading-relaxed">{doctor.detail}</p>
|
||||
<h2 className="text-sm font-semibold text-[var(--text)] mb-3">بیوگرافی</h2>
|
||||
<p className="text-sm text-[var(--text-2)] leading-relaxed">{doctor.detail}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Addresses */}
|
||||
<div className="cp-card p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300">
|
||||
<h2 className="text-sm font-semibold text-[var(--text)]">
|
||||
آدرسهای مطب ({formatNumber(addresses.length)})
|
||||
</h2>
|
||||
{!isReadOnly && (
|
||||
@@ -1571,7 +1571,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-slate-400 dark:text-slate-500">
|
||||
<div className="text-center py-8 text-[var(--text-3)]">
|
||||
<MapPinIcon className="w-10 h-10 mx-auto mb-2 opacity-30" />
|
||||
<p className="text-sm">هنوز آدرسی ثبت نشده است</p>
|
||||
{!isReadOnly && (
|
||||
@@ -1590,18 +1590,18 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
|
||||
{doctor.clinics && doctor.clinics.length > 0 && (
|
||||
<div className="cp-card p-6">
|
||||
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">
|
||||
<h2 className="text-sm font-semibold text-[var(--text)] mb-3">
|
||||
کلینیکهای مرتبط ({formatNumber(doctor.clinics.length)})
|
||||
</h2>
|
||||
<div className="space-y-2">
|
||||
{doctor.clinics.map(c => (
|
||||
<div key={c.uuid} className="flex items-start gap-3 p-3 rounded-xl bg-slate-50 dark:bg-gray-800/60">
|
||||
<div className="w-8 h-8 rounded-lg bg-white dark:bg-gray-700 shadow-sm flex items-center justify-center shrink-0">
|
||||
<BuildingOfficeIcon className="w-4 h-4 text-slate-400" />
|
||||
<div key={c.uuid} className="flex items-start gap-3 p-3 rounded-xl bg-[var(--surface-2)] dark:bg-[var(--surface)]/60">
|
||||
<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)]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-800 dark:text-slate-200">{c.name}</p>
|
||||
{c.address && <p className="text-xs text-slate-500 mt-0.5 flex items-center gap-1"><MapPinIcon className="w-3 h-3" />{c.address}</p>}
|
||||
<p className="text-sm font-medium text-[var(--text)]">{c.name}</p>
|
||||
{c.address && <p className="text-xs text-[var(--text-2)] mt-0.5 flex items-center gap-1"><MapPinIcon className="w-3 h-3" />{c.address}</p>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -1613,7 +1613,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-5">
|
||||
<div className="cp-card p-5">
|
||||
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">
|
||||
<h2 className="text-sm font-semibold text-[var(--text)] mb-3">
|
||||
تخصصها ({formatNumber(doctor.specialties.length)})
|
||||
</h2>
|
||||
{doctor.specialties.length > 0
|
||||
@@ -1622,7 +1622,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
<span key={s.id} className={`text-xs font-medium px-2.5 py-1 rounded-lg ${SPECIALTY_COLORS[i % SPECIALTY_COLORS.length]}`}>{s.name}</span>
|
||||
))}
|
||||
</div>
|
||||
: <p className="text-sm text-slate-400">تخصصی ثبت نشده</p>
|
||||
: <p className="text-sm text-[var(--text-3)]">تخصصی ثبت نشده</p>
|
||||
}
|
||||
{!isReadOnly && (
|
||||
<button onClick={() => setEditOpen(true)} className="w-full mt-3 text-xs text-primary-600 dark:text-primary-400 hover:underline text-right">
|
||||
@@ -1632,16 +1632,16 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
</div>
|
||||
|
||||
<div className="cp-card p-5">
|
||||
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">
|
||||
<h2 className="text-sm font-semibold text-[var(--text)] mb-3">
|
||||
خدمات ({formatNumber(doctor.expertise.length)})
|
||||
</h2>
|
||||
{doctor.expertise.length > 0
|
||||
? <div className="flex flex-wrap gap-1.5">
|
||||
{doctor.expertise.map(e => (
|
||||
<span key={e.id} className="text-xs px-2.5 py-1 rounded-lg bg-slate-100 dark:bg-gray-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-gray-700">{e.name}</span>
|
||||
<span key={e.id} className="text-xs px-2.5 py-1 rounded-lg bg-[var(--surface-2)] text-[var(--text-2)] border border-[var(--border)]">{e.name}</span>
|
||||
))}
|
||||
</div>
|
||||
: <p className="text-sm text-slate-400">خدمتی ثبت نشده</p>
|
||||
: <p className="text-sm text-[var(--text-3)]">خدمتی ثبت نشده</p>
|
||||
}
|
||||
{!isReadOnly && (
|
||||
<button onClick={() => setEditOpen(true)} className="w-full mt-3 text-xs text-primary-600 dark:text-primary-400 hover:underline text-right">
|
||||
@@ -1651,30 +1651,30 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
</div>
|
||||
|
||||
<div className="cp-card p-5">
|
||||
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">امتیاز و رضایت</h2>
|
||||
<h2 className="text-sm font-semibold text-[var(--text)] mb-3">امتیاز و رضایت</h2>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1">امتیاز کلی</p>
|
||||
<p className="text-xs text-[var(--text-2)] mb-1">امتیاز کلی</p>
|
||||
<div dir="ltr"><StarRating rate={rateNum} /></div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1">درصد رضایت</p>
|
||||
<p className="text-xs text-[var(--text-2)] mb-1">درصد رضایت</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 h-2 rounded-full bg-slate-200 dark:bg-gray-700">
|
||||
<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>
|
||||
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">{doctor.satisfaction}%</span>
|
||||
<span className="text-sm font-medium text-[var(--text)]">{doctor.satisfaction}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="cp-card p-5">
|
||||
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">UUID</h2>
|
||||
<h2 className="text-sm font-semibold text-[var(--text)] mb-2">UUID</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-[11px] font-mono text-slate-500 dark:text-slate-400 break-all flex-1" dir="ltr">{doctor.uuid}</p>
|
||||
<p className="text-[11px] font-mono text-[var(--text-2)] break-all flex-1" dir="ltr">{doctor.uuid}</p>
|
||||
<button onClick={() => { navigator.clipboard.writeText(doctor.uuid); toast.success('UUID کپی شد'); }}
|
||||
className="w-7 h-7 flex items-center justify-center rounded-lg text-slate-400 hover:text-slate-600 hover:bg-slate-100 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)] transition-colors shrink-0">
|
||||
<ClipboardDocumentIcon className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -1887,7 +1887,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
</>
|
||||
}
|
||||
>
|
||||
<label className="block text-sm text-slate-600 mb-2">انتخاب نماینده</label>
|
||||
<label className="block text-sm text-[var(--text-2)] mb-2">انتخاب نماینده</label>
|
||||
<GlobalSearchableSelect
|
||||
options={repOptions}
|
||||
value={selRepId}
|
||||
|
||||
@@ -429,7 +429,7 @@ export default function DoctorFormPage() {
|
||||
<button type="submit" disabled={createMut.isPending} className="cp-btn-primary" style={{ minWidth: 140 }}>
|
||||
{createMut.isPending ? (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ width: 16, height: 16, border: '2px solid rgba(255,255,255,.3)', borderTopColor: '#fff', borderRadius: '50%', animation: 'spin .7s linear infinite', flexShrink: 0 }} />
|
||||
<span style={{ width: 16, height: 16, border: '2px solid rgba(255,255,255,.3)', borderTopColor: 'var(--surface)', borderRadius: '50%', animation: 'spin .7s linear infinite', flexShrink: 0 }} />
|
||||
در حال ذخیره...
|
||||
</span>
|
||||
) : (
|
||||
|
||||
@@ -42,20 +42,20 @@ export default function EditSessionPage() {
|
||||
return (
|
||||
<div className="fade-in" style={{ width: '100%' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-start', marginBottom: 30 }}>
|
||||
<div className="dark:text-[#A1A1A1]" style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#6B7280', fontSize: 12, padding: '0 16px' }}>
|
||||
<div onClick={() => nav(-1)} className="bg-white dark:bg-[#222433]" style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 8px', borderRadius: 12, cursor: 'pointer' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--text-2)', fontSize: 12, padding: '0 16px' }}>
|
||||
<div onClick={() => nav(-1)} className="bg-[var(--surface)]" style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 8px', borderRadius: 12, cursor: 'pointer' }}>
|
||||
<ArrowLeftPH style={{ width: 18, height: 18, rotate: '180deg' }} />
|
||||
<span>بازگشت</span>
|
||||
</div>
|
||||
<ArrowLeftD />
|
||||
<span>پرونده</span>
|
||||
<ArrowLeftD />
|
||||
<span className="dark:text-[#D7D8ED]" style={{ color: '#111827' }}>{patientName}</span>
|
||||
<span style={{ color: 'var(--text)' }}>{patientName}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
|
||||
<div className="bg-white dark:bg-[#222433]" style={{ width: 748, maxWidth: '100%', padding: 24 }}>
|
||||
<div className="bg-[var(--surface)]" style={{ width: 748, maxWidth: '100%', padding: 24 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
||||
<h2 style={{ fontSize: 16, fontWeight: 700 }}>ویرایش مراجعه</h2>
|
||||
<button type="button" aria-label="بستن" onClick={() => nav(-1)}
|
||||
|
||||
@@ -227,7 +227,7 @@ export default function LoginPage() {
|
||||
flex: 1, padding: '8px 0', borderRadius: 8, border: 'none', cursor: 'pointer',
|
||||
fontFamily: 'inherit', fontSize: 13, fontWeight: 600, transition: 'all 0.15s',
|
||||
background: mode === m ? 'var(--primary)' : 'transparent',
|
||||
color: mode === m ? '#fff' : 'var(--text-2)',
|
||||
color: mode === m ? 'var(--on-primary)' : 'var(--text-2)',
|
||||
}}>
|
||||
{m === 'password' ? 'رمز عبور' : 'ورود با پیامک'}
|
||||
</button>
|
||||
|
||||
@@ -901,7 +901,7 @@ function MyPatientsPageInner() {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "#fff",
|
||||
color: "var(--on-primary)",
|
||||
fontWeight: 700,
|
||||
fontSize: 16,
|
||||
}}
|
||||
@@ -1066,11 +1066,11 @@ function MyPatientsPageInner() {
|
||||
{selectedPatientName}
|
||||
</span>
|
||||
{hasUnpaid ? (
|
||||
<span style={{ background: "rgba(255,192,81,0.15)", color: "#f59e0b", fontSize: 14, fontWeight: 500, padding: "4px 12px", borderRadius: 8 }}>
|
||||
<span style={{ background: "rgba(255,192,81,0.15)", color: "var(--warning)", fontSize: 14, fontWeight: 500, padding: "4px 12px", borderRadius: 8 }}>
|
||||
تکمیل نشده
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ background: "rgba(60,154,79,0.15)", color: "#3C9A4F", fontSize: 14, fontWeight: 500, padding: "4px 12px", borderRadius: 8 }}>
|
||||
<span style={{ background: "rgba(60,154,79,0.15)", color: "var(--success)", fontSize: 14, fontWeight: 500, padding: "4px 12px", borderRadius: 8 }}>
|
||||
تکمیل شده
|
||||
</span>
|
||||
)}
|
||||
@@ -1089,12 +1089,12 @@ function MyPatientsPageInner() {
|
||||
{/* وسط: شماره تماس و تاریخ تشکیل پرونده */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
|
||||
<BannerInfoRow
|
||||
icon={<PhoneArrowUpRightIcon style={{ width: 20, color: "#F17732" }} />}
|
||||
icon={<PhoneArrowUpRightIcon style={{ width: 20, color: "var(--accent)" }} />}
|
||||
label="شماره تماس:"
|
||||
value={<span dir="ltr">{selectedPatientPhone}</span>}
|
||||
/>
|
||||
<BannerInfoRow
|
||||
icon={<CalendarDaysIcon style={{ width: 20, color: "#F17732" }} />}
|
||||
icon={<CalendarDaysIcon style={{ width: 20, color: "var(--accent)" }} />}
|
||||
label="تاریخ تشکیل پرونده:"
|
||||
value={formatDate(selectedRecord.created_at)}
|
||||
/>
|
||||
@@ -1115,7 +1115,7 @@ function MyPatientsPageInner() {
|
||||
<button
|
||||
onClick={() => toast.info("امکان یادداشت بهزودی اضافه میشود")}
|
||||
style={{
|
||||
background: "#F17732", color: "#fff", border: "none", cursor: "pointer",
|
||||
background: "var(--accent)", color: "var(--on-primary)", border: "none", cursor: "pointer",
|
||||
display: "inline-flex", alignItems: "center", gap: 6,
|
||||
padding: "8px 16px", borderRadius: 12, fontSize: 15, fontWeight: 500,
|
||||
}}
|
||||
@@ -1714,7 +1714,7 @@ function patientAvatar(_name: string, size = 32) {
|
||||
style={{
|
||||
width: size, height: size, borderRadius: "50%", flexShrink: 0,
|
||||
background: "var(--accent, #F17732)",
|
||||
color: "#fff", display: "grid", placeItems: "center",
|
||||
color: "var(--on-primary)", display: "grid", placeItems: "center",
|
||||
}}
|
||||
>
|
||||
<UserIcon style={{ width: size * 0.6, height: size * 0.6 }} />
|
||||
@@ -1882,8 +1882,8 @@ function VisitSummaryCard({
|
||||
}}
|
||||
>
|
||||
{paid
|
||||
? <CheckCircleIcon style={{ width: 20, color: "#3C9A4F" }} />
|
||||
: <ClockIcon style={{ width: 20, color: "#F17732" }} />}
|
||||
? <CheckCircleIcon style={{ width: 20, color: "var(--success)" }} />
|
||||
: <ClockIcon style={{ width: 20, color: "var(--accent)" }} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ function Avatar({ name }: { name: string | null }) {
|
||||
<div style={{
|
||||
width: 44, height: 44, borderRadius: '50%', flexShrink: 0,
|
||||
background: 'linear-gradient(145deg, var(--primary), var(--primary-700, var(--primary)))',
|
||||
display: 'grid', placeItems: 'center', color: '#fff', fontSize: 17, fontWeight: 700,
|
||||
display: 'grid', placeItems: 'center', color: 'var(--on-primary)', fontSize: 17, fontWeight: 700,
|
||||
}}>
|
||||
{(name ?? '؟').charAt(0)}
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@ function Avatar({ name }: { name: string | null }) {
|
||||
<div style={{
|
||||
width: 32, height: 32, borderRadius: '50%', flexShrink: 0,
|
||||
background: 'linear-gradient(145deg, var(--primary), var(--primary-700, var(--primary)))',
|
||||
display: 'grid', placeItems: 'center', color: '#fff', fontSize: 13, fontWeight: 700,
|
||||
display: 'grid', placeItems: 'center', color: 'var(--on-primary)', fontSize: 13, fontWeight: 700,
|
||||
}}>
|
||||
{(name ?? '؟').charAt(0)}
|
||||
</div>
|
||||
|
||||
@@ -16,23 +16,23 @@ import type { Secretary, SecretaryPermissions } from "../types";
|
||||
|
||||
const PlusIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M5 10H15" stroke="#EFEFEF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M10 15V5" stroke="#EFEFEF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M5 10H15" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--on-primary)' }}/>
|
||||
<path d="M10 15V5" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--on-primary)' }}/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const EyeIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M12.9833 9.99993C12.9833 11.6499 11.6499 12.9833 9.99993 12.9833C8.34993 12.9833 7.0166 11.6499 7.0166 9.99993C7.0166 8.34993 8.34993 7.0166 9.99993 7.0166C11.6499 7.0166 12.9833 8.34993 12.9833 9.99993Z" stroke="#616161" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M9.99987 16.8918C12.9415 16.8918 15.6832 15.1584 17.5915 12.1584C18.3415 10.9834 18.3415 9.00843 17.5915 7.83343C15.6832 4.83343 12.9415 3.1001 9.99987 3.1001C7.0582 3.1001 4.31654 4.83343 2.4082 7.83343C1.6582 9.00843 1.6582 10.9834 2.4082 12.1584C4.31654 15.1584 7.0582 16.8918 9.99987 16.8918Z" stroke="#616161" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M12.9833 9.99993C12.9833 11.6499 11.6499 12.9833 9.99993 12.9833C8.34993 12.9833 7.0166 11.6499 7.0166 9.99993C7.0166 8.34993 8.34993 7.0166 9.99993 7.0166C11.6499 7.0166 12.9833 8.34993 12.9833 9.99993Z" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--text-2)' }}/>
|
||||
<path d="M9.99987 16.8918C12.9415 16.8918 15.6832 15.1584 17.5915 12.1584C18.3415 10.9834 18.3415 9.00843 17.5915 7.83343C15.6832 4.83343 12.9415 3.1001 9.99987 3.1001C7.0582 3.1001 4.31654 4.83343 2.4082 7.83343C1.6582 9.00843 1.6582 10.9834 2.4082 12.1584C4.31654 15.1584 7.0582 16.8918 9.99987 16.8918Z" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--text-2)' }}/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const EditIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M11.0504 3.00002L4.20878 10.2417C3.95045 10.5167 3.70045 11.0584 3.65045 11.4334L3.34211 14.1334C3.23378 15.1084 3.93378 15.775 4.90045 15.6084L7.58378 15.15C7.95878 15.0834 8.48378 14.8084 8.74211 14.525L15.5838 7.28335C16.7671 6.03335 17.3004 4.60835 15.4588 2.86668C13.6254 1.14168 12.2338 1.75002 11.0504 3.00002Z" stroke="#616161" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M9.9082 4.2085C10.2665 6.5085 12.1332 8.26683 14.4499 8.50016" stroke="#616161" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M2.5 18.3335H17.5" stroke="#616161" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M11.0504 3.00002L4.20878 10.2417C3.95045 10.5167 3.70045 11.0584 3.65045 11.4334L3.34211 14.1334C3.23378 15.1084 3.93378 15.775 4.90045 15.6084L7.58378 15.15C7.95878 15.0834 8.48378 14.8084 8.74211 14.525L15.5838 7.28335C16.7671 6.03335 17.3004 4.60835 15.4588 2.86668C13.6254 1.14168 12.2338 1.75002 11.0504 3.00002Z" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--text-2)' }}/>
|
||||
<path d="M9.9082 4.2085C10.2665 6.5085 12.1332 8.26683 14.4499 8.50016" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--text-2)' }}/>
|
||||
<path d="M2.5 18.3335H17.5" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--text-2)' }}/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -43,7 +43,7 @@ const ChevronDown = ({ open }: { open: boolean }) => (
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
className="text-[#616161] dark:text-[#A1A1A1] transition-transform"
|
||||
className="text-[var(--text-2)] transition-transform"
|
||||
style={{ transform: open ? "rotate(180deg)" : "rotate(0deg)" }}
|
||||
>
|
||||
<path d="M6 9L12 15L18 9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
@@ -56,7 +56,7 @@ function Avatar({ name, size = 32 }: { name?: string; size?: number }) {
|
||||
const firstLetter = (name?.trim().charAt(0) || "?").toUpperCase();
|
||||
return (
|
||||
<div
|
||||
className="rounded-full flex items-center justify-center bg-gradient-to-br from-[#5559CE] to-[#7B7FE8] text-white font-bold flex-shrink-0"
|
||||
className="rounded-full flex items-center justify-center bg-gradient-to-br from-[var(--primary)] to-[var(--primary)] text-[var(--on-primary)] font-bold flex-shrink-0"
|
||||
style={{ width: size, height: size, fontSize: size / 2 }}
|
||||
>
|
||||
{firstLetter}
|
||||
@@ -268,7 +268,7 @@ function PermissionAccordions({
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<p className="text-[#525252] dark:text-[#D7D8ED] text-[16px] font-bold mb-[16px]">
|
||||
<p className="text-[var(--text)] text-[16px] font-bold mb-[16px]">
|
||||
مجوزهای دسترسی
|
||||
</p>
|
||||
<div className="flex flex-col gap-[8px] w-full items-stretch">
|
||||
@@ -278,14 +278,14 @@ function PermissionAccordions({
|
||||
return (
|
||||
<div
|
||||
key={section.key}
|
||||
className="w-full border border-[#DBDBDB] dark:border-[#343645] rounded-[8px] overflow-hidden"
|
||||
className="w-full border border-[var(--border-2)] rounded-[8px] overflow-hidden"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleOpen(section.key)}
|
||||
className="w-full flex items-center justify-between px-[16px] min-h-[64px] cursor-pointer"
|
||||
>
|
||||
<p className="text-[#525252] dark:text-[#D7D8ED] text-[14px] font-medium">
|
||||
<p className="text-[var(--text)] text-[14px] font-medium">
|
||||
{section.title}
|
||||
</p>
|
||||
<ChevronDown open={open} />
|
||||
@@ -306,9 +306,9 @@ function PermissionAccordions({
|
||||
onChange(section.key, item.key, e.target.checked)
|
||||
}
|
||||
className="w-[20px] h-[20px] shrink-0"
|
||||
style={{ accentColor: "#5559CE", cursor: "pointer" }}
|
||||
style={{ accentColor: "var(--primary)", cursor: "pointer" }}
|
||||
/>
|
||||
<span className="text-[#616161] dark:text-[#A1A1A1] text-[14px]">
|
||||
<span className="text-[var(--text-2)] text-[14px]">
|
||||
{item.label}
|
||||
</span>
|
||||
</label>
|
||||
@@ -347,9 +347,9 @@ function DefaultTextField({
|
||||
maxDigits?: number;
|
||||
}) {
|
||||
const cls =
|
||||
"w-full bg-[#FAFAFA] dark:bg-[#222433] rounded-[8px] border border-[#D7D7D7] dark:border-[#343645] " +
|
||||
"text-[#7E7E7E] dark:text-[#D7D8ED] text-[16px] font-normal px-[12px] py-[12.5px] outline-none " +
|
||||
"focus:border-[#5559CE] disabled:opacity-70";
|
||||
"w-full bg-[var(--bg)] dark:bg-[var(--surface)] rounded-[8px] border border-[var(--border-2)] dark:border-[var(--border)] " +
|
||||
"text-[var(--text-2)] dark:text-[var(--text)] text-[16px] font-normal px-[12px] py-[12.5px] outline-none " +
|
||||
"focus:border-[var(--primary)] disabled:opacity-70";
|
||||
if (multiline) {
|
||||
return (
|
||||
<textarea
|
||||
@@ -493,7 +493,7 @@ function SecretaryModal({
|
||||
<>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="border border-[#5559CE] dark:border-transparent dark:bg-[#C7CEF4] text-[#5559CE] dark:text-[#222433]
|
||||
className="border border-[var(--primary)] dark:border-transparent dark:bg-[var(--primary-soft2)] text-[var(--primary)] dark:text-[var(--surface)]
|
||||
h-[40px] md:h-[44px] lg:h-[48px] py-[11px] px-[48px] rounded-[4px] cursor-pointer"
|
||||
>
|
||||
{mode === "view" ? "بستن" : "انصراف"}
|
||||
@@ -502,7 +502,7 @@ function SecretaryModal({
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={saving}
|
||||
className="bg-[#5559CE] text-[#EFEFEF] rounded-[4px] h-[40px] md:h-[44px] lg:h-[48px] py-[11px] px-[48px] cursor-pointer disabled:opacity-60"
|
||||
className="bg-[var(--primary)] text-[var(--on-primary)] rounded-[4px] h-[40px] md:h-[44px] lg:h-[48px] py-[11px] px-[48px] cursor-pointer disabled:opacity-60"
|
||||
>
|
||||
{saving ? "در حال ذخیره..." : "ذخیره"}
|
||||
</button>
|
||||
@@ -515,12 +515,12 @@ function SecretaryModal({
|
||||
{showDoctorPicker && (
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between mb-[16px]">
|
||||
<p className="text-[#525252] dark:text-[#D7D8ED] text-[16px] font-bold">
|
||||
<p className="text-[var(--text)] text-[16px] font-bold">
|
||||
پزشکانِ این منشی
|
||||
</p>
|
||||
<span className="text-[13px] text-[#7E7E7E]">{doctorUuids.length} انتخابشده</span>
|
||||
<span className="text-[13px] text-[var(--text-2)]">{doctorUuids.length} انتخابشده</span>
|
||||
</div>
|
||||
<p className="text-[13px] text-[#7E7E7E] mb-[12px]">
|
||||
<p className="text-[13px] text-[var(--text-2)] mb-[12px]">
|
||||
منشی فقط به نوبتها و اطلاعاتِ پزشکانِ انتخابشده دسترسی خواهد داشت.
|
||||
</p>
|
||||
<DoctorMultiSelect doctors={clinicDoctors} selected={doctorUuids} onChange={setDoctorUuids} />
|
||||
@@ -529,16 +529,16 @@ function SecretaryModal({
|
||||
|
||||
{/* اطلاعات پایه */}
|
||||
<div className="w-full">
|
||||
<p className="text-[#525252] dark:text-[#D7D8ED] text-[16px] font-bold mb-[16px]">
|
||||
<p className="text-[var(--text)] text-[16px] font-bold mb-[16px]">
|
||||
اطلاعات پایه
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-[16px]">
|
||||
<div className="flex w-full flex-col items-start justify-start gap-[12px]">
|
||||
<p className="w-full text-right text-[#525252] dark:text-[#D7D8ED] text-[14px] font-medium">نام</p>
|
||||
<p className="w-full text-right text-[var(--text)] text-[14px] font-medium">نام</p>
|
||||
<DefaultTextField placeholder="نام" value={form.name} onChange={(v) => setField("name", v)} disabled={disabled} />
|
||||
</div>
|
||||
<div className="flex w-full flex-col items-start justify-start gap-[12px]">
|
||||
<p className="w-full text-right text-[#525252] dark:text-[#D7D8ED] text-[14px] font-medium">نام خانوادگی</p>
|
||||
<p className="w-full text-right text-[var(--text)] text-[14px] font-medium">نام خانوادگی</p>
|
||||
<DefaultTextField placeholder="نام خانوادگی" value={form.family} onChange={(v) => setField("family", v)} disabled={disabled} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -546,17 +546,17 @@ function SecretaryModal({
|
||||
|
||||
<div className="w-full grid grid-cols-1 md:grid-cols-2 gap-[16px]">
|
||||
<div className="flex w-full flex-col items-start justify-start gap-[12px]">
|
||||
<p className="w-full text-right text-[#525252] dark:text-[#D7D8ED] text-[14px] font-medium">شماره موبایل</p>
|
||||
<p className="w-full text-right text-[var(--text)] text-[14px] font-medium">شماره موبایل</p>
|
||||
<DefaultTextField numeric maxDigits={11} placeholder="09121234567" value={form.telephone} onChange={(v) => setField("telephone", v)} disabled={disabled || mode === "edit"} />
|
||||
</div>
|
||||
<div className="flex w-full flex-col items-start justify-start gap-[12px]">
|
||||
<p className="w-full text-right text-[#525252] dark:text-[#D7D8ED] text-[14px] font-medium">کد ملی</p>
|
||||
<p className="w-full text-right text-[var(--text)] text-[14px] font-medium">کد ملی</p>
|
||||
<DefaultTextField numeric maxDigits={10} placeholder="کد ملی" value={form.national_code} onChange={(v) => setField("national_code", v)} disabled={disabled} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col items-start justify-start gap-[12px]">
|
||||
<p className="w-full text-right text-[#525252] dark:text-[#D7D8ED] text-[14px] font-medium">آدرس</p>
|
||||
<p className="w-full text-right text-[var(--text)] text-[14px] font-medium">آدرس</p>
|
||||
<DefaultTextField placeholder="آدرس" value={form.address} onChange={(v) => setField("address", v)} disabled={disabled} multiline rows={2} />
|
||||
</div>
|
||||
|
||||
@@ -636,15 +636,15 @@ function SecretaryTable({
|
||||
showDoctor: boolean;
|
||||
}) {
|
||||
const headCls =
|
||||
"text-[#616161] dark:text-[#D7D8ED] text-[14px] font-normal py-[10px] px-[18px] text-start";
|
||||
"text-[var(--text-2)] dark:text-[var(--text)] text-[14px] font-normal py-[10px] px-[18px] text-start";
|
||||
const cellCls =
|
||||
"text-[#616161] dark:text-[#A1A1A1] text-[16px] font-medium px-[18px] py-[10px] whitespace-nowrap";
|
||||
"text-[var(--text-2)] text-[16px] font-medium px-[18px] py-[10px] whitespace-nowrap";
|
||||
return (
|
||||
<div className="hidden lg:block mt-[24px]">
|
||||
<div className="rounded-[8px] overflow-hidden border border-solid border-[#E7E7E7] dark:border-[#35343D]">
|
||||
<div className="rounded-[8px] overflow-hidden border border-solid border-[var(--border)]">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-[#EFEFEF] dark:bg-[#35343D]">
|
||||
<tr className="bg-[var(--surface-2)]">
|
||||
<th className={headCls}>ردیف</th>
|
||||
<th className={headCls}>نام منشی</th>
|
||||
{showDoctor && <th className={headCls}>پزشک</th>}
|
||||
@@ -659,7 +659,7 @@ function SecretaryTable({
|
||||
{data.map((group, idx) => {
|
||||
const row = group.primary;
|
||||
return (
|
||||
<tr key={group.key} className="border-b border-[#DBDBDB] dark:border-[#343645]">
|
||||
<tr key={group.key} className="border-b border-[var(--border-2)]">
|
||||
<td className={cellCls}>{idx + 1}</td>
|
||||
<td className={cellCls}>
|
||||
<div className="flex items-center justify-start gap-[8px]">
|
||||
@@ -682,14 +682,14 @@ function SecretaryTable({
|
||||
{row.is_active ? (
|
||||
<button
|
||||
onClick={() => onDeactivate(group)}
|
||||
className="border border-[#E53935] text-[#E53935] hover:bg-[#FFEBEE] text-[12px] font-medium py-[6px] px-[16px] rounded-[4px] cursor-pointer"
|
||||
className="border border-[var(--danger)] text-[var(--danger)] hover:bg-[var(--danger-bg)] text-[12px] font-medium py-[6px] px-[16px] rounded-[4px] cursor-pointer"
|
||||
>
|
||||
لغو همکاری
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => onDeactivate(group)}
|
||||
className="border border-[#2E7D32] text-[#2E7D32] hover:bg-[#E8F5E9] text-[12px] font-medium py-[6px] px-[16px] rounded-[4px] cursor-pointer"
|
||||
className="border border-[var(--success)] text-[var(--success)] hover:bg-[var(--success-bg)] text-[12px] font-medium py-[6px] px-[16px] rounded-[4px] cursor-pointer"
|
||||
>
|
||||
فعالسازی
|
||||
</button>
|
||||
@@ -727,49 +727,49 @@ function SecretaryCards({
|
||||
return (
|
||||
<div
|
||||
key={group.key}
|
||||
className="bg-[#FFF] dark:bg-[#222433] dark:shadow-transparent rounded-[8px] shadow-[0px_1px_24.8px_0px_rgba(204,204,204,0.18)] p-[12px]"
|
||||
className="bg-[var(--surface)] dark:shadow-transparent rounded-[8px] shadow-[0px_1px_24.8px_0px_rgba(204,204,204,0.18)] p-[12px]"
|
||||
>
|
||||
<div className="flex items-center justify-start gap-[8px]">
|
||||
<Avatar name={item.user_name} size={32} />
|
||||
<p className="text-[#616161] dark:text-[#D7D8ED] text-[14px] font-medium">{item.user_name}</p>
|
||||
<p className="text-[var(--text-2)] text-[14px] font-medium">{item.user_name}</p>
|
||||
</div>
|
||||
<div className="w-full mt-[16px]">
|
||||
{showDoctor && (
|
||||
<>
|
||||
<div className="flex items-start justify-between gap-[8px]">
|
||||
<p className="text-[#7E7E7E] dark:text-[#A1A1A1] text-[14px] font-normal shrink-0">پزشک:</p>
|
||||
<p className="text-[#616161] dark:text-[#A1A1A1] text-[14px] font-normal text-left">
|
||||
<p className="text-[var(--text-2)] text-[14px] font-normal shrink-0">پزشک:</p>
|
||||
<p className="text-[var(--text-2)] text-[14px] font-normal text-left">
|
||||
{group.doctorNames.join("، ")}
|
||||
</p>
|
||||
</div>
|
||||
<span className="block w-full h-px bg-[#EFEFEF] dark:bg-[#343645] my-[8px]" />
|
||||
<span className="block w-full h-px bg-[var(--surface-2)] my-[8px]" />
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[#7E7E7E] dark:text-[#A1A1A1] text-[14px] font-normal">کدملی:</p>
|
||||
<p className="text-[#616161] dark:text-[#A1A1A1] text-[14px] font-normal">{item.national_code || "-"}</p>
|
||||
<p className="text-[var(--text-2)] text-[14px] font-normal">کدملی:</p>
|
||||
<p className="text-[var(--text-2)] text-[14px] font-normal">{item.national_code || "-"}</p>
|
||||
</div>
|
||||
<span className="block w-full h-px bg-[#EFEFEF] dark:bg-[#343645] my-[8px]" />
|
||||
<span className="block w-full h-px bg-[var(--surface-2)] my-[8px]" />
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[#7E7E7E] dark:text-[#A1A1A1] text-[14px] font-normal">تاریخ همکاری:</p>
|
||||
<p className="text-[#616161] dark:text-[#A1A1A1] text-[14px] font-normal">{formatDate(Number(item.created_at))}</p>
|
||||
<p className="text-[var(--text-2)] text-[14px] font-normal">تاریخ همکاری:</p>
|
||||
<p className="text-[var(--text-2)] text-[14px] font-normal">{formatDate(Number(item.created_at))}</p>
|
||||
</div>
|
||||
<span className="block w-full h-px bg-[#EFEFEF] dark:bg-[#343645] my-[8px]" />
|
||||
<span className="block w-full h-px bg-[var(--surface-2)] my-[8px]" />
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[#7E7E7E] dark:text-[#A1A1A1] text-[14px] font-normal">شماره تماس:</p>
|
||||
<p className="text-[#616161] dark:text-[#A1A1A1] text-[14px] font-normal" dir="ltr">{item.mobile_number}</p>
|
||||
<p className="text-[var(--text-2)] text-[14px] font-normal">شماره تماس:</p>
|
||||
<p className="text-[var(--text-2)] text-[14px] font-normal" dir="ltr">{item.mobile_number}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-[16px]">
|
||||
<button
|
||||
onClick={() => onView(group)}
|
||||
className="text-[#5559CE] dark:bg-[#C7CEF4] gap-[5px] text-[14px] font-medium border border-[#5559CE] py-[8px] px-[12px] rounded-[4px] cursor-pointer flex items-center"
|
||||
className="text-[var(--primary)] dark:bg-[var(--primary-soft2)] gap-[5px] text-[14px] font-medium border border-[var(--primary)] py-[8px] px-[12px] rounded-[4px] cursor-pointer flex items-center"
|
||||
>
|
||||
<EyeIcon />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onEdit(group)}
|
||||
className="border border-[#EFEFEF] dark:border-[#343645] rounded-[4px] p-[7px] cursor-pointer"
|
||||
className="border border-[var(--border)] rounded-[4px] p-[7px] cursor-pointer"
|
||||
>
|
||||
<EditIcon />
|
||||
</button>
|
||||
@@ -778,8 +778,8 @@ function SecretaryCards({
|
||||
onClick={() => onDeactivate(group)}
|
||||
className={
|
||||
item.is_active
|
||||
? "w-full border border-[#E53935] text-[#E53935] hover:bg-[#FFEBEE] text-[14px] font-medium py-[8px] mt-[12px] rounded-[4px] cursor-pointer"
|
||||
: "w-full border border-[#2E7D32] text-[#2E7D32] hover:bg-[#E8F5E9] text-[14px] font-medium py-[8px] mt-[12px] rounded-[4px] cursor-pointer"
|
||||
? "w-full border border-[var(--danger)] text-[var(--danger)] hover:bg-[var(--danger-bg)] text-[14px] font-medium py-[8px] mt-[12px] rounded-[4px] cursor-pointer"
|
||||
: "w-full border border-[var(--success)] text-[var(--success)] hover:bg-[var(--success-bg)] text-[14px] font-medium py-[8px] mt-[12px] rounded-[4px] cursor-pointer"
|
||||
}
|
||||
>
|
||||
{item.is_active ? "لغو همکاری" : "فعالسازی"}
|
||||
@@ -816,22 +816,22 @@ function DoctorMultiSelect({
|
||||
const toggleAll = () => onChange(allSelected ? [] : doctors.map((d) => d.uuid));
|
||||
|
||||
return (
|
||||
<div className="w-full border border-[#EFEFEF] dark:border-[#343645] rounded-[8px] overflow-hidden">
|
||||
<div className="w-full border border-[var(--border)] rounded-[8px] overflow-hidden">
|
||||
{/* هدر: جستجو + انتخاب همه */}
|
||||
<div className="flex items-center gap-[8px] p-[10px] border-b border-[#EFEFEF] dark:border-[#343645] bg-[#FAFAFC] dark:bg-[#222433]">
|
||||
<div className="flex items-center gap-[8px] p-[10px] border-b border-[var(--border)] bg-[var(--surface)]">
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" className="flex-shrink-0">
|
||||
<path d="M9 16A7 7 0 109 2a7 7 0 000 14zM18 18l-3.5-3.5" stroke="#9A9AB0" strokeWidth="1.5" strokeLinecap="round" />
|
||||
<path d="M9 16A7 7 0 109 2a7 7 0 000 14zM18 18l-3.5-3.5" style={{ stroke: 'var(--text-3)' }} strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
<input
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
placeholder="جستجوی پزشک..."
|
||||
className="flex-1 text-[13px] bg-transparent outline-none text-[#525252] dark:text-[#D7D8ED]"
|
||||
className="flex-1 text-[13px] bg-transparent outline-none text-[var(--text)]"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleAll}
|
||||
className="text-[12px] text-[#5559CE] font-medium whitespace-nowrap cursor-pointer"
|
||||
className="text-[12px] text-[var(--primary)] font-medium whitespace-nowrap cursor-pointer"
|
||||
>
|
||||
{allSelected ? "لغو همه" : "انتخاب همه"}
|
||||
</button>
|
||||
@@ -839,7 +839,7 @@ function DoctorMultiSelect({
|
||||
|
||||
{/* چیپهای انتخابشده */}
|
||||
{selected.length > 0 && (
|
||||
<div className="flex flex-wrap gap-[6px] p-[10px] border-b border-[#EFEFEF] dark:border-[#343645]">
|
||||
<div className="flex flex-wrap gap-[6px] p-[10px] border-b border-[var(--border)]">
|
||||
{selected.map((uuid) => {
|
||||
const d = doctors.find((x) => x.uuid === uuid);
|
||||
if (!d) return null;
|
||||
@@ -847,7 +847,7 @@ function DoctorMultiSelect({
|
||||
<span
|
||||
key={uuid}
|
||||
onClick={() => toggle(uuid)}
|
||||
className="inline-flex items-center gap-[4px] bg-[#EEF0FF] dark:bg-[#33365A] text-[#5559CE] dark:text-[#C7CEF4] text-[12px] px-[8px] py-[3px] rounded-full cursor-pointer"
|
||||
className="inline-flex items-center gap-[4px] bg-[var(--primary-soft)] text-[var(--primary)] text-[12px] px-[8px] py-[3px] rounded-full cursor-pointer"
|
||||
>
|
||||
{d.name}
|
||||
<span className="text-[14px] leading-none">×</span>
|
||||
@@ -860,7 +860,7 @@ function DoctorMultiSelect({
|
||||
{/* لیست پزشکان */}
|
||||
<div className="max-h-[220px] overflow-y-auto">
|
||||
{filtered.length === 0 ? (
|
||||
<p className="text-center text-[12px] text-[#7E7E7E] py-[14px]">نتیجهای یافت نشد</p>
|
||||
<p className="text-center text-[12px] text-[var(--text-2)] py-[14px]">نتیجهای یافت نشد</p>
|
||||
) : (
|
||||
filtered.map((d) => {
|
||||
const checked = selected.includes(d.uuid);
|
||||
@@ -868,18 +868,18 @@ function DoctorMultiSelect({
|
||||
<label
|
||||
key={d.uuid}
|
||||
className={
|
||||
"flex items-center gap-[10px] px-[12px] py-[9px] cursor-pointer border-b border-[#F2F2F6] dark:border-[#2A2C3A] last:border-b-0 " +
|
||||
(checked ? "bg-[#F5F6FF] dark:bg-[#2A2D45]" : "hover:bg-[#FAFAFC] dark:hover:bg-[#2A2C3A]")
|
||||
"flex items-center gap-[10px] px-[12px] py-[9px] cursor-pointer border-b border-[var(--border)] last:border-b-0 " +
|
||||
(checked ? "bg-[var(--primary-soft)] dark:bg-[var(--surface-3)]" : "hover:bg-[var(--surface)] dark:hover:bg-[var(--surface-2)]")
|
||||
}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="accent-[#5559CE] w-[16px] h-[16px]"
|
||||
className="accent-[var(--primary)] w-[16px] h-[16px]"
|
||||
checked={checked}
|
||||
onChange={() => toggle(d.uuid)}
|
||||
/>
|
||||
<Avatar name={d.name} size={26} />
|
||||
<span className="text-[13px] text-[#525252] dark:text-[#D7D8ED]">{d.name}</span>
|
||||
<span className="text-[13px] text-[var(--text)]">{d.name}</span>
|
||||
</label>
|
||||
);
|
||||
})
|
||||
@@ -1040,12 +1040,12 @@ function MySecretariesPageContent() {
|
||||
return (
|
||||
<div dir="rtl">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[#525252] dark:text-[#D7D8ED] text-[20px] font-bold">لیست منشی ها</p>
|
||||
<p className="text-[var(--text)] text-[20px] font-bold">لیست منشی ها</p>
|
||||
</div>
|
||||
|
||||
{/* تبها + دکمه افزودن */}
|
||||
<div className="w-full flex items-end justify-between mt-[20px]">
|
||||
<div className="flex items-center gap-[8px] border-b border-[#EFEFEF] dark:border-[#343645]">
|
||||
<div className="flex items-center gap-[8px] border-b border-[var(--border)]">
|
||||
{["منشی های فعلی", "منشی های قبلی"].map((label, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
@@ -1053,8 +1053,8 @@ function MySecretariesPageContent() {
|
||||
className={
|
||||
"px-[12px] py-[10px] text-[14px] md:text-[16px] font-bold cursor-pointer border-b-2 -mb-px " +
|
||||
(tab === idx
|
||||
? "text-[#5559CE] border-[#5559CE]"
|
||||
: "text-[#495057] dark:text-[#A1A1A1] border-transparent")
|
||||
? "text-[var(--primary)] border-[var(--primary)]"
|
||||
: "text-[var(--text)] dark:text-[var(--text-2)] border-transparent")
|
||||
}
|
||||
>
|
||||
{label}
|
||||
@@ -1063,7 +1063,7 @@ function MySecretariesPageContent() {
|
||||
</div>
|
||||
<button
|
||||
onClick={handleAddClick}
|
||||
className="shadow-none gap-[8px] bg-[#5559CE] text-[#EFEFEF] text-[14px] md:text-[15px] lg:text-[16px]
|
||||
className="shadow-none gap-[8px] bg-[var(--primary)] text-[var(--on-primary)] text-[14px] md:text-[15px] lg:text-[16px]
|
||||
font-medium py-[10px] px-[16px] h-[43px] md:h-[45px] lg:h-[48px] rounded-[4px] cursor-pointer
|
||||
flex items-center disabled:opacity-60"
|
||||
>
|
||||
@@ -1073,11 +1073,11 @@ function MySecretariesPageContent() {
|
||||
</div>
|
||||
|
||||
{!isClinic && !doctorUuid ? (
|
||||
<div className="mt-[24px] text-center text-[#7E7E7E] py-[40px]">پروفایل پزشک یافت نشد</div>
|
||||
<div className="mt-[24px] text-center text-[var(--text-2)] py-[40px]">پروفایل پزشک یافت نشد</div>
|
||||
) : isLoading ? (
|
||||
<div className="mt-[24px] text-center text-[#7E7E7E] py-[40px]">در حال بارگذاری...</div>
|
||||
<div className="mt-[24px] text-center text-[var(--text-2)] py-[40px]">در حال بارگذاری...</div>
|
||||
) : secretaries.length === 0 ? (
|
||||
<div className="mt-[24px] text-center text-[#7E7E7E] py-[60px]">
|
||||
<div className="mt-[24px] text-center text-[var(--text-2)] py-[60px]">
|
||||
{tab === 0 ? "هنوز منشی فعالی اضافه نشده است" : "منشی قبلیای وجود ندارد"}
|
||||
</div>
|
||||
) : (
|
||||
@@ -1101,7 +1101,7 @@ function MySecretariesPageContent() {
|
||||
|
||||
{atLimit && (
|
||||
<div className="mt-[16px]">
|
||||
<Link to="/admin/subscription" className="text-[#5559CE] text-[13px] underline">
|
||||
<Link to="/admin/subscription" className="text-[var(--primary)] text-[13px] underline">
|
||||
برای افزودن منشی بیشتر، پنل خود را ارتقا دهید
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -54,10 +54,10 @@ export default function NewSessionPage() {
|
||||
<div className="fade-in" style={{ width: '100%' }}>
|
||||
{/* breadcrumb — tauri AddService header */}
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-start', marginBottom: 30 }}>
|
||||
<div className="dark:text-[#A1A1A1]" style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#6B7280', fontSize: 12, padding: '0 16px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--text-2)', fontSize: 12, padding: '0 16px' }}>
|
||||
<div
|
||||
onClick={() => nav(-1)}
|
||||
className="bg-white dark:bg-[#222433]"
|
||||
className="bg-[var(--surface)]"
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 8px', borderRadius: 12, cursor: 'pointer' }}
|
||||
>
|
||||
<ArrowLeftPH style={{ width: 18, height: 18, rotate: '180deg' }} />
|
||||
@@ -66,13 +66,13 @@ export default function NewSessionPage() {
|
||||
<ArrowLeftD />
|
||||
<span>پرونده</span>
|
||||
<ArrowLeftD />
|
||||
<span className="dark:text-[#D7D8ED]" style={{ color: '#111827' }}>{patientName}</span>
|
||||
<span style={{ color: 'var(--text)' }}>{patientName}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* card — tauri width 748 centered */}
|
||||
<div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
|
||||
<div className="bg-white dark:bg-[#222433]" style={{ width: 748, maxWidth: '100%', padding: 24 }}>
|
||||
<div className="bg-[var(--surface)]" style={{ width: 748, maxWidth: '100%', padding: 24 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 8 }}>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -195,10 +195,10 @@ export default function PatientDetailPage() {
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6, whiteSpace: 'nowrap',
|
||||
padding: '10px 14px', border: 'none', background: 'none', cursor: 'pointer',
|
||||
fontFamily: 'inherit', fontSize: 13, fontWeight: on ? 700 : 500,
|
||||
color: on ? '#5559ce' : '#616161',
|
||||
borderBottom: `2px solid ${on ? '#5559ce' : 'transparent'}`,
|
||||
color: on ? 'var(--primary)' : 'var(--text-2)',
|
||||
borderBottom: `2px solid ${on ? 'var(--primary)' : 'transparent'}`,
|
||||
}}>
|
||||
{t.icon(on ? '#5559ce' : '#616161')} {t.label}
|
||||
{t.icon(on ? 'var(--primary)' : 'var(--text-2)')} {t.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -237,8 +237,8 @@ export default function PatientDetailPage() {
|
||||
<button className={sessionFilter === 'archived' ? 'on' : ''} onClick={() => setSessionFilter('archived')}>آرشیو</button>
|
||||
</div>
|
||||
{canUpdate && (
|
||||
<Link to={`/admin/patients/${uuid}/session/new`} className="flex items-center justify-center gap-2 rounded-[8px]" style={{ height: 48, minWidth: 137, background: '#5559ce', color: '#fff', textDecoration: 'none', padding: '0 16px', fontSize: 14 }}>
|
||||
<AddTurn color="#fff" /> سرویس جدید
|
||||
<Link to={`/admin/patients/${uuid}/session/new`} className="flex items-center justify-center gap-2 rounded-[8px]" style={{ height: 48, minWidth: 137, background: 'var(--primary)', color: 'var(--on-primary)', textDecoration: 'none', padding: '0 16px', fontSize: 14 }}>
|
||||
<AddTurn color="var(--on-primary)" /> سرویس جدید
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
@@ -485,9 +485,9 @@ interface Note { uuid: string; body: string; pinned: boolean; author: string | n
|
||||
/** آیکن سنجاق (پین) — پرشده وقتی یادداشت پین است. Heroicons سنجاق ندارد. */
|
||||
function PinIcon({ filled, color, size = 15 }: { filled?: boolean; color: string; size?: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill={filled ? color : 'none'} stroke={color} strokeWidth={filled ? 0 : 1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill={filled ? color : 'none'} stroke="currentColor" strokeWidth={filled ? 0 : 1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" style={{ color }}>
|
||||
<path d="M9 4h6l-1 5 3 3v2H7v-2l3-3-1-5Z" />
|
||||
<line x1="12" y1="14" x2="12" y2="21" stroke={color} strokeWidth={1.8} />
|
||||
<line x1="12" y1="14" x2="12" y2="21" stroke="currentColor" strokeWidth={1.8} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -988,16 +988,16 @@ function AppointmentsTab({ uuid, q }: {
|
||||
<div style={{ width: 320, maxWidth: '100%' }}>
|
||||
<SearchableSelect options={APPT_SORT_OPTS} value={sort} onChange={(v) => setSort(String(v ?? 'newest'))} height={48} />
|
||||
</div>
|
||||
<button type="button" aria-label="فیلتر" className="flex items-center justify-center rounded-[4px] cursor-pointer" style={{ width: 62, height: 48, border: '1px solid #5559ce', background: 'transparent' }}>
|
||||
<TurnsFilter color="#5559ce" />
|
||||
<button type="button" aria-label="فیلتر" className="flex items-center justify-center rounded-[4px] cursor-pointer" style={{ width: 62, height: 48, border: '1px solid var(--primary)', background: 'transparent' }}>
|
||||
<TurnsFilter color="var(--primary)" />
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<Link to="/admin/appointments/reserve" className="flex items-center justify-center gap-2 rounded-[8px]" style={{ height: 48, minWidth: 137, border: '1px solid #5559ce', color: '#5559ce', textDecoration: 'none', padding: '0 16px', fontSize: 14 }}>
|
||||
<AddTurn color="#5559ce" /> نوبت رزرو
|
||||
<Link to="/admin/appointments/reserve" className="flex items-center justify-center gap-2 rounded-[8px]" style={{ height: 48, minWidth: 137, border: '1px solid var(--primary)', color: 'var(--primary)', textDecoration: 'none', padding: '0 16px', fontSize: 14 }}>
|
||||
<AddTurn color="var(--primary)" /> نوبت رزرو
|
||||
</Link>
|
||||
<Link to="/admin/appointments/new" className="flex items-center justify-center gap-2 rounded-[8px]" style={{ height: 48, minWidth: 137, background: '#5559ce', color: '#fff', textDecoration: 'none', padding: '0 16px', fontSize: 14 }}>
|
||||
<AddTurn color="#fff" /> نوبت جدید
|
||||
<Link to="/admin/appointments/new" className="flex items-center justify-center gap-2 rounded-[8px]" style={{ height: 48, minWidth: 137, background: 'var(--primary)', color: 'var(--on-primary)', textDecoration: 'none', padding: '0 16px', fontSize: 14 }}>
|
||||
<AddTurn color="var(--on-primary)" /> نوبت جدید
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,15 +45,15 @@ function PatientCard({ r, onView, onEdit, canUpdate }: { r: PatientRecord; onVie
|
||||
const [menu, setMenu] = useState(false);
|
||||
return (
|
||||
<div
|
||||
className="bg-white dark:bg-[#222433] rounded-[6px] p-[14px] border border-[#EDEDED] dark:border-[#35343D] shadow-sm transition-all duration-300 hover:shadow-md hover:-translate-y-1 cursor-pointer"
|
||||
className="bg-[var(--surface)] rounded-[6px] p-[14px] border border-[var(--border)] shadow-sm transition-all duration-300 hover:shadow-md hover:-translate-y-1 cursor-pointer"
|
||||
onClick={(e) => { if ((e.target as HTMLElement).closest('.more-square-icon')) return; onView(); }}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-[6px] mb-[12px] pb-[8px] border-b border-[#E0E0E0] dark:border-[#404040]">
|
||||
<div className="flex items-center justify-between gap-[6px] mb-[12px] pb-[8px] border-b border-[var(--border-2)]">
|
||||
<div className="flex items-center gap-[6px] flex-1 min-w-0">
|
||||
<span style={{ width: 28, height: 28, borderRadius: '50%', background: 'var(--primary-soft)', display: 'grid', placeItems: 'center', flexShrink: 0 }}>
|
||||
<UserIcon style={{ width: 16, color: 'var(--primary)' }} />
|
||||
</span>
|
||||
<h3 className="text-[#525252] dark:text-[#D7D8ED] text-[14px] font-semibold truncate">{r.user_name || '—'}</h3>
|
||||
<h3 className="text-[var(--text)] text-[14px] font-semibold truncate">{r.user_name || '—'}</h3>
|
||||
</div>
|
||||
<div className="flex-shrink-0 more-square-icon" style={{ position: 'relative' }}>
|
||||
<button
|
||||
@@ -78,20 +78,20 @@ function PatientCard({ r, onView, onEdit, canUpdate }: { r: PatientRecord; onVie
|
||||
|
||||
<div className="mb-[12px] my-4" style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<div className="flex items-start justify-between text-[15px]">
|
||||
<span className="text-[#616161] dark:text-[#A1A1A1] font-medium min-w-[65px]">شماره پرونده:</span>
|
||||
<span className="text-[#525252] dark:text-[#D7D8ED] truncate">{r.record_number || '—'}</span>
|
||||
<span className="text-[var(--text-2)] font-medium min-w-[65px]">شماره پرونده:</span>
|
||||
<span className="text-[var(--text)] truncate">{r.record_number || '—'}</span>
|
||||
</div>
|
||||
<div className="flex items-start justify-between text-[15px]">
|
||||
<span className="text-[#616161] dark:text-[#A1A1A1] font-medium min-w-[65px]">موبایل:</span>
|
||||
<span className="text-[#525252] dark:text-[#D7D8ED] truncate" dir="ltr">{r.user_mobile || '—'}</span>
|
||||
<span className="text-[var(--text-2)] font-medium min-w-[65px]">موبایل:</span>
|
||||
<span className="text-[var(--text)] truncate" dir="ltr">{r.user_mobile || '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex items-center pt-[8px] justify-end gap-[8px] border-t border-[#E0E0E0] dark:border-[#404040]"
|
||||
className="flex items-center pt-[8px] justify-end gap-[8px] border-t border-[var(--border-2)]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span className="text-[#616161] dark:text-[#A1A1A1] text-[14px] min-w-[65px]">برچسبها:</span>
|
||||
<span className="text-[var(--text-2)] text-[14px] min-w-[65px]">برچسبها:</span>
|
||||
<div className="flex-1 flex justify-end"><PatientTagsCell record={r} /></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -140,12 +140,12 @@ export default function PatientsListPage() {
|
||||
|
||||
// نمایش جدولی/کارتی — دو دکمه با آیکون SVG سفارشی (عین tauri ViewModeToggle).
|
||||
const viewToggle = (
|
||||
<div className="flex border border-[#E0E0E0] dark:border-[#404040] rounded-[4px] overflow-hidden shrink-0">
|
||||
<button type="button" aria-label="نمایش جدولی" onClick={() => switchView('table')} style={{ padding: 8, border: 'none', cursor: 'pointer', display: 'grid', placeItems: 'center', background: view === 'table' ? '#f4f5fd' : 'transparent' }}>
|
||||
<PatientsGridView color={view === 'table' ? '#5559ce' : '#616161'} />
|
||||
<div className="flex border border-[var(--border-2)] rounded-[4px] overflow-hidden shrink-0">
|
||||
<button type="button" aria-label="نمایش جدولی" onClick={() => switchView('table')} style={{ padding: 8, border: 'none', cursor: 'pointer', display: 'grid', placeItems: 'center', background: view === 'table' ? 'var(--primary-soft)' : 'transparent' }}>
|
||||
<PatientsGridView color={view === 'table' ? 'var(--primary)' : 'var(--text-2)'} />
|
||||
</button>
|
||||
<button type="button" aria-label="نمایش کارتی" onClick={() => switchView('card')} style={{ padding: 8, border: 'none', cursor: 'pointer', display: 'grid', placeItems: 'center', background: view === 'card' ? '#f4f5fd' : 'transparent' }}>
|
||||
<PatientsCategoryView color={view === 'card' ? '#5559ce' : '#616161'} />
|
||||
<button type="button" aria-label="نمایش کارتی" onClick={() => switchView('card')} style={{ padding: 8, border: 'none', cursor: 'pointer', display: 'grid', placeItems: 'center', background: view === 'card' ? 'var(--primary-soft)' : 'transparent' }}>
|
||||
<PatientsCategoryView color={view === 'card' ? 'var(--primary)' : 'var(--text-2)'} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@@ -153,7 +153,7 @@ export default function PatientsListPage() {
|
||||
return (
|
||||
<div className="fade-in">
|
||||
{/* Head — تیتر تنها (tauri files/head) */}
|
||||
<p className="text-[#525252] dark:text-[#D7D8ED] text-[16px] md:text-[18px] lg:text-[20px] font-bold">پروندهها</p>
|
||||
<p className="text-[var(--text)] text-[16px] md:text-[18px] lg:text-[20px] font-bold">پروندهها</p>
|
||||
|
||||
{/* Inputs — ردیف کنترلها (tauri files/inputs) */}
|
||||
<div className="my-[16px] md:my-[20px] lg:my-[24px]">
|
||||
@@ -161,16 +161,16 @@ export default function PatientsListPage() {
|
||||
{/* جستجو + سوییچ نما */}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 flex-1 w-full">
|
||||
<div className="w-full md:w-[442px]">
|
||||
<div className="flex items-center rounded-[6px] border border-[#EFEFEF] dark:border-[#35343D] bg-[#FAFAFA] dark:bg-[#222433]" style={{ height: 48, paddingInline: 4 }}>
|
||||
<div className="flex items-center rounded-[6px] border border-[var(--border)] bg-[var(--bg)]" style={{ height: 48, paddingInline: 4 }}>
|
||||
<input
|
||||
dir="rtl"
|
||||
value={search}
|
||||
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
|
||||
placeholder="کد ملی، شماره پرونده یا نام مراجعه کننده را وارد کنید..."
|
||||
className="text-[#525252] dark:text-[#D7D8ED]"
|
||||
className="text-[var(--text)]"
|
||||
style={{ flex: 1, minWidth: 0, background: 'transparent', border: 'none', outline: 'none', fontSize: 13, textAlign: 'left' }}
|
||||
/>
|
||||
<span style={{ width: 40, height: 40, marginInlineEnd: -4, flexShrink: 0, borderRadius: 6, background: '#5559CE', display: 'grid', placeItems: 'center' }}>
|
||||
<span style={{ width: 40, height: 40, marginInlineEnd: -4, flexShrink: 0, borderRadius: 6, background: 'var(--primary)', display: 'grid', placeItems: 'center' }}>
|
||||
<SearchHeaderP color="white" />
|
||||
</span>
|
||||
</div>
|
||||
@@ -183,21 +183,21 @@ export default function PatientsListPage() {
|
||||
<button
|
||||
type="button" aria-label="فیلترها" onClick={() => setFilterOpen(true)}
|
||||
className="flex items-center justify-center rounded-[4px] cursor-pointer"
|
||||
style={{ width: 62, height: 48, border: '1px solid #5559ce', background: 'transparent', position: 'relative' }}
|
||||
style={{ width: 62, height: 48, border: '1px solid var(--primary)', background: 'transparent', position: 'relative' }}
|
||||
>
|
||||
<TurnsFilter color="#5559ce" />
|
||||
<TurnsFilter color="var(--primary)" />
|
||||
{activeFilters > 0 && (
|
||||
<span style={{ position: 'absolute', top: -6, insetInlineEnd: -6, minWidth: 16, height: 16, padding: '0 4px', borderRadius: 999, background: '#5559ce', color: '#fff', fontSize: 10, display: 'grid', placeItems: 'center' }}>{formatNumber(activeFilters)}</span>
|
||||
<span style={{ position: 'absolute', top: -6, insetInlineEnd: -6, minWidth: 16, height: 16, padding: '0 4px', borderRadius: 999, background: 'var(--primary)', color: 'var(--on-primary)', fontSize: 10, display: 'grid', placeItems: 'center' }}>{formatNumber(activeFilters)}</span>
|
||||
)}
|
||||
</button>
|
||||
{canCreate && (
|
||||
<button
|
||||
type="button" onClick={() => navigate('/admin/patients/new')}
|
||||
className="flex items-center justify-center gap-2 rounded-[4px] cursor-pointer"
|
||||
style={{ height: 48, minWidth: 137, background: '#5559ce', border: 'none', padding: '0 16px' }}
|
||||
style={{ height: 48, minWidth: 137, background: 'var(--primary)', border: 'none', padding: '0 16px' }}
|
||||
>
|
||||
<AddTurn color="#fff" />
|
||||
<span style={{ color: '#fff', fontSize: 14 }}>تشکیل پرونده</span>
|
||||
<AddTurn color="var(--on-primary)" />
|
||||
<span style={{ color: 'var(--on-primary)', fontSize: 14 }}>تشکیل پرونده</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function PaymentDetailPage() {
|
||||
]}
|
||||
action={
|
||||
<button onClick={() => navigate('/admin/payments')}
|
||||
className="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-100 transition-colors">
|
||||
className="flex items-center gap-2 text-sm text-[var(--text-2)] hover:text-[var(--text)] transition-colors">
|
||||
<ArrowRightIcon className="w-4 h-4" />
|
||||
بازگشت
|
||||
</button>
|
||||
@@ -82,7 +82,7 @@ export default function PaymentDetailPage() {
|
||||
</div>
|
||||
) : payment ? (
|
||||
<div className="max-w-lg">
|
||||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6">
|
||||
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm p-6">
|
||||
<InfoRow label="شناسه" value={<span dir="ltr" className="font-mono text-xs">{payment.uuid}</span>} />
|
||||
<InfoRow label="شماره سفارش" value={payment.order_id ? <span dir="ltr" className="font-mono text-xs">{payment.order_id}</span> : null} />
|
||||
<InfoRow label="بیمار" value={payment.patient_name} />
|
||||
@@ -109,7 +109,7 @@ export default function PaymentDetailPage() {
|
||||
</div>
|
||||
|
||||
{payment.refunds && payment.refunds.length > 0 && (
|
||||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6 mt-4">
|
||||
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm p-6 mt-4">
|
||||
<h3 className="text-sm font-semibold mb-3">استردادها</h3>
|
||||
{payment.refunds.map((r, i) => (
|
||||
<InfoRow
|
||||
@@ -118,7 +118,7 @@ export default function PaymentDetailPage() {
|
||||
value={
|
||||
<span>
|
||||
{formatRial(r.amount)}
|
||||
{r.ref && <span dir="ltr" className="font-mono text-xs text-gray-400"> ({r.ref})</span>}
|
||||
{r.ref && <span dir="ltr" className="font-mono text-xs text-[var(--text-3)]"> ({r.ref})</span>}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
@@ -153,7 +153,7 @@ export default function PaymentDetailPage() {
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white rounded-2xl border border-gray-100 p-16 text-center text-gray-400">
|
||||
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] p-16 text-center text-[var(--text-3)]">
|
||||
پرداختی یافت نشد
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -90,7 +90,7 @@ export default function RepresentationBlogFormPage() {
|
||||
<div>
|
||||
<label>عنوان مقاله *</label>
|
||||
<input {...register('title')} className="cp-input h-11" placeholder="عنوان جذاب بنویسید..." />
|
||||
{errors.title && <p className="text-red-500 text-xs mt-1">{errors.title.message}</p>}
|
||||
{errors.title && <p className="text-[var(--danger)] text-xs mt-1">{errors.title.message}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<label>خلاصه</label>
|
||||
@@ -112,7 +112,7 @@ export default function RepresentationBlogFormPage() {
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{errors.body && <p className="text-red-500 text-xs mt-1">{errors.body.message}</p>}
|
||||
{errors.body && <p className="text-[var(--danger)] text-xs mt-1">{errors.body.message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ interface UnassignedDoctor {
|
||||
function DetailRow({ label, value }: { label: string; value: React.ReactNode }) {
|
||||
return (
|
||||
<div className="cp-info-row items-start gap-4">
|
||||
<span className="text-sm text-gray-500 w-40 shrink-0">{label}</span>
|
||||
<span className="text-sm text-[var(--text-2)] w-40 shrink-0">{label}</span>
|
||||
<span className="cp-info-value">{value ?? '—'}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -191,10 +191,10 @@ export default function RepresentationDetailPage() {
|
||||
{ label: 'جزئیات' },
|
||||
]}
|
||||
/>
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6 animate-pulse">
|
||||
<div className="bg-[var(--surface)] rounded-2xl shadow-sm border border-[var(--border)] p-6 animate-pulse">
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<div key={i} className="h-8 bg-gray-100 rounded" />
|
||||
<div key={i} className="h-8 bg-[var(--surface-2)] rounded" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -212,7 +212,7 @@ export default function RepresentationDetailPage() {
|
||||
{ label: 'نمایندگان', to: '/admin/representations' },
|
||||
]}
|
||||
/>
|
||||
<div className="cp-card p-12 text-center text-slate-400 dark:text-slate-500">
|
||||
<div className="cp-card p-12 text-center text-[var(--text-3)]">
|
||||
نمایندهای با این شناسه یافت نشد.
|
||||
</div>
|
||||
</div>
|
||||
@@ -237,8 +237,8 @@ export default function RepresentationDetailPage() {
|
||||
disabled={toggleActiveMutation.isPending}
|
||||
className={`flex items-center gap-1.5 px-3 py-2 text-sm rounded-[10px] border transition-colors disabled:opacity-50 ${
|
||||
isActive
|
||||
? 'border-red-300 text-red-600 hover:bg-red-50'
|
||||
: 'border-green-300 text-green-600 hover:bg-green-50'
|
||||
? 'border-[var(--danger)] text-[var(--danger)] hover:bg-[var(--danger-bg)]'
|
||||
: 'border-[var(--success)] text-[var(--success)] hover:bg-[var(--success-bg)]'
|
||||
}`}
|
||||
>
|
||||
{isActive
|
||||
@@ -248,14 +248,14 @@ export default function RepresentationDetailPage() {
|
||||
</button>
|
||||
<button
|
||||
onClick={openEdit}
|
||||
className="flex items-center gap-1.5 px-3 py-2 text-sm rounded-[10px] border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors"
|
||||
className="flex items-center gap-1.5 px-3 py-2 text-sm rounded-[10px] border border-[var(--border-2)] text-[var(--text)] hover:bg-[var(--surface-2)] transition-colors"
|
||||
>
|
||||
<PencilIcon className="w-4 h-4" />
|
||||
ویرایش
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDeleteOpen(true)}
|
||||
className="flex items-center gap-1.5 px-3 py-2 text-sm rounded-[10px] border border-red-300 text-red-600 hover:bg-red-50 transition-colors"
|
||||
className="flex items-center gap-1.5 px-3 py-2 text-sm rounded-[10px] border border-[var(--danger)] text-[var(--danger)] hover:bg-[var(--danger-bg)] transition-colors"
|
||||
>
|
||||
<TrashIcon className="w-4 h-4" />
|
||||
حذف
|
||||
@@ -275,7 +275,7 @@ export default function RepresentationDetailPage() {
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Basic Info */}
|
||||
<div className="cp-card p-6">
|
||||
<h2 className="text-sm font-semibold text-gray-700 mb-4">اطلاعات پایه</h2>
|
||||
<h2 className="text-sm font-semibold text-[var(--text)] mb-4">اطلاعات پایه</h2>
|
||||
<DetailRow label="نام کامل" value={rep.full_name} />
|
||||
<DetailRow label="موبایل" value={
|
||||
rep.mobile_number
|
||||
@@ -290,7 +290,7 @@ export default function RepresentationDetailPage() {
|
||||
|
||||
{/* Bank Account */}
|
||||
<div className="cp-card p-6">
|
||||
<h2 className="text-sm font-semibold text-gray-700 mb-4">اطلاعات بانکی</h2>
|
||||
<h2 className="text-sm font-semibold text-[var(--text)] mb-4">اطلاعات بانکی</h2>
|
||||
{rep.bank_account ? (
|
||||
<>
|
||||
<DetailRow label="شماره کارت" value={
|
||||
@@ -306,7 +306,7 @@ export default function RepresentationDetailPage() {
|
||||
} />
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-gray-400 text-center py-6">اطلاعات بانکی ثبت نشده</p>
|
||||
<p className="text-sm text-[var(--text-3)] text-center py-6">اطلاعات بانکی ثبت نشده</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -316,7 +316,7 @@ export default function RepresentationDetailPage() {
|
||||
<div className="flex items-center justify-between mb-4 flex-wrap gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<UserGroupIcon className="w-5 h-5 text-[var(--text-3)]" />
|
||||
<h2 className="text-sm font-semibold text-gray-700">پزشکان این نماینده</h2>
|
||||
<h2 className="text-sm font-semibold text-[var(--text)]">پزشکان این نماینده</h2>
|
||||
<span className="badge violet">{formatNumber(repDoctorsTotal)}</span>
|
||||
</div>
|
||||
<button className="btn primary sm inline-flex items-center gap-1" onClick={() => setAttachOpen(true)}>
|
||||
@@ -324,11 +324,11 @@ export default function RepresentationDetailPage() {
|
||||
</button>
|
||||
</div>
|
||||
{doctorsQuery.isLoading ? (
|
||||
<p className="text-sm text-gray-400 text-center py-8">در حال بارگذاری…</p>
|
||||
<p className="text-sm text-[var(--text-3)] text-center py-8">در حال بارگذاری…</p>
|
||||
) : repDoctors.length === 0 ? (
|
||||
<div className="text-center py-10">
|
||||
<UserGroupIcon className="w-10 h-10 mx-auto text-gray-300 mb-2" />
|
||||
<p className="text-sm text-gray-400">این نماینده هنوز پزشکی ندارد</p>
|
||||
<UserGroupIcon className="w-10 h-10 mx-auto text-[var(--text-3)] mb-2" />
|
||||
<p className="text-sm text-[var(--text-3)]">این نماینده هنوز پزشکی ندارد</p>
|
||||
<button className="btn primary sm mt-3 inline-flex items-center gap-1" onClick={() => setAttachOpen(true)}>
|
||||
<PlusIcon className="w-4 h-4" /> افزودن اولین پزشک
|
||||
</button>
|
||||
@@ -366,7 +366,7 @@ export default function RepresentationDetailPage() {
|
||||
<div className="flex items-center justify-between mb-4 flex-wrap gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarDaysIcon className="w-5 h-5 text-[var(--text-3)]" />
|
||||
<h2 className="text-sm font-semibold text-gray-700">نوبتهای این نماینده</h2>
|
||||
<h2 className="text-sm font-semibold text-[var(--text)]">نوبتهای این نماینده</h2>
|
||||
{!appointmentsQuery.isLoading && <span className="badge blue">{formatNumber(repAppointmentsTotal)}</span>}
|
||||
</div>
|
||||
<div className="seg">
|
||||
@@ -375,11 +375,11 @@ export default function RepresentationDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
{appointmentsQuery.isLoading ? (
|
||||
<p className="text-sm text-gray-400 text-center py-8">در حال بارگذاری…</p>
|
||||
<p className="text-sm text-[var(--text-3)] text-center py-8">در حال بارگذاری…</p>
|
||||
) : repAppointments.length === 0 ? (
|
||||
<div className="text-center py-10">
|
||||
<CalendarDaysIcon className="w-10 h-10 mx-auto text-gray-300 mb-2" />
|
||||
<p className="text-sm text-gray-400">
|
||||
<CalendarDaysIcon className="w-10 h-10 mx-auto text-[var(--text-3)] mb-2" />
|
||||
<p className="text-sm text-[var(--text-3)]">
|
||||
{apptScope === 'upcoming' ? 'نوبت آیندهای ثبت نشده است' : 'نوبت گذشتهای وجود ندارد'}
|
||||
</p>
|
||||
</div>
|
||||
@@ -422,16 +422,16 @@ export default function RepresentationDetailPage() {
|
||||
onChange={(e) => setAttachSearch(e.target.value)}
|
||||
/>
|
||||
{unassignedQuery.isLoading ? (
|
||||
<p className="text-sm text-gray-400 text-center py-6">در حال بارگذاری…</p>
|
||||
<p className="text-sm text-[var(--text-3)] text-center py-6">در حال بارگذاری…</p>
|
||||
) : unassignedDoctors.length === 0 ? (
|
||||
<p className="text-sm text-gray-400 text-center py-6">پزشکِ بدون نمایندهای یافت نشد</p>
|
||||
<p className="text-sm text-[var(--text-3)] text-center py-6">پزشکِ بدون نمایندهای یافت نشد</p>
|
||||
) : (
|
||||
<div style={{ maxHeight: 360, overflowY: 'auto' }}>
|
||||
{unassignedDoctors.map((d) => (
|
||||
<div key={d.uuid} className="flex items-center justify-between py-2 border-b border-gray-100">
|
||||
<div key={d.uuid} className="flex items-center justify-between py-2 border-b border-[var(--border)]">
|
||||
<div>
|
||||
<b className="text-sm">{d.name}</b>
|
||||
<div className="text-xs text-gray-400" dir="ltr">{d.medical_code ?? d.mobile ?? '—'}</div>
|
||||
<div className="text-xs text-[var(--text-3)]" dir="ltr">{d.medical_code ?? d.mobile ?? '—'}</div>
|
||||
</div>
|
||||
<button className="btn primary sm" disabled={attachMutation.isPending}
|
||||
onClick={() => attachMutation.mutate(d.uuid)}>افزودن</button>
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function SelectContextPage() {
|
||||
padding: '.875rem 1rem',
|
||||
border: '1.5px solid var(--border, #e2e8f0)',
|
||||
borderRadius: '0.625rem',
|
||||
background: loading === ctx.db_uuid ? 'var(--surface-alt, #f4f6f9)' : '#fff',
|
||||
background: loading === ctx.db_uuid ? 'var(--surface-alt, #f4f6f9)' : 'var(--surface)',
|
||||
cursor: loading !== null ? 'wait' : 'pointer',
|
||||
textAlign: 'right',
|
||||
transition: 'border-color .15s, box-shadow .15s',
|
||||
|
||||
@@ -57,10 +57,10 @@ export default function SessionPaymentPage() {
|
||||
<div className="fade-in" style={{ width: '100%' }}>
|
||||
{/* breadcrumb — tauri AddService header */}
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-start', marginBottom: 30 }}>
|
||||
<div className="dark:text-[#A1A1A1]" style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#6B7280', fontSize: 12, padding: '0 16px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--text-2)', fontSize: 12, padding: '0 16px' }}>
|
||||
<div
|
||||
onClick={() => nav(-1)}
|
||||
className="bg-white dark:bg-[#222433]"
|
||||
className="bg-[var(--surface)]"
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 8px', borderRadius: 12, cursor: 'pointer' }}
|
||||
>
|
||||
<ArrowLeftPH style={{ width: 18, height: 18, rotate: '180deg' }} />
|
||||
@@ -69,13 +69,13 @@ export default function SessionPaymentPage() {
|
||||
<ArrowLeftD />
|
||||
<span>پرونده</span>
|
||||
<ArrowLeftD />
|
||||
<span className="dark:text-[#D7D8ED]" style={{ color: '#111827' }}>{patientName}</span>
|
||||
<span style={{ color: 'var(--text)' }}>{patientName}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* card — tauri width 748 centered */}
|
||||
<div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
|
||||
<div className="bg-white dark:bg-[#222433]" style={{ width: 748, maxWidth: '100%', padding: 24 }}>
|
||||
<div className="bg-[var(--surface)]" style={{ width: 748, maxWidth: '100%', padding: 24 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 8 }}>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -382,7 +382,7 @@ export default function SmsPage() {
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
minWidth: 18, height: 18, padding: '0 4px', borderRadius: 9,
|
||||
fontSize: 10, fontWeight: 700,
|
||||
background: 'oklch(0.62 0.22 30)', color: '#fff', lineHeight: 1,
|
||||
background: 'oklch(0.62 0.22 30)', color: 'var(--on-primary)', lineHeight: 1,
|
||||
}}>
|
||||
{t.badge}
|
||||
</span>
|
||||
|
||||
@@ -117,7 +117,7 @@ function SmsWalletPageInner() {
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, oklch(0.52 0.22 256), oklch(0.40 0.18 256))',
|
||||
borderRadius: 'var(--r)', padding: '24px 24px 20px',
|
||||
color: '#fff', position: 'relative', overflow: 'hidden', gridColumn: '1 / 2',
|
||||
color: 'var(--on-primary)', position: 'relative', overflow: 'hidden', gridColumn: '1 / 2',
|
||||
}}>
|
||||
<div style={{
|
||||
position: 'absolute', top: -30, left: -30,
|
||||
@@ -147,14 +147,14 @@ function SmsWalletPageInner() {
|
||||
<div style={{
|
||||
height: '100%', borderRadius: 8,
|
||||
width: `${Math.min(100, (smsCount / 500) * 100)}%`,
|
||||
background: isLowBalance ? '#ef4444' : 'rgba(255,255,255,0.85)',
|
||||
background: isLowBalance ? 'var(--danger)' : 'rgba(255,255,255,0.85)',
|
||||
transition: 'width 0.4s ease',
|
||||
}} />
|
||||
</div>
|
||||
{canCreate && (
|
||||
<button
|
||||
style={{
|
||||
background: '#fff', color: 'var(--primary)', border: 'none', borderRadius: 8,
|
||||
background: 'var(--surface)', color: 'var(--primary)', border: 'none', borderRadius: 8,
|
||||
padding: '8px 18px', fontWeight: 700, fontSize: 13.5, cursor: 'pointer',
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
}}
|
||||
|
||||
@@ -44,9 +44,9 @@ const PLAN_META: Record<string, { label: string; desc: string; tint: string }> =
|
||||
|
||||
/** Gradient blur behind each plan card — matches the tauri source per plan tier. */
|
||||
const PLAN_GRADIENT: Record<string, string> = {
|
||||
free: 'radial-gradient(circle, #f5ecfd 0%, #e9eaf9 70%, transparent 100%)',
|
||||
basic: 'radial-gradient(circle, #e2eee5 0%, #f5edfd 60%, transparent 100%)',
|
||||
professional: 'linear-gradient(180deg, #f9eedd 0%, #e8e9fa 0%, rgba(255,255,255,0) 100%)',
|
||||
free: 'radial-gradient(circle, var(--violet-bg) 0%, var(--primary-soft) 70%, transparent 100%)',
|
||||
basic: 'radial-gradient(circle, var(--success-bg) 0%, var(--violet-bg) 60%, transparent 100%)',
|
||||
professional: 'linear-gradient(180deg, var(--warning-bg) 0%, var(--primary-soft) 0%, transparent 100%)',
|
||||
};
|
||||
|
||||
export const planMetaOf = (name: string) => PLAN_META[name] ?? PLAN_META.free;
|
||||
@@ -305,7 +305,7 @@ function CurrentPlanCard({ my, onRenew }: { my: MySubscriptionData['subscription
|
||||
{daysLeft < 3 && (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 4 }}>
|
||||
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
||||
<InfoCircleRedFilled color="#d22d32" />
|
||||
<InfoCircleRedFilled color="var(--danger)" />
|
||||
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>
|
||||
{daysLeft <= 0 ? 'اشتراک شما به پایان رسیده است' : `${formatNumber(daysLeft)} روز تا پایان اشتراک`}
|
||||
</span>
|
||||
@@ -380,7 +380,7 @@ function PlanCard({
|
||||
data-testid={`plan-card-${plan.name}`}
|
||||
style={{
|
||||
width: '100%', minWidth: 0, height: 522, borderRadius: 10,
|
||||
border: isSelected ? '2px solid #494cb3' : '2px solid var(--border)',
|
||||
border: isSelected ? '2px solid var(--primary-600)' : '2px solid var(--border)',
|
||||
background: 'var(--surface)', display: 'flex', flexDirection: 'column',
|
||||
padding: 16, position: 'relative', overflow: 'hidden',
|
||||
}}
|
||||
@@ -397,11 +397,11 @@ function PlanCard({
|
||||
{isPopular && (
|
||||
<div style={{
|
||||
position: 'absolute', width: 'fit-content', height: 32, top: -18, right: -20,
|
||||
background: '#3d4395', color: '#fff', padding: '0 16px', borderRadius: '0 10px 0 10px',
|
||||
background: 'var(--primary-700)', color: 'var(--on-primary)', padding: '0 16px', borderRadius: '0 10px 0 10px',
|
||||
fontSize: 13, fontWeight: 500, display: 'flex', alignItems: 'center', gap: 8, zIndex: 1,
|
||||
}}>
|
||||
محبوبترین
|
||||
<SparkleIcon size={15} color="#FDD835" />
|
||||
<SparkleIcon size={15} color="var(--warning)" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -490,8 +490,8 @@ function PlanCard({
|
||||
</span>
|
||||
<span style={{ position: 'relative', display: 'inline-flex', width: 22, height: 22 }}>
|
||||
{enabled
|
||||
? <CheckCircleIcon style={{ width: 22, height: 22, color: '#3c9a4f' }} />
|
||||
: <CloseCircleFilled size={22} color="#d7d7d7" />}
|
||||
? <CheckCircleIcon style={{ width: 22, height: 22, color: 'var(--success)' }} />
|
||||
: <CloseCircleFilled size={22} color="var(--border-2)" />}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -507,7 +507,7 @@ function PlanCard({
|
||||
}}>
|
||||
<button
|
||||
onClick={onTrial} disabled={trialPending}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 16, fontWeight: 500, color: '#5559ce' }}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 16, fontWeight: 500, color: 'var(--primary)' }}
|
||||
>
|
||||
{trialPending ? 'در حال فعالسازی...' : 'فعال سازی'}
|
||||
</button>
|
||||
@@ -519,7 +519,7 @@ function PlanCard({
|
||||
</div>
|
||||
</div>
|
||||
<svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', pointerEvents: 'none' }} viewBox="0 0 268 59" preserveAspectRatio="none">
|
||||
<rect x="0.5" y="0.5" width="267" height="58" rx="6" fill="transparent" stroke="#636ed0" strokeWidth="1" strokeDasharray="6 4" />
|
||||
<rect x="0.5" y="0.5" width="267" height="58" rx="6" fill="transparent" strokeWidth="1" strokeDasharray="6 4" style={{ stroke: 'var(--primary)' }}/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
@@ -541,7 +541,7 @@ function PlanCard({
|
||||
disabled={!selectedPeriod}
|
||||
style={{
|
||||
marginTop: 'auto', width: '100%', height: 48, borderRadius: 4, border: 'none',
|
||||
background: 'var(--primary)', color: '#fff', fontWeight: 700, fontSize: 16,
|
||||
background: 'var(--primary)', color: 'var(--on-primary)', fontWeight: 700, fontSize: 16,
|
||||
fontFamily: 'inherit', cursor: selectedPeriod ? 'pointer' : 'not-allowed',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -72,10 +72,10 @@ const MEDICAL_SECTIONS: {
|
||||
|
||||
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' },
|
||||
doctor: { label: 'پزشک', cls: 'bg-blue-100 dark:bg-blue-400/10 text-blue-700 dark:text-blue-300', dot: '#3b82f6' },
|
||||
doctor: { label: 'پزشک', cls: 'bg-[var(--info-bg)] dark:bg-blue-400/10 text-[var(--info)] dark:text-blue-300', dot: '#3b82f6' },
|
||||
secretary: { label: 'منشی', cls: 'bg-orange-100 dark:bg-orange-400/10 text-orange-700 dark:text-orange-300', dot: '#f97316' },
|
||||
clinic: { label: 'کلینیک', cls: 'bg-emerald-100 dark:bg-emerald-400/10 text-emerald-700 dark:text-emerald-300', dot: '#10b981' },
|
||||
patient: { label: 'بیمار', cls: 'bg-slate-100 dark:bg-slate-400/10 text-slate-600 dark:text-slate-400', dot: '#94a3b8' },
|
||||
patient: { label: 'بیمار', cls: 'bg-[var(--surface-2)] dark:bg-slate-400/10 text-[var(--text-2)] dark:text-[var(--text-3)]', dot: '#94a3b8' },
|
||||
};
|
||||
|
||||
const ROLE_MAP: Record<string, string[]> = {
|
||||
@@ -107,7 +107,7 @@ function BigAvatar({ name, id }: { name: string | null; id: number }) {
|
||||
? name.split(' ').filter(Boolean).map(w => w[0]).join('').toUpperCase().slice(0, 2)
|
||||
: '؟';
|
||||
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-white text-2xl font-bold shadow-lg shrink-0`}>
|
||||
<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`}>
|
||||
{initials}
|
||||
</div>
|
||||
);
|
||||
@@ -124,21 +124,21 @@ function InfoCard({ icon: Icon, label, value, mono = false, copyable = false }:
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="flex items-start gap-3 p-4 rounded-xl bg-slate-50 dark:bg-gray-800/60 hover:bg-slate-100 dark:hover:bg-gray-800 transition-colors group">
|
||||
<div className="w-9 h-9 rounded-lg bg-white dark:bg-gray-700 shadow-sm flex items-center justify-center shrink-0">
|
||||
<Icon className="w-4.5 h-4.5 text-slate-500 dark:text-slate-400" />
|
||||
<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="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)]" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[11px] font-medium text-slate-400 dark:text-slate-500 uppercase tracking-wide mb-0.5">{label}</p>
|
||||
<p className={`text-sm font-medium text-slate-800 dark:text-slate-200 truncate ${mono ? 'font-mono' : ''}`}
|
||||
<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' : ''}`}
|
||||
dir={mono ? 'ltr' : undefined}>
|
||||
{value ?? <span className="text-slate-400 dark:text-slate-500 font-normal">ثبت نشده</span>}
|
||||
{value ?? <span className="text-[var(--text-3)] font-normal">ثبت نشده</span>}
|
||||
</p>
|
||||
</div>
|
||||
{copyable && typeof value === 'string' && (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="opacity-0 group-hover:opacity-100 w-7 h-7 flex items-center justify-center rounded-lg text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 hover:bg-white dark:hover:bg-gray-700 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)] dark:hover:text-[var(--text-3)] hover:bg-[var(--surface)] dark:hover:bg-[var(--surface-2)] transition-all shrink-0"
|
||||
>
|
||||
<ClipboardDocumentIcon className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@@ -181,13 +181,13 @@ function ChangeRoleModal({ current, loading, onSave, onClose }: {
|
||||
className={`flex items-center gap-3 p-3 rounded-xl cursor-pointer border-2 transition-all ${
|
||||
selected === key
|
||||
? 'border-primary-500 bg-primary-50 dark:bg-primary-500/10'
|
||||
: 'border-transparent hover:bg-slate-50 dark:hover:bg-gray-800 border border-slate-200 dark:border-gray-700'
|
||||
: 'border-transparent hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] border border-[var(--border)] dark:border-[var(--border)]'
|
||||
}`}
|
||||
>
|
||||
<input type="radio" name="role" value={key} checked={selected === key}
|
||||
onChange={() => setSelected(key)} className="sr-only" />
|
||||
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: meta.dot }} />
|
||||
<span className="text-sm font-medium text-slate-800 dark:text-slate-200 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" />}
|
||||
</label>
|
||||
))}
|
||||
@@ -320,11 +320,11 @@ export default function UserDetailPage() {
|
||||
return (
|
||||
<div className="animate-slide-up">
|
||||
<div className="cp-card p-16 flex flex-col items-center gap-4 text-center">
|
||||
<div className="w-16 h-16 rounded-2xl bg-red-100 dark:bg-red-400/10 flex items-center justify-center">
|
||||
<XCircleIcon className="w-8 h-8 text-red-500" />
|
||||
<div className="w-16 h-16 rounded-2xl bg-[var(--danger-bg)] dark:bg-red-400/10 flex items-center justify-center">
|
||||
<XCircleIcon className="w-8 h-8 text-[var(--danger)]" />
|
||||
</div>
|
||||
<p className="font-semibold text-slate-800 dark:text-slate-200">کاربر یافت نشد</p>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">این کاربر حذف شده یا UUID اشتباه است.</p>
|
||||
<p className="font-semibold text-[var(--text)]">کاربر یافت نشد</p>
|
||||
<p className="text-sm text-[var(--text-2)]">این کاربر حذف شده یا UUID اشتباه است.</p>
|
||||
<button onClick={() => navigate('/admin/users')} className="cp-btn-secondary mt-2">
|
||||
بازگشت به لیست کاربران
|
||||
</button>
|
||||
@@ -340,16 +340,16 @@ export default function UserDetailPage() {
|
||||
<div className="animate-slide-up space-y-5">
|
||||
|
||||
{/* ── Breadcrumb / Back ─────────────────────────────────── */}
|
||||
<div className="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400">
|
||||
<div className="flex items-center gap-2 text-sm text-[var(--text-2)]">
|
||||
<button
|
||||
onClick={() => navigate('/admin/users')}
|
||||
className="flex items-center gap-1.5 hover:text-slate-800 dark:hover:text-slate-100 transition-colors"
|
||||
className="flex items-center gap-1.5 hover:text-[var(--text)] dark:hover:text-[var(--text)] transition-colors"
|
||||
>
|
||||
<ArrowRightIcon className="w-4 h-4" />
|
||||
کاربران
|
||||
</button>
|
||||
<span>/</span>
|
||||
<span className="text-slate-700 dark:text-slate-300 font-medium">پروفایل کاربر</span>
|
||||
<span className="text-[var(--text)] font-medium">پروفایل کاربر</span>
|
||||
</div>
|
||||
|
||||
{/* ── Profile Card ──────────────────────────────────────── */}
|
||||
@@ -363,8 +363,8 @@ export default function UserDetailPage() {
|
||||
<BigAvatar name={user.name} id={user.id} />
|
||||
|
||||
<div className="flex-1 min-w-0 sm:mb-1">
|
||||
<h1 className="text-xl font-bold text-slate-900 dark:text-slate-50 leading-tight">
|
||||
{user.name || <span className="text-slate-400 font-normal">نام ثبت نشده</span>}
|
||||
<h1 className="text-xl font-bold text-[var(--text)] dark:text-slate-50 leading-tight">
|
||||
{user.name || <span className="text-[var(--text-3)] font-normal">نام ثبت نشده</span>}
|
||||
</h1>
|
||||
<div className="flex items-center gap-2 mt-1.5 flex-wrap">
|
||||
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full ${roleMeta.cls}`}>
|
||||
@@ -372,15 +372,15 @@ export default function UserDetailPage() {
|
||||
{roleMeta.label}
|
||||
</span>
|
||||
{user.is_active ? (
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-green-100 dark:bg-green-400/10 text-green-700 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)] dark:bg-green-400/10 text-[var(--success)] dark:text-green-300">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />فعال
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-slate-100 dark:bg-slate-400/10 text-slate-500 dark:text-slate-400">
|
||||
<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="w-1.5 h-1.5 rounded-full bg-slate-400" />غیرفعال
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-slate-400 dark:text-slate-500">#{user.id}</span>
|
||||
<span className="text-xs text-[var(--text-3)]">#{user.id}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -404,29 +404,29 @@ export default function UserDetailPage() {
|
||||
<EllipsisVerticalIcon className="w-4 h-4" />
|
||||
</button>
|
||||
{menuOpen && (
|
||||
<div className="absolute left-0 top-12 z-30 w-52 cp-card shadow-2xl border border-slate-200 dark:border-gray-700 py-1.5 animate-scale-in" data-upmenu>
|
||||
<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-upmenu>
|
||||
<button
|
||||
onClick={() => { toggleStatusMut.mutate(); setMenuOpen(false); }}
|
||||
disabled={toggleStatusMut.isPending}
|
||||
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-gray-800 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
|
||||
? <XCircleIcon className="w-4 h-4 text-orange-400 shrink-0" />
|
||||
: <CheckCircleIcon className="w-4 h-4 text-green-500 shrink-0" />
|
||||
: <CheckCircleIcon className="w-4 h-4 text-[var(--success)] shrink-0" />
|
||||
}
|
||||
{user.is_active ? 'غیرفعال کردن حساب' : 'فعالسازی حساب'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setRoleOpen(true); setMenuOpen(false); }}
|
||||
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-gray-800 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" />
|
||||
تغییر نقش کاربر
|
||||
</button>
|
||||
<div className="border-t border-slate-100 dark:border-gray-700 my-1" />
|
||||
<div className="border-t border-[var(--border)] my-1" />
|
||||
<button
|
||||
onClick={() => { setDeleteOpen(true); setMenuOpen(false); }}
|
||||
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors"
|
||||
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"
|
||||
>
|
||||
<TrashIcon className="w-4 h-4 shrink-0" />
|
||||
حذف این کاربر
|
||||
@@ -443,7 +443,7 @@ export default function UserDetailPage() {
|
||||
|
||||
{/* Left: Contact info */}
|
||||
<div className="lg:col-span-2 cp-card p-6 space-y-4">
|
||||
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300">اطلاعات تماس و حساب</h2>
|
||||
<h2 className="text-sm font-semibold text-[var(--text)]">اطلاعات تماس و حساب</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<InfoCard icon={UserIcon} label="نام کامل" value={user.name} />
|
||||
<InfoCard icon={PhoneIcon} label="شماره موبایل" value={user.mobile_number} mono copyable />
|
||||
@@ -459,10 +459,10 @@ export default function UserDetailPage() {
|
||||
|
||||
{/* Roles card */}
|
||||
<div className="cp-card p-5 space-y-3">
|
||||
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300">نقشهای سیستمی</h2>
|
||||
<h2 className="text-sm font-semibold text-[var(--text)]">نقشهای سیستمی</h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{user.roles.map((r) => (
|
||||
<span key={r} className="text-xs font-mono px-2.5 py-1 rounded-lg bg-slate-100 dark:bg-gray-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-gray-700">
|
||||
<span key={r} className="text-xs font-mono px-2.5 py-1 rounded-lg bg-[var(--surface-2)] text-[var(--text-2)] border border-[var(--border)]">
|
||||
{r}
|
||||
</span>
|
||||
))}
|
||||
@@ -477,14 +477,14 @@ export default function UserDetailPage() {
|
||||
|
||||
{/* UUID card */}
|
||||
<div className="cp-card p-5 space-y-2">
|
||||
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300">شناسه یکتا (UUID)</h2>
|
||||
<h2 className="text-sm font-semibold text-[var(--text)]">شناسه یکتا (UUID)</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-[11px] font-mono text-slate-500 dark:text-slate-400 break-all flex-1" dir="ltr">
|
||||
<p className="text-[11px] font-mono text-[var(--text-2)] break-all flex-1" dir="ltr">
|
||||
{user.uuid}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => { navigator.clipboard.writeText(user.uuid); toast.success('UUID کپی شد'); }}
|
||||
className="w-7 h-7 flex items-center justify-center rounded-lg text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 hover:bg-slate-100 dark:hover:bg-gray-700 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)] dark:hover:text-[var(--text-3)] hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface-2)] transition-colors shrink-0"
|
||||
>
|
||||
<ClipboardDocumentIcon className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@@ -494,13 +494,13 @@ export default function UserDetailPage() {
|
||||
{/* Status card */}
|
||||
<div className="cp-card p-5">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300">وضعیت حساب</h2>
|
||||
<h2 className="text-sm font-semibold text-[var(--text)]">وضعیت حساب</h2>
|
||||
{user.is_active
|
||||
? <span className="text-xs font-medium text-green-600 dark:text-green-400">فعال</span>
|
||||
: <span className="text-xs font-medium text-slate-500">غیرفعال</span>
|
||||
? <span className="text-xs font-medium text-[var(--success)] dark:text-green-400">فعال</span>
|
||||
: <span className="text-xs font-medium text-[var(--text-2)]">غیرفعال</span>
|
||||
}
|
||||
</div>
|
||||
<div className={`w-full h-2 rounded-full ${user.is_active ? 'bg-green-200 dark:bg-green-400/20' : 'bg-slate-200 dark:bg-gray-700'}`}>
|
||||
<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={`h-full rounded-full transition-all ${user.is_active ? 'w-full bg-green-500' : 'w-0'}`} />
|
||||
</div>
|
||||
<button
|
||||
@@ -509,7 +509,7 @@ export default function UserDetailPage() {
|
||||
className={`w-full mt-4 text-sm font-medium py-2 rounded-xl transition-colors disabled:opacity-50 ${
|
||||
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-green-50 dark:bg-green-400/10 text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-400/20'
|
||||
: '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'
|
||||
}`}
|
||||
>
|
||||
{toggleStatusMut.isPending
|
||||
@@ -523,14 +523,14 @@ export default function UserDetailPage() {
|
||||
|
||||
{/* ── User Profile (read-only) ──────────────────────────── */}
|
||||
<div className="cp-card p-6 space-y-4">
|
||||
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300">پروفایل کاربر</h2>
|
||||
<h2 className="text-sm font-semibold text-[var(--text)]">پروفایل کاربر</h2>
|
||||
|
||||
{profileQ.isLoading ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => <div key={i} className="h-16 rounded-xl skeleton" />)}
|
||||
</div>
|
||||
) : !profile ? (
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400 py-4">
|
||||
<p className="text-sm text-[var(--text-2)] py-4">
|
||||
این کاربر هنوز پروفایلی تکمیل نکرده است.
|
||||
</p>
|
||||
) : (
|
||||
@@ -552,8 +552,8 @@ export default function UserDetailPage() {
|
||||
</div>
|
||||
|
||||
{/* Medical history */}
|
||||
<div className="pt-2 border-t border-slate-100 dark:border-gray-700 space-y-3">
|
||||
<h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300">سوابق پزشکی</h3>
|
||||
<div className="pt-2 border-t border-[var(--border)] space-y-3">
|
||||
<h3 className="text-sm font-semibold text-[var(--text)]">سوابق پزشکی</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{MEDICAL_SECTIONS.map(({ key, label, format, filter }) => {
|
||||
const raw = Array.isArray(profile.other?.[key]) ? (profile.other![key] as any[]) : [];
|
||||
@@ -561,14 +561,14 @@ export default function UserDetailPage() {
|
||||
.map(format)
|
||||
.filter((s) => s && s.trim());
|
||||
return (
|
||||
<div key={key} className="p-4 rounded-xl bg-slate-50 dark:bg-gray-800/60">
|
||||
<p className="text-[11px] font-medium text-slate-400 dark:text-slate-500 uppercase tracking-wide mb-1.5">{label}</p>
|
||||
<div key={key} className="p-4 rounded-xl bg-[var(--surface-2)] dark:bg-[var(--surface)]/60">
|
||||
<p className="text-[11px] font-medium text-[var(--text-3)] uppercase tracking-wide mb-1.5">{label}</p>
|
||||
{items.length === 0 ? (
|
||||
<p className="text-sm text-slate-400 dark:text-slate-500">موردی ثبت نشده</p>
|
||||
<p className="text-sm text-[var(--text-3)]">موردی ثبت نشده</p>
|
||||
) : (
|
||||
<ul className="flex flex-wrap gap-1.5">
|
||||
{items.map((text, i) => (
|
||||
<li key={i} className="text-xs px-2 py-0.5 rounded-lg bg-white dark:bg-gray-700 text-slate-600 dark:text-slate-300 border border-slate-200 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)] dark:border-gray-600">
|
||||
{text}
|
||||
</li>
|
||||
))}
|
||||
@@ -606,19 +606,19 @@ export default function UserDetailPage() {
|
||||
<form id="edit-user-form" onSubmit={handleSubmit((v) => updateMut.mutate(v))} className="space-y-4">
|
||||
|
||||
{/* Avatar preview */}
|
||||
<div className="flex items-center gap-4 pb-4 border-b border-slate-100 dark:border-gray-700">
|
||||
<div className="flex items-center gap-4 pb-4 border-b border-[var(--border)]">
|
||||
<BigAvatar name={user.name} id={user.id} />
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-slate-800 dark:text-slate-200">
|
||||
<p className="text-sm font-semibold text-[var(--text)]">
|
||||
{user.name || 'نام ثبت نشده'}
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 font-mono" dir="ltr">{user.mobile_number}</p>
|
||||
<p className="text-xs text-[var(--text-2)] font-mono" dir="ltr">{user.mobile_number}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">
|
||||
نام و نام خانوادگی
|
||||
</label>
|
||||
<input
|
||||
@@ -627,12 +627,12 @@ export default function UserDetailPage() {
|
||||
className={`cp-input ${errors.name ? 'border-red-400 dark:border-red-500' : ''}`}
|
||||
{...register('name')}
|
||||
/>
|
||||
{errors.name && <p className="text-xs text-red-500 dark:text-red-400 mt-1">{errors.name.message}</p>}
|
||||
{errors.name && <p className="text-xs text-[var(--danger)] dark:text-red-400 mt-1">{errors.name.message}</p>}
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">
|
||||
آدرس ایمیل
|
||||
</label>
|
||||
<input
|
||||
@@ -642,17 +642,17 @@ export default function UserDetailPage() {
|
||||
className={`cp-input text-left ${errors.email ? 'border-red-400 dark:border-red-500' : ''}`}
|
||||
{...register('email')}
|
||||
/>
|
||||
{errors.email && <p className="text-xs text-red-500 dark:text-red-400 mt-1">{errors.email.message}</p>}
|
||||
{errors.email && <p className="text-xs text-[var(--danger)] dark:text-red-400 mt-1">{errors.email.message}</p>}
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">
|
||||
رمز عبور جدید
|
||||
<span className="text-xs font-normal text-slate-400 dark:text-slate-500 mr-1">(خالی = بدون تغییر)</span>
|
||||
<span className="text-xs font-normal text-[var(--text-3)] mr-1">(خالی = بدون تغییر)</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<KeyIcon className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" />
|
||||
<KeyIcon className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-3)] pointer-events-none" />
|
||||
<input
|
||||
type="password"
|
||||
placeholder="حداقل ۶ کاراکتر"
|
||||
@@ -661,21 +661,21 @@ export default function UserDetailPage() {
|
||||
{...register('password')}
|
||||
/>
|
||||
</div>
|
||||
{errors.password && <p className="text-xs text-red-500 dark:text-red-400 mt-1">{errors.password.message}</p>}
|
||||
{errors.password && <p className="text-xs text-[var(--danger)] dark:text-red-400 mt-1">{errors.password.message}</p>}
|
||||
</div>
|
||||
|
||||
{/* Mobile (read-only) */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">
|
||||
شماره موبایل
|
||||
<span className="text-xs font-normal text-slate-400 dark:text-slate-500 mr-1">(قابل تغییر نیست)</span>
|
||||
<span className="text-xs font-normal text-[var(--text-3)] mr-1">(قابل تغییر نیست)</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={user.mobile_number}
|
||||
readOnly
|
||||
dir="ltr"
|
||||
className="cp-input text-left bg-slate-50 dark:bg-gray-800/60 text-slate-400 cursor-not-allowed"
|
||||
className="cp-input text-left bg-[var(--surface-2)] dark:bg-[var(--surface)]/60 text-[var(--text-3)] cursor-not-allowed"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -5,71 +5,71 @@ import React from 'react';
|
||||
|
||||
type IconProps = { size?: number; color?: string };
|
||||
|
||||
export function SparkleIcon({ size = 15, color = '#FDD835' }: IconProps) {
|
||||
export function SparkleIcon({ size = 15, color = 'currentColor' }: IconProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 15 15" fill="none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 15 15" fill="none" style={{ color }}>
|
||||
<path
|
||||
d="M13.4059 6.94375L11.5309 6.00625L9.85586 5.16875L9.83711 5.15L8.99961 3.475L8.06211 1.6C7.84961 1.175 7.15586 1.175 6.94336 1.6L6.00586 3.475L5.16836 5.15L5.14961 5.16875L3.47461 6.00625L1.59961 6.94375C1.38711 7.05 1.25586 7.26875 1.25586 7.5C1.25586 7.73125 1.38711 7.95 1.59961 8.05625L3.47461 8.99375L5.14961 9.83125L5.16836 9.85L6.00586 11.525L6.94336 13.4C7.04961 13.6125 7.26836 13.7437 7.49961 13.7437C7.73086 13.7437 7.94961 13.6125 8.05586 13.4L8.99336 11.525L9.83086 9.85L9.84961 9.83125L11.5246 8.99375L13.3996 8.05625C13.6121 7.95 13.7434 7.73125 13.7434 7.5C13.7434 7.26875 13.6121 7.05 13.3996 6.94375H13.4059ZM12.1871 0.9375L11.5996 2.225L10.3121 2.8125L11.5996 3.4L12.1871 4.6875L12.7746 3.4L14.0621 2.8125L12.7746 2.225L12.1871 0.9375Z"
|
||||
fill={color}
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function PlanCardPerson({ size = 20, color = '#2F2F2F' }: IconProps) {
|
||||
export function PlanCardPerson({ size = 20, color = 'currentColor' }: IconProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 18 18" fill="none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 18 18" fill="none" style={{ color }}>
|
||||
<path
|
||||
d="M9 9C11.0711 9 12.75 7.32107 12.75 5.25C12.75 3.17893 11.0711 1.5 9 1.5C6.92893 1.5 5.25 3.17893 5.25 5.25C5.25 7.32107 6.92893 9 9 9Z"
|
||||
stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"
|
||||
stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M15.4416 16.5C15.4416 13.5975 12.5541 11.25 8.99914 11.25C5.44414 11.25 2.55664 13.5975 2.55664 16.5"
|
||||
stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"
|
||||
stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function CloseCircleFilled({ size = 20, color = '#D7D7D7' }: IconProps) {
|
||||
export function CloseCircleFilled({ size = 20, color = 'currentColor' }: IconProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 20 20" fill="none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 20 20" fill="none" style={{ color }}>
|
||||
<path
|
||||
d="M9.99902 2.16699C14.3144 2.16699 17.8328 5.68462 17.833 10C17.833 14.3155 14.3145 17.834 9.99902 17.834C5.68365 17.8338 2.16602 14.3154 2.16602 10C2.16619 5.68473 5.68376 2.16717 9.99902 2.16699ZM13.1533 6.84668C12.7164 6.40977 11.9994 6.40982 11.5625 6.84668L9.99902 8.40918L8.43652 6.84668C7.99959 6.40975 7.28263 6.40975 6.8457 6.84668C6.40877 7.28361 6.40877 8.00057 6.8457 8.4375L8.4082 10L6.8457 11.5635C6.40884 12.0004 6.4088 12.7174 6.8457 13.1543C7.06929 13.3778 7.35703 13.4833 7.64062 13.4834C7.92438 13.4834 8.21282 13.378 8.43652 13.1543L9.99902 11.5908L11.5625 13.1543C11.7861 13.3778 12.0738 13.4833 12.3574 13.4834C12.6412 13.4834 12.9296 13.378 13.1533 13.1543C13.5902 12.7174 13.59 12.0004 13.1533 11.5635L11.5898 10L13.1533 8.4375C13.5901 8.00062 13.5901 7.2836 13.1533 6.84668Z"
|
||||
fill={color} stroke={color}
|
||||
fill="currentColor" stroke="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function GiftIcon({ size = 24, color = '#F17732' }: IconProps) {
|
||||
export function GiftIcon({ size = 24, color = 'currentColor' }: IconProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none">
|
||||
<path d="M20 12V18C20 20.21 18.21 22 16 22H8C5.79 22 4 20.21 4 18V12C4 11.45 4.45 11 5 11H6.97C7.52 11 7.97 11.45 7.97 12V15.14C7.97 15.88 8.38 16.56 9.03 16.91C9.32 17.07 9.64 17.15 9.97 17.15C10.35 17.15 10.73 17.04 11.06 16.82L12.01 16.2L12.89 16.79C13.5 17.2 14.28 17.25 14.93 16.9C15.59 16.55 16 15.88 16 15.13V12C16 11.45 16.45 11 17 11H19C19.55 11 20 11.45 20 12Z" fill={color} />
|
||||
<path d="M21.5 7V8C21.5 9.1 20.97 10 19.5 10H4.5C2.97 10 2.5 9.1 2.5 8V7C2.5 5.9 2.97 5 4.5 5H19.5C20.97 5 21.5 5.9 21.5 7Z" fill={color} />
|
||||
<path d="M11.6388 5.00043H6.11881C5.77881 4.63043 5.78881 4.06043 6.14881 3.70043L7.56881 2.28043C7.93881 1.91043 8.54881 1.91043 8.91881 2.28043L11.6388 5.00043Z" fill={color} />
|
||||
<path d="M17.8716 5.00043H12.3516L15.0716 2.28043C15.4416 1.91043 16.0516 1.91043 16.4216 2.28043L17.8416 3.70043C18.2016 4.06043 18.2116 4.63043 17.8716 5.00043Z" fill={color} />
|
||||
<path d="M13.9714 11C14.5214 11 14.9714 11.45 14.9714 12V15.13C14.9714 15.93 14.0814 16.41 13.4214 15.96L12.5214 15.36C12.1914 15.14 11.7614 15.14 11.4214 15.36L10.4814 15.98C9.82141 16.42 8.94141 15.94 8.94141 15.15V12C8.94141 11.45 9.39141 11 9.94141 11H13.9714Z" fill={color} />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" style={{ color }}>
|
||||
<path d="M20 12V18C20 20.21 18.21 22 16 22H8C5.79 22 4 20.21 4 18V12C4 11.45 4.45 11 5 11H6.97C7.52 11 7.97 11.45 7.97 12V15.14C7.97 15.88 8.38 16.56 9.03 16.91C9.32 17.07 9.64 17.15 9.97 17.15C10.35 17.15 10.73 17.04 11.06 16.82L12.01 16.2L12.89 16.79C13.5 17.2 14.28 17.25 14.93 16.9C15.59 16.55 16 15.88 16 15.13V12C16 11.45 16.45 11 17 11H19C19.55 11 20 11.45 20 12Z" fill="currentColor" />
|
||||
<path d="M21.5 7V8C21.5 9.1 20.97 10 19.5 10H4.5C2.97 10 2.5 9.1 2.5 8V7C2.5 5.9 2.97 5 4.5 5H19.5C20.97 5 21.5 5.9 21.5 7Z" fill="currentColor" />
|
||||
<path d="M11.6388 5.00043H6.11881C5.77881 4.63043 5.78881 4.06043 6.14881 3.70043L7.56881 2.28043C7.93881 1.91043 8.54881 1.91043 8.91881 2.28043L11.6388 5.00043Z" fill="currentColor" />
|
||||
<path d="M17.8716 5.00043H12.3516L15.0716 2.28043C15.4416 1.91043 16.0516 1.91043 16.4216 2.28043L17.8416 3.70043C18.2016 4.06043 18.2116 4.63043 17.8716 5.00043Z" fill="currentColor" />
|
||||
<path d="M13.9714 11C14.5214 11 14.9714 11.45 14.9714 12V15.13C14.9714 15.93 14.0814 16.41 13.4214 15.96L12.5214 15.36C12.1914 15.14 11.7614 15.14 11.4214 15.36L10.4814 15.98C9.82141 16.42 8.94141 15.94 8.94141 15.15V12C8.94141 11.45 9.39141 11 9.94141 11H13.9714Z" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function InfoCircleRedFilled({ size = 20, color = '#D32F2F' }: IconProps) {
|
||||
export function InfoCircleRedFilled({ size = 20, color = 'currentColor' }: IconProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 20 20" fill="none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 20 20" fill="none" style={{ color }}>
|
||||
<path
|
||||
d="M9.99902 2.16699C14.3144 2.16699 17.8328 5.68462 17.833 10C17.833 14.3155 14.3145 17.834 9.99902 17.834C5.68365 17.8338 2.16602 14.3154 2.16602 10C2.16619 5.68473 5.68376 2.16717 9.99902 2.16699ZM10.5088 12.1055C10.1857 11.9709 9.81331 11.9709 9.49023 12.1055C9.3364 12.1696 9.197 12.2588 9.07324 12.3701L9.05371 12.3877L9.03613 12.4072C8.92481 12.5309 8.83561 12.6705 8.77148 12.8242C8.70851 12.9754 8.66602 13.1498 8.66602 13.334C8.66606 13.5127 8.70667 13.6812 8.7666 13.8291L8.76562 13.8301C8.83536 14.0113 8.93351 14.1457 9.03613 14.2598L9.05371 14.2793L9.07324 14.2969C9.197 14.4083 9.33639 14.4974 9.49023 14.5615C9.64129 14.6245 9.81496 14.6669 9.99902 14.667C10.1832 14.667 10.3576 14.6245 10.5088 14.5615C10.6625 14.4974 10.8021 14.4082 10.9258 14.2969L10.9453 14.2793L10.9629 14.2598C11.0631 14.1484 11.1572 14.0168 11.2266 13.8418L11.2275 13.8428C11.2905 13.6917 11.333 13.518 11.333 13.334C11.333 13.1498 11.2905 12.9754 11.2275 12.8242C11.1634 12.6704 11.0742 12.5309 10.9629 12.4072L10.9453 12.3877L10.9258 12.3701L10.8301 12.291C10.7315 12.2159 10.6242 12.1536 10.5088 12.1055ZM9.99902 5.54199C9.38136 5.54217 8.87402 6.04929 8.87402 6.66699V10.834C8.8742 11.4515 9.38147 11.9588 9.99902 11.959C10.6167 11.959 11.1238 11.4516 11.124 10.834V6.66699C11.124 6.04918 10.6168 5.54199 9.99902 5.54199Z"
|
||||
fill={color} stroke={color}
|
||||
fill="currentColor" stroke="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function SearchHeaderP({ color = '#7E7E7E' }: { color?: string }) {
|
||||
export function SearchHeaderP({ color = 'currentColor' }: { color?: string }) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M22 22L20 20" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" style={{ color }}>
|
||||
<path d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M22 22L20 20" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user