Files
clinicpro/assets/admin/components/inventory/AddPackageModal.tsx
T
hamed 55ab2f5dfc 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.
2026-07-27 16:41:52 +03:30

140 lines
7.2 KiB
TypeScript

import React, { useEffect, useMemo, useState } from 'react';
import { TrashIcon } from '@heroicons/react/24/outline';
import Modal from '../ui/Modal';
import SearchableSelect from '../ui/SearchableSelect';
import { formatRial, digitsOnly } from '../../lib/utils';
import type { InventoryItem, InventoryPackage, PackagePayload } from '../../hooks/useInventory';
interface Props {
open: boolean;
editing: InventoryPackage | null;
items: InventoryItem[]; // the tenant's available items to pick from
saving: boolean;
onClose: () => void;
onSave: (payload: PackagePayload, uuid?: string) => void;
}
interface Line { itemUuid: string; name: string; unit: string; price: number; amount: number }
/** «افزودن/ویرایش پکیج» — mirrors tauri ModalAddPackage (two-column builder). */
export default function AddPackageModal({ open, editing, items, saving, onClose, onSave }: Props) {
const [title, setTitle] = useState('');
const [pickUuid, setPickUuid] = useState('');
const [amount, setAmount] = useState('1');
const [lines, setLines] = useState<Line[]>([]);
const [error, setError] = useState('');
useEffect(() => {
if (!open) return;
setError('');
setPickUuid(items[0]?.uuid ?? '');
setAmount('1');
if (editing) {
setTitle(editing.title);
setLines(editing.items.map((l) => ({ itemUuid: l.itemUuid, name: l.name, unit: l.unit, price: l.price, amount: l.amount })));
} else {
setTitle('');
setLines([]);
}
}, [open, editing, items]);
const total = useMemo(() => lines.reduce((s, l) => s + l.price * l.amount, 0), [lines]);
const addLine = () => {
const item = items.find((i) => i.uuid === pickUuid);
if (!item) return;
const qty = Math.max(1, Number(amount) || 1);
setLines((prev) => [...prev, { itemUuid: item.uuid, name: item.name, unit: item.unit, price: item.price, amount: qty }]);
setAmount('1');
};
const changeAmount = (idx: number, delta: number) =>
setLines((prev) => prev.map((l, i) => (i === idx ? { ...l, amount: Math.max(1, l.amount + delta) } : l)));
const removeLine = (idx: number) => setLines((prev) => prev.filter((_, i) => i !== idx));
const submit = () => {
if (title.trim() === '') { setError('نام پکیج الزامی است'); return; }
onSave({ title: title.trim(), items: lines.map((l) => ({ itemUuid: l.itemUuid, amount: l.amount })) }, editing?.uuid);
};
const footer = (
<div style={{ display: 'flex', gap: 16, justifyContent: 'flex-end', width: '100%' }}>
<button className="btn ghost" style={{ height: 48, width: 160, justifyContent: 'center' }} onClick={onClose}>انصراف</button>
<button className="btn primary" style={{ height: 48, width: 160, justifyContent: 'center' }} onClick={submit} disabled={saving}>
{editing ? 'ذخیره تغییرات' : 'افزودن پکیج'}
</button>
</div>
);
return (
<Modal open={open} onClose={onClose} title={editing ? 'ویرایش پکیج' : 'افزودن پکیج'} size="xl" footer={footer}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 32 }}>
{/* Left — builder */}
<div style={{ flex: '1 1 300px', display: 'flex', flexDirection: 'column', gap: 20 }}>
<div>
<label className="field-label">نام پکیج</label>
<div className="field"><input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="نام پکیج" /></div>
</div>
<div>
<label className="field-label">اجزای پکیج</label>
<SearchableSelect
options={items.map((i) => ({ value: i.uuid, label: i.name }))}
value={pickUuid || null}
onChange={(v) => setPickUuid(v ? String(v) : '')}
placeholder={items.length === 0 ? 'ابتدا کالا اضافه کنید' : 'انتخاب کالا'}
isDisabled={items.length === 0}
height={38}
/>
</div>
<div>
<label className="field-label">مقدار</label>
<div className="field">
<input value={amount} inputMode="numeric" onChange={(e) => setAmount(digitsOnly(e.target.value))} placeholder="1" />
</div>
</div>
<button className="btn ghost" style={{ justifyContent: 'center', height: 46 }} onClick={addLine} disabled={items.length === 0}>
افزودن کالا
</button>
</div>
{/* Right — selected items */}
<div
style={{
flex: '1 1 300px', minHeight: 320, borderRadius: 8, padding: 24,
border: '1px dashed var(--primary)', display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
}}
>
<div>
<div style={{ fontSize: 16, fontWeight: 600, color: 'var(--text)', marginBottom: 16 }}>کالاهای انتخاب شده:</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{lines.length === 0 ? (
<span style={{ fontSize: 14, color: 'var(--text-3)' }}>هنوز کالایی به پکیج اضافه نشده است.</span>
) : (
lines.map((l, idx) => (
<div key={idx} style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<span style={{ flex: 1, minWidth: 0, fontSize: 14, fontWeight: 600, color: 'var(--text-2)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{l.name}</span>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: 97, height: 40, borderRadius: 4, border: '1px solid var(--border-2)', padding: '0 8px' }}>
<button type="button" onClick={() => changeAmount(idx, 1)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--accent)', fontSize: 18, fontWeight: 600 }}>+</button>
<span style={{ color: 'var(--accent)', fontSize: 14, fontWeight: 500 }}>{l.amount}</span>
<button type="button" onClick={() => changeAmount(idx, -1)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--accent)', fontSize: 18, fontWeight: 600 }}>-</button>
</div>
<span style={{ fontSize: 14, color: 'var(--text-2)', whiteSpace: 'nowrap' }}>{formatRial(l.price)}</span>
<button type="button" onClick={() => removeLine(idx)} aria-label="حذف" style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--danger)' }}>
<TrashIcon style={{ width: 18 }} />
</button>
</div>
))
)}
</div>
</div>
<div style={{ marginTop: 24, paddingTop: 16, borderTop: '1px solid var(--border)' }}>
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>قیمت کل پکیج: {formatRial(total)}</span>
</div>
</div>
</div>
{error && <span className="field-error">{error}</span>}
</div>
</Modal>
);
}