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:
hamed
2026-07-27 16:41:52 +03:30
parent c29035c21e
commit 55ab2f5dfc
73 changed files with 1485 additions and 1110 deletions
+51 -51
View File
@@ -30,9 +30,9 @@ const todayISO = () => new Date().toISOString().slice(0, 10);
/** YYYY-MM-DD → unix (ظهر همان روز تا با هر timezone یک روز بماند) */
const isoToUnix = (iso: string) => Math.floor(new Date(`${iso}T12:00:00`).getTime() / 1000);
const fieldLabel: React.CSSProperties = { fontSize: 14, color: '#6B7280', marginBottom: 8, display: 'block' };
const primaryBtn: React.CSSProperties = { background: '#5559CE', color: '#fff', border: 'none', borderRadius: 4, height: 46, fontSize: 14, fontWeight: 500, cursor: 'pointer' };
const ghostBtn: React.CSSProperties = { background: 'transparent', color: '#5559CE', border: '1px solid #5559CE', borderRadius: 4, height: 46, fontSize: 14, fontWeight: 500, cursor: 'pointer' };
const fieldLabel: React.CSSProperties = { fontSize: 14, color: 'var(--text-2)', marginBottom: 8, display: 'block' };
const primaryBtn: React.CSSProperties = { background: 'var(--primary)', color: 'var(--on-primary)', border: 'none', borderRadius: 4, height: 46, fontSize: 14, fontWeight: 500, cursor: 'pointer' };
const ghostBtn: React.CSSProperties = { background: 'transparent', color: 'var(--primary)', border: '1px solid var(--primary)', borderRadius: 4, height: 46, fontSize: 14, fontWeight: 500, cursor: 'pointer' };
interface Props {
recordUuid: string;
@@ -146,26 +146,26 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
{/* هزینه سرویس */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12, margin: '12px 0 16px' }}>
<Step2PaymentCard />
<span style={{ fontSize: 14, color: '#F97316', fontWeight: 700 }}>هزینه سرویس:</span>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 700, color: '#525252' }}>{formatRial(finalPrice)}</span>
<span style={{ fontSize: 14, color: 'var(--accent)', fontWeight: 700 }}>هزینه سرویس:</span>
<span style={{ fontSize: 14, fontWeight: 700, color: 'var(--text)' }}>{formatRial(finalPrice)}</span>
</div>
{/* تخفیف‌های پیشنهادی (قوانین تخفیف) */}
{suggestions.length > 0 && (
<div className="dark:border-[#35343D]" style={{ border: '1px solid #E8EBFF', background: '#F7F8FF', borderRadius: 8, padding: 12, marginBottom: 16 }}>
<span style={{ ...fieldLabel, marginBottom: 10, fontWeight: 600, color: '#3B3F9F' }}>تخفیفهای قابل اعمال:</span>
<div style={{ border: '1px solid var(--primary-soft2)', background: 'var(--primary-soft)', borderRadius: 8, padding: 12, marginBottom: 16 }}>
<span style={{ ...fieldLabel, marginBottom: 10, fontWeight: 600, color: 'var(--primary-700)' }}>تخفیفهای قابل اعمال:</span>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{suggestions.map((s) => (
<div key={s.rule_uuid} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 13, color: '#374151' }}>
{s.rule_name} <span style={{ color: '#D32F2F' }}>{formatRial(s.discount_rials)}</span>
{' '}<span style={{ color: '#6B7280', fontSize: 12 }}>({s.discount_type === 'percent' ? `${s.value}٪` : 'مبلغ ثابت'})</span>
<span style={{ fontSize: 13, color: 'var(--text)' }}>
{s.rule_name} <span style={{ color: 'var(--danger)' }}>{formatRial(s.discount_rials)}</span>
{' '}<span style={{ color: 'var(--text-2)', fontSize: 12 }}>({s.discount_type === 'percent' ? `${s.value}٪` : 'مبلغ ثابت'})</span>
</span>
<button
type="button"
onClick={() => applyRule(s.rule_uuid)}
disabled={discountMut.isPending}
style={{ height: 32, minWidth: 64, border: 'none', borderRadius: 6, background: '#5559CE', color: '#fff', fontSize: 12.5, fontWeight: 600, cursor: 'pointer' }}
style={{ height: 32, minWidth: 64, border: 'none', borderRadius: 6, background: 'var(--primary)', color: 'var(--on-primary)', fontSize: 12.5, fontWeight: 600, cursor: 'pointer' }}
>
اعمال
</button>
@@ -178,8 +178,8 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
{/* تخفیف — port of tauri DiscountInput (نوع + مقدار + ثبت) */}
<span style={fieldLabel}>تخفیف:</span>
<div style={{ display: 'flex', alignItems: 'stretch', gap: 8 }}>
<div className="bg-white dark:bg-[#222433] dark:border-[#35343D]" style={{ flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', border: '1px solid #e0e0e0', borderRadius: 8, marginBottom: 16 }}>
<div style={{ minWidth: 150, borderLeft: '1px solid #e0e0e0', padding: '0 4px' }}>
<div className="bg-[var(--surface)]" style={{ flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', border: '1px solid var(--border-2)', borderRadius: 8, marginBottom: 16 }}>
<div style={{ minWidth: 150, borderLeft: '1px solid var(--border-2)', padding: '0 4px' }}>
<SearchableSelect
options={[{ value: 'percent', label: 'درصدی' }, { value: 'fixed', label: 'مبلغ ثابت' }]}
value={discountType}
@@ -198,7 +198,7 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
type="button"
onClick={applyDiscount}
disabled={discountMut.isPending || !discountType || discountValue <= 0}
style={{ height: 40, minWidth: 72, border: 'none', borderRadius: '0 4px 4px 0', background: '#E8EBFF', color: '#3B3F9F', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}
style={{ height: 40, minWidth: 72, border: 'none', borderRadius: '0 4px 4px 0', background: 'var(--primary-soft2)', color: 'var(--primary-700)', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}
>
ثبت
</button>
@@ -207,64 +207,64 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
onClick={removeDiscount}
style={{ display: 'flex', alignItems: 'center', gap: 4, flexShrink: 0, cursor: 'pointer', marginBottom: 16, minWidth: 120, justifyContent: 'center' }}
>
<TrashRed color="#EF4444" size={18} />
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 13, color: '#6B7280' }}>حذف تخفیف</span>
<TrashRed color="var(--danger)" size={18} />
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>حذف تخفیف</span>
</div>
</div>
{/* خلاصه‌ی مبلغ: قبل از تخفیف / تخفیف / نهایی / منبع */}
<div className="dark:border-[#35343D]" style={{ border: '1px solid #e0e0e0', borderRadius: 8, padding: 12, marginBottom: 16, display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ border: '1px solid var(--border-2)', borderRadius: 8, padding: 12, marginBottom: 16, display: 'flex', flexDirection: 'column', gap: 6 }}>
{hasInsurance && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
<span className="dark:text-[#A1A1A1]" style={{ color: '#6B7280' }}>هزینه کل خدمات:</span>
<span className="dark:text-[#D7D8ED]" style={{ color: '#2f2f2f' }}>{formatRial(grossTotal)}</span>
<span style={{ color: 'var(--text-2)' }}>هزینه کل خدمات:</span>
<span style={{ color: 'var(--text)' }}>{formatRial(grossTotal)}</span>
</div>
{baseInsurance > 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
<span className="dark:text-[#A1A1A1]" style={{ color: '#6B7280' }}>سهم بیمه پایه:</span>
<span style={{ color: '#2E7D32' }}>{formatRial(baseInsurance)}</span>
<span style={{ color: 'var(--text-2)' }}>سهم بیمه پایه:</span>
<span style={{ color: 'var(--success)' }}>{formatRial(baseInsurance)}</span>
</div>
)}
{suppInsurance > 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
<span className="dark:text-[#A1A1A1]" style={{ color: '#6B7280' }}>سهم بیمه تکمیلی:</span>
<span style={{ color: '#2E7D32' }}>{formatRial(suppInsurance)}</span>
<span style={{ color: 'var(--text-2)' }}>سهم بیمه تکمیلی:</span>
<span style={{ color: 'var(--success)' }}>{formatRial(suppInsurance)}</span>
</div>
)}
</>
)}
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
<span className="dark:text-[#A1A1A1]" style={{ color: '#6B7280' }}>{hasInsurance ? 'سهم بیمار:' : 'مبلغ قبل از تخفیف:'}</span>
<span className="dark:text-[#D7D8ED]" style={{ color: '#2f2f2f' }}>{formatRial(finalPrice)}</span>
<span style={{ color: 'var(--text-2)' }}>{hasInsurance ? 'سهم بیمار:' : 'مبلغ قبل از تخفیف:'}</span>
<span style={{ color: 'var(--text)' }}>{formatRial(finalPrice)}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
<span className="dark:text-[#A1A1A1]" style={{ color: '#6B7280' }}>مبلغ تخفیف{appliedRuleLabel ? ` (${appliedRuleLabel})` : ''}:</span>
<span style={{ color: '#D32F2F' }}>{formatRial(discountRials)}</span>
<span style={{ color: 'var(--text-2)' }}>مبلغ تخفیف{appliedRuleLabel ? ` (${appliedRuleLabel})` : ''}:</span>
<span style={{ color: 'var(--danger)' }}>{formatRial(discountRials)}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, fontWeight: 700, borderTop: '1px dashed #e0e0e0', paddingTop: 6 }}>
<span className="dark:text-[#D7D8ED]" style={{ color: '#2f2f2f' }}>مبلغ نهایی قابل پرداخت:</span>
<span className="dark:text-[#D7D8ED]" style={{ color: '#2f2f2f' }}>{formatRial(payable)}</span>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, fontWeight: 700, borderTop: '1px dashed var(--border-2)', paddingTop: 6 }}>
<span style={{ color: 'var(--text)' }}>مبلغ نهایی قابل پرداخت:</span>
<span style={{ color: 'var(--text)' }}>{formatRial(payable)}</span>
</div>
</div>
{/* تاریخ پرداخت */}
<span className="dark:text-[#A1A1A1]" style={{ ...fieldLabel, color: '#3b3b3b' }}>تاریخ پرداخت</span>
<span style={{ ...fieldLabel, color: 'var(--text)' }}>تاریخ پرداخت</span>
<PersianDateInput value={paymentDate} onChange={setPaymentDate} />
{/* روش‌های پرداخت */}
<span className="dark:text-[#A1A1A1]" style={{ ...fieldLabel, color: '#3b3b3b', marginTop: 24 }}>انتخاب روش های پرداخت:</span>
<span style={{ ...fieldLabel, color: 'var(--text)', marginTop: 24 }}>انتخاب روش های پرداخت:</span>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ display: 'flex', gap: 4 }}>
<FilesServiceBalanceWallet />
<span style={{ fontSize: 14, color: '#F97316', marginBottom: 8, fontWeight: 600 }}>موجودی کیف پول:</span>
<span style={{ fontSize: 14, color: 'var(--accent)', marginBottom: 8, fontWeight: 600 }}>موجودی کیف پول:</span>
</div>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 600, color: '#525252', marginBottom: 4 }}>{formatRial(walletBalance)}</span>
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)', marginBottom: 4 }}>{formatRial(walletBalance)}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
<span style={{ fontSize: 14, fontWeight: 600, color: '#111827' }}>مبلغ باقیمانده:</span>
<span className="dark:text-[#FF5252]" style={{ fontSize: 14, fontWeight: 700, color: '#d32f2f' }}>{formatRial(debt)}</span>
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>مبلغ باقیمانده:</span>
<span style={{ fontSize: 14, fontWeight: 700, color: 'var(--danger)' }}>{formatRial(debt)}</span>
</div>
</div>
@@ -273,15 +273,15 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
{METHODS.map((m) => {
const open = expanded === m.key;
return (
<div key={m.key} className="dark:border-[#35343D]" style={{ borderBottom: '1px solid #e0e0e0' }}>
<div key={m.key} style={{ borderBottom: '1px solid var(--border-2)' }}>
<button
type="button"
aria-expanded={open}
onClick={() => { setExpanded(open ? null : m.key); setAmount(open ? 0 : defaultAmountFor(m.key)); }}
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%', padding: '14px 4px', background: 'transparent', border: 'none', cursor: 'pointer' }}
>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 600, color: '#111827' }}>{m.label}</span>
<ChevronDownIcon style={{ width: 16, color: '#6B7280', transform: open ? 'rotate(180deg)' : undefined, transition: 'transform .15s' }} />
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>{m.label}</span>
<ChevronDownIcon style={{ width: 16, color: 'var(--text-2)', transform: open ? 'rotate(180deg)' : undefined, transition: 'transform .15s' }} />
</button>
{open && (
<div style={{ display: 'flex', gap: 8, padding: '0 4px 14px' }}>
@@ -309,34 +309,34 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
</div>
{/* پرداخت شده‌ها — باکس خط‌چین tauri */}
<div className="dark:border-[#404040] dark:bg-[#222433]" style={{ border: '1px dashed #C7C9F4', borderRadius: 8, padding: 16, marginTop: 16, background: '#fff' }}>
<span className="dark:text-[#6A6AD9]" style={{ fontSize: 16, fontWeight: 500, color: '#636bd4', display: 'block', marginBottom: 16 }}>پرداخت شده ها:</span>
<div style={{ border: '1px dashed var(--primary-soft2)', borderRadius: 8, padding: 16, marginTop: 16, background: 'var(--surface)' }}>
<span style={{ fontSize: 16, fontWeight: 500, color: 'var(--primary)', display: 'block', marginBottom: 16 }}>پرداخت شده ها:</span>
{payments.length === 0 ? (
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 13, color: '#6B7280' }}>پرداختی ثبت نشده است</span>
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>پرداختی ثبت نشده است</span>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{payments.map((p) => (
<div key={p.uuid} className="dark:border-[#404040]" style={{ padding: '6px 0', borderBottom: '1px solid #e0e0e0' }}>
<div key={p.uuid} style={{ padding: '6px 0', borderBottom: '1px solid var(--border-2)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span className="dark:bg-[#6A6AD9]" style={{ width: 8, height: 8, borderRadius: '50%', background: '#636bd4' }} />
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, color: '#111827' }}>{METHOD_LABELS[p.method] ?? p.method}</span>
<span style={{ width: 8, height: 8, borderRadius: '50%', background: 'var(--primary)' }} />
<span style={{ fontSize: 14, color: 'var(--text)' }}>{METHOD_LABELS[p.method] ?? p.method}</span>
</span>
<span className="dark:text-[#D7D8ED]" style={{ flex: 1, textAlign: 'left', fontSize: 14, color: '#111827' }}>مبلغ : {formatRial(p.amount_rials)}</span>
<span style={{ flex: 1, textAlign: 'left', fontSize: 14, color: 'var(--text)' }}>مبلغ : {formatRial(p.amount_rials)}</span>
{p.method !== 'wallet' && (
<span style={{ display: 'flex', gap: 6, flexShrink: 0 }}>
<button type="button" aria-label="ویرایش پرداخت" onClick={() => setEditPay({ uuid: p.uuid, method: p.method, amountToman: rialToToman(p.amount_rials) })}
style={{ border: 'none', background: 'transparent', cursor: 'pointer', color: '#5559ce', display: 'flex' }}>
style={{ border: 'none', background: 'transparent', cursor: 'pointer', color: 'var(--primary)', display: 'flex' }}>
<PencilIcon style={{ width: 16 }} />
</button>
<button type="button" aria-label="حذف پرداخت" onClick={() => setDelPayUuid(p.uuid)}
style={{ border: 'none', background: 'transparent', cursor: 'pointer', color: '#EF4444', display: 'flex' }}>
style={{ border: 'none', background: 'transparent', cursor: 'pointer', color: 'var(--danger)', display: 'flex' }}>
<TrashIcon style={{ width: 16 }} />
</button>
</span>
)}
</div>
<div className="dark:text-[#A1A1A1]" style={{ display: 'flex', gap: 12, marginTop: 4, fontSize: 12, color: '#9CA3AF', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', gap: 12, marginTop: 4, fontSize: 12, color: 'var(--text-3)', flexWrap: 'wrap' }}>
{(p.paid_at ?? p.created_at) && <span>{formatDateTime(p.paid_at ?? p.created_at!)}</span>}
{p.created_by_name && <span>ثبت: {p.created_by_name}</span>}
</div>
@@ -345,8 +345,8 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
</div>
)}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 8, marginTop: 16 }}>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 500, color: '#111827' }}>مبلغ باقیمانده :</span>
<span className="dark:text-[#FF5252]" style={{ fontSize: 14, fontWeight: 600, color: '#d32f2f' }}>{formatRial(debt)}</span>
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>مبلغ باقیمانده :</span>
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--danger)' }}>{formatRial(debt)}</span>
</div>
</div>