- 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.
140 lines
7.2 KiB
TypeScript
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>
|
|
);
|
|
}
|