Update App.jsx via upload script - 2026-08-21 17:23:49

This commit is contained in:
mike 2026-08-21 17:24:08 +08:00
parent d493486b76
commit fb9bf6dbe2
1 changed files with 252 additions and 5 deletions

257
App.jsx
View File

@ -5,7 +5,7 @@ import {
Building2, Globe, Phone, Save, RefreshCw, LayoutDashboard, Users, Building2, Globe, Phone, Save, RefreshCw, LayoutDashboard, Users,
Package, Layers, Zap, Tag, Inbox, Paperclip, Package, Layers, Zap, Tag, Inbox, Paperclip,
Menu, LogOut, GripVertical, BookOpen, FileText, FolderTree, Lock, Sparkles, Printer, Image as ImageIcon, Menu, LogOut, GripVertical, BookOpen, FileText, FolderTree, Lock, Sparkles, Printer, Image as ImageIcon,
ShieldCheck, Settings2, Boxes, Download, Wifi, Radio, QrCode, Folder, FolderPlus ShieldCheck, Settings2, Boxes, Download, Wifi, Radio, QrCode, Folder, FolderPlus, Factory
} from 'lucide-react'; } from 'lucide-react';
import { import {
storageGet, storageSet, uploadFile, deleteFile, storageGet, storageSet, uploadFile, deleteFile,
@ -245,7 +245,7 @@ const COUNTRIES = [
const COUNTRY_BY_NAME = Object.fromEntries(COUNTRIES.map(c => [c.name, c])); const COUNTRY_BY_NAME = Object.fromEntries(COUNTRIES.map(c => [c.name, c]));
const STORAGE_KEYS = { customers: 'crm:customers', products: 'crm:products', quotes: 'crm:quotes', sellerProfiles: 'crm:sellerProfiles', certifications: 'crm:certifications', productCategories: 'crm:productCategories', packingLists: 'crm:packingLists', catalogDocs: 'crm:catalogDocs', jmsLines: 'crm:jmsLines', backPanelTemplates: 'crm:backPanelTemplates' }; const STORAGE_KEYS = { customers: 'crm:customers', products: 'crm:products', quotes: 'crm:quotes', sellerProfiles: 'crm:sellerProfiles', certifications: 'crm:certifications', productCategories: 'crm:productCategories', packingLists: 'crm:packingLists', catalogDocs: 'crm:catalogDocs', jmsLines: 'crm:jmsLines', backPanelTemplates: 'crm:backPanelTemplates', suppliers: 'crm:suppliers' };
const blankCustomer = () => ({ const blankCustomer = () => ({
id: 'c_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7), id: 'c_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7),
@ -262,6 +262,13 @@ const blankCustomer = () => ({
fileFolders: [], // {id, name, parentId}parentIdnull fileFolders: [], // {id, name, parentId}parentIdnull
}); });
const blankSupplier = () => ({
id: 'sup_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7),
name: '', contact: '', phone: '', email: '', wechatOrWhatsapp: '', address: '', website: '',
mainProducts: '', notes: '',
attachments: [], // PDF/{fileName, originalName, note, date}
});
const OUTPUT_PORT_TYPES = ['USB', 'DC', 'DC1', 'DC2', 'DC3', 'POE', 'Type-C', 'DC interface', 'POE interface']; const OUTPUT_PORT_TYPES = ['USB', 'DC', 'DC1', 'DC2', 'DC3', 'POE', 'Type-C', 'DC interface', 'POE interface'];
const EXPORT_LANGUAGES = [ const EXPORT_LANGUAGES = [
@ -1424,7 +1431,12 @@ async function loadAll() {
const bpt = await storageGet(STORAGE_KEYS.backPanelTemplates); const bpt = await storageGet(STORAGE_KEYS.backPanelTemplates);
if (bpt && bpt.value) backPanelTemplates = JSON.parse(bpt.value); if (bpt && bpt.value) backPanelTemplates = JSON.parse(bpt.value);
} catch (e) {} } catch (e) {}
return { customers, products, quotes, sellerProfiles, certifications, productCategories, packingLists, catalogDocs, jmsLines, backPanelTemplates }; let suppliers = [];
try {
const sup = await storageGet(STORAGE_KEYS.suppliers);
if (sup && sup.value) suppliers = JSON.parse(sup.value);
} catch (e) {}
return { customers, products, quotes, sellerProfiles, certifications, productCategories, packingLists, catalogDocs, jmsLines, backPanelTemplates, suppliers };
} }
/* -------------------------------- styles -------------------------------- */ /* -------------------------------- styles -------------------------------- */
@ -1966,6 +1978,7 @@ const NAV_GROUPS = [
]; ];
const STANDALONE_NAV = [ const STANDALONE_NAV = [
{ id: 'certs', label: '授权管理', icon: ShieldCheck }, { id: 'certs', label: '授权管理', icon: ShieldCheck },
{ id: 'suppliers', label: '供应商名录', icon: Factory },
{ id: 'catalog', label: '产品目录', icon: BookOpen }, { id: 'catalog', label: '产品目录', icon: BookOpen },
{ id: 'quotes', label: '报价单', icon: FileText }, { id: 'quotes', label: '报价单', icon: FileText },
{ id: 'packing', label: '箱单', icon: Boxes }, { id: 'packing', label: '箱单', icon: Boxes },
@ -2119,6 +2132,17 @@ function MainApp({ username, onLogout }) {
const [certUploading, setCertUploading] = useState(false); const [certUploading, setCertUploading] = useState(false);
const [editingCertId, setEditingCertId] = useState(null); const [editingCertId, setEditingCertId] = useState(null);
const [certBrandDraft, setCertBrandDraft] = useState(''); const [certBrandDraft, setCertBrandDraft] = useState('');
const [suppliers, setSuppliers] = useState([]);
const [selectedSupplierId, setSelectedSupplierId] = useState(null);
const [supplierDraft, setSupplierDraft] = useState(null);
const [supplierSearch, setSupplierSearch] = useState('');
const [supplierAttachmentNote, setSupplierAttachmentNote] = useState('');
const [supplierAttachmentFile, setSupplierAttachmentFile] = useState(null);
const [supplierUploading, setSupplierUploading] = useState(false);
const [supplierFileInputKey, setSupplierFileInputKey] = useState(0);
const [supplierFileDragActive, setSupplierFileDragActive] = useState(false);
const [quoteSubView, setQuoteSubView] = useState('list'); // 'list' | 'editor' const [quoteSubView, setQuoteSubView] = useState('list'); // 'list' | 'editor'
const [quoteDraft, setQuoteDraft] = useState(null); const [quoteDraft, setQuoteDraft] = useState(null);
const [quoteTypeFilter, setQuoteTypeFilter] = useState('all'); const [quoteTypeFilter, setQuoteTypeFilter] = useState('all');
@ -2161,6 +2185,7 @@ function MainApp({ username, onLogout }) {
setCatalogDocs((data.catalogDocs || []).filter(d => d.fileName)); setCatalogDocs((data.catalogDocs || []).filter(d => d.fileName));
setJmsLines(data.jmsLines || []); setJmsLines(data.jmsLines || []);
setBackPanelTemplates(data.backPanelTemplates || []); setBackPanelTemplates(data.backPanelTemplates || []);
setSuppliers(data.suppliers || []);
setLoaded(true); setLoaded(true);
})(); })();
}, []); }, []);
@ -2174,6 +2199,7 @@ function MainApp({ username, onLogout }) {
useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.catalogDocs, JSON.stringify(catalogDocs)).catch(() => {}); }, [catalogDocs, loaded]); useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.catalogDocs, JSON.stringify(catalogDocs)).catch(() => {}); }, [catalogDocs, loaded]);
useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.jmsLines, JSON.stringify(jmsLines)).catch(() => {}); }, [jmsLines, loaded]); useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.jmsLines, JSON.stringify(jmsLines)).catch(() => {}); }, [jmsLines, loaded]);
useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.backPanelTemplates, JSON.stringify(backPanelTemplates)).catch(() => {}); }, [backPanelTemplates, loaded]); useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.backPanelTemplates, JSON.stringify(backPanelTemplates)).catch(() => {}); }, [backPanelTemplates, loaded]);
useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.suppliers, JSON.stringify(suppliers)).catch(() => {}); }, [suppliers, loaded]);
function notify(msg) { setToast(msg); setTimeout(() => setToast(''), 2200); } function notify(msg) { setToast(msg); setTimeout(() => setToast(''), 2200); }
@ -2186,6 +2212,7 @@ function MainApp({ username, onLogout }) {
const selected = customers.find(c => c.id === selectedId) || null; const selected = customers.find(c => c.id === selectedId) || null;
const selectedProduct = products.find(p => p.id === selectedProductId) || null; const selectedProduct = products.find(p => p.id === selectedProductId) || null;
const selectedSupplier = suppliers.find(s => s.id === selectedSupplierId) || null;
const [productExportLang, setProductExportLang] = useState('zh'); const [productExportLang, setProductExportLang] = useState('zh');
const [productExportLabels, setProductExportLabels] = useState({}); const [productExportLabels, setProductExportLabels] = useState({});
const [productExportLoading, setProductExportLoading] = useState(false); const [productExportLoading, setProductExportLoading] = useState(false);
@ -2234,6 +2261,19 @@ function MainApp({ username, onLogout }) {
return [...list].sort((a, b) => categoryBreadcrumb(a, productCategories).localeCompare(categoryBreadcrumb(b, productCategories)) || (a.name || '').localeCompare(b.name || '')); return [...list].sort((a, b) => categoryBreadcrumb(a, productCategories).localeCompare(categoryBreadcrumb(b, productCategories)) || (a.name || '').localeCompare(b.name || ''));
}, [products, productSearch, productCategoryFilter, productCategories]); }, [products, productSearch, productCategoryFilter, productCategories]);
const filteredSuppliers = useMemo(() => {
let list = suppliers;
if (supplierSearch.trim()) {
const q = supplierSearch.trim().toLowerCase();
list = list.filter(s =>
(s.name || '').toLowerCase().includes(q) ||
(s.contact || '').toLowerCase().includes(q) ||
(s.mainProducts || '').toLowerCase().includes(q)
);
}
return [...list].sort((a, b) => (a.name || '').localeCompare(b.name || ''));
}, [suppliers, supplierSearch]);
const counts = useMemo(() => ({ const counts = useMemo(() => ({
total: customers.length, total: customers.length,
products: products.length, products: products.length,
@ -2552,6 +2592,62 @@ function MainApp({ username, onLogout }) {
notify('已删除'); notify('已删除');
} }
/* --------------------------------- 供应商名录 --------------------------------- */
function startNewSupplier() { setSupplierDraft(blankSupplier()); setSelectedSupplierId(null); }
function startEditSupplier(s) { setSupplierDraft({ ...s }); setSelectedSupplierId(null); }
function cancelSupplierDraft() { setSupplierDraft(null); }
function saveSupplierDraft(s) {
setSuppliers(prev => {
const exists = prev.some(x => x.id === s.id);
return exists ? prev.map(x => (x.id === s.id ? s : x)) : [s, ...prev];
});
setSupplierDraft(null);
setSelectedSupplierId(s.id);
notify('已保存');
}
function deleteSupplier(id) {
if (!window.confirm('确定删除这家供应商?此操作不可恢复。')) return;
setSuppliers(prev => prev.filter(s => s.id !== id));
if (selectedSupplierId === id) setSelectedSupplierId(null);
notify('已删除');
}
function handleSupplierFileDragOver(e) { e.preventDefault(); setSupplierFileDragActive(true); }
function handleSupplierFileDragLeave() { setSupplierFileDragActive(false); }
function handleSupplierFileDrop(e) {
e.preventDefault();
setSupplierFileDragActive(false);
const f = e.dataTransfer.files && e.dataTransfer.files[0];
if (f) setSupplierAttachmentFile(f);
}
async function handleUploadSupplierAttachment(supplierId) {
if (!supplierAttachmentFile) { notify('请先选择文件'); return; }
setSupplierUploading(true);
try {
const res = await uploadFile(supplierId, supplierAttachmentFile);
setSuppliers(prev => prev.map(s => {
if (s.id !== supplierId) return s;
const entry = { fileName: res.fileName, originalName: res.originalName, note: supplierAttachmentNote.trim(), date: todayStr() };
return { ...s, attachments: [entry, ...(s.attachments || [])] };
}));
setSupplierAttachmentFile(null);
setSupplierAttachmentNote('');
setSupplierFileInputKey(k => k + 1);
notify('已上传');
} catch (e) {
notify('上传失败,请重试');
} finally {
setSupplierUploading(false);
}
}
async function handleDeleteSupplierAttachment(supplierId, fileName) {
if (!window.confirm('确定删除这个文件?此操作不可恢复。')) return;
try { await deleteFile(supplierId, fileName); } catch (e) {}
setSuppliers(prev => prev.map(s => (
s.id === supplierId ? { ...s, attachments: (s.attachments || []).filter(a => a.fileName !== fileName) } : s
)));
}
/* --------------------------------- Just My Socks --------------------------------- */ /* --------------------------------- Just My Socks --------------------------------- */
function saveJmsLine(line, editingId) { function saveJmsLine(line, editingId) {
@ -3579,6 +3675,56 @@ function MainApp({ username, onLogout }) {
</> </>
)} )}
{view === 'suppliers' && (
<div className="crm-layout">
<div>
<div className="search-box"><Search size={14} color="var(--ink-soft)" /><input placeholder="搜索厂家名称 / 联系人 / 主营产品" value={supplierSearch} onChange={e => setSupplierSearch(e.target.value)} /></div>
<button className="btn btn-primary" style={{ width: '100%', justifyContent: 'center', marginBottom: 12 }} onClick={startNewSupplier}><Plus size={14} />新建供应商</button>
<div className="panel">
<div className="panel-body">
{filteredSuppliers.length === 0 ? (
<div className="empty-state"><Factory size={26} />暂无供应商点击上方按钮添加第一个</div>
) : filteredSuppliers.map(s => (
<div key={s.id} className="row" style={{ cursor: 'pointer', background: selectedSupplierId === s.id ? '#FAFBFC' : undefined }} onClick={() => { setSupplierDraft(null); setSelectedSupplierId(s.id); }}>
<div className="row-main">
<div className="row-title">{s.name || '(未命名)'}</div>
<div className="row-sub">{s.contact && `${s.contact} · `}{s.mainProducts || '—'}</div>
</div>
<ChevronRight size={15} color="var(--ink-soft)" />
</div>
))}
</div>
</div>
</div>
<div>
{supplierDraft ? (
<SupplierForm draft={supplierDraft} setDraft={setSupplierDraft} onSave={saveSupplierDraft} onCancel={cancelSupplierDraft} />
) : selectedSupplier ? (
<SupplierDetail
supplier={selectedSupplier}
onEdit={() => startEditSupplier(selectedSupplier)}
onDelete={() => deleteSupplier(selectedSupplier.id)}
attachmentNote={supplierAttachmentNote}
setAttachmentNote={setSupplierAttachmentNote}
attachmentFileName={supplierAttachmentFile ? supplierAttachmentFile.name : ''}
onPickAttachment={(f) => setSupplierAttachmentFile(f)}
uploading={supplierUploading}
fileInputKey={supplierFileInputKey}
onUploadAttachment={() => handleUploadSupplierAttachment(selectedSupplier.id)}
onDeleteAttachment={(fileName) => handleDeleteSupplierAttachment(selectedSupplier.id, fileName)}
fileDragActive={supplierFileDragActive}
onFileDragOver={handleSupplierFileDragOver}
onFileDragLeave={handleSupplierFileDragLeave}
onFileDrop={handleSupplierFileDrop}
/>
) : (
<div className="panel"><div className="empty-state"><Factory size={28} />选择左侧供应商查看详情或新建一个供应商档案</div></div>
)}
</div>
</div>
)}
{view === 'quotes' && quoteSubView === 'list' && ( {view === 'quotes' && quoteSubView === 'list' && (
<> <>
<div style={{ display: 'flex', gap: 8, marginBottom: 14, flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: 8, marginBottom: 14, flexWrap: 'wrap' }}>
@ -3761,10 +3907,10 @@ function MainApp({ username, onLogout }) {
<div className="modal-box" onClick={e => e.stopPropagation()}> <div className="modal-box" onClick={e => e.stopPropagation()}>
<div className="modal-head"><div className="panel-title">数据备份JSON</div><button className="btn btn-ghost btn-sm" onClick={() => setExportOpen(false)}><X size={14} /></button></div> <div className="modal-head"><div className="panel-title">数据备份JSON</div><button className="btn btn-ghost btn-sm" onClick={() => setExportOpen(false)}><X size={14} /></button></div>
<div className="modal-body"> <div className="modal-body">
<textarea className="textarea" rows={14} readOnly value={JSON.stringify({ customers, products, quotes, sellerProfiles, certifications, productCategories, packingLists, catalogDocs }, null, 2)} /> <textarea className="textarea" rows={14} readOnly value={JSON.stringify({ customers, products, quotes, sellerProfiles, certifications, productCategories, packingLists, catalogDocs, suppliers }, null, 2)} />
</div> </div>
<div className="modal-foot"> <div className="modal-foot">
<button className="btn btn-primary" onClick={() => copyText(JSON.stringify({ customers, products, quotes, sellerProfiles, certifications, productCategories, packingLists, catalogDocs }, null, 2))}><Copy size={12} />复制全部</button> <button className="btn btn-primary" onClick={() => copyText(JSON.stringify({ customers, products, quotes, sellerProfiles, certifications, productCategories, packingLists, catalogDocs, suppliers }, null, 2))}><Copy size={12} />复制全部</button>
</div> </div>
</div> </div>
</div> </div>
@ -3997,6 +4143,107 @@ function MainApp({ username, onLogout }) {
/* ----------------------------- customer form / detail ----------------------------- */ /* ----------------------------- customer form / detail ----------------------------- */
function SupplierForm({ draft, setDraft, onSave, onCancel }) {
const set = (patch) => setDraft(d => ({ ...d, ...patch }));
return (
<div className="panel">
<div className="panel-header"><div className="panel-title"><Edit2 size={14} />{draft.name ? '编辑供应商' : '新建供应商'}</div></div>
<div className="panel-body" style={{ padding: 18 }}>
<div className="grid-2">
<div className="field"><span className="field-label">厂家 / 公司名称</span><input className="input" value={draft.name} onChange={e => set({ name: e.target.value })} /></div>
<div className="field"><span className="field-label">联系人</span><input className="input" value={draft.contact} onChange={e => set({ contact: e.target.value })} /></div>
<div className="field"><span className="field-label">电话</span><input className="input" value={draft.phone} onChange={e => set({ phone: e.target.value })} /></div>
<div className="field"><span className="field-label">邮箱</span><input className="input" value={draft.email} onChange={e => set({ email: e.target.value })} /></div>
<div className="field"><span className="field-label">微信 / WhatsApp</span><input className="input" value={draft.wechatOrWhatsapp} onChange={e => set({ wechatOrWhatsapp: e.target.value })} /></div>
<div className="field"><span className="field-label">网站</span><input className="input" value={draft.website} onChange={e => set({ website: e.target.value })} placeholder="https://" /></div>
</div>
<div className="field"><span className="field-label">地址</span><input className="input" value={draft.address} onChange={e => set({ address: e.target.value })} /></div>
<div className="field"><span className="field-label">主营产品</span><input className="input" value={draft.mainProducts} onChange={e => set({ mainProducts: e.target.value })} placeholder="如UPS电源、锂电池模块" /></div>
<div className="field"><span className="field-label">备注</span><textarea className="textarea" style={{ fontFamily: 'inherit', fontSize: 13 }} value={draft.notes} onChange={e => set({ notes: e.target.value })} /></div>
</div>
<div className="modal-foot" style={{ borderTop: '1px solid var(--line)', padding: 14 }}>
<button className="btn" onClick={onCancel}>取消</button>
<button className="btn btn-primary" onClick={() => onSave({ ...draft, name: draft.name.trim() })} disabled={!draft.name.trim()}><Save size={13} />保存</button>
</div>
</div>
);
}
function SupplierDetail({
supplier: s, onEdit, onDelete,
attachmentNote, setAttachmentNote, attachmentFileName, onPickAttachment, uploading, fileInputKey,
onUploadAttachment, onDeleteAttachment, fileDragActive, onFileDragOver, onFileDragLeave, onFileDrop,
}) {
const attachments = s.attachments || [];
return (
<div className="panel">
<div className="panel-header">
<div className="panel-title">{s.name || '(未命名)'}</div>
<div style={{ display: 'flex', gap: 8 }}>
<button className="btn btn-sm" onClick={onEdit}><Edit2 size={12} />编辑</button>
<button className="btn btn-sm btn-ghost btn-danger" onClick={onDelete}><Trash2 size={12} /></button>
</div>
</div>
<div className="panel-body" style={{ padding: 18 }}>
<div className="grid-2" style={{ marginBottom: 16 }}>
<Info icon={<Building2 size={13} />} label="联系人" value={s.contact || '—'} />
<Info icon={<Phone size={13} />} label="电话" value={s.phone || '—'} />
<Info icon={<Mail size={13} />} label="邮箱" value={s.email || '—'} />
<Info icon={<Phone size={13} />} label="微信 / WhatsApp" value={s.wechatOrWhatsapp || '—'} />
<Info icon={<Globe size={13} />} label="网站" value={s.website ? <a href={s.website} target="_blank" rel="noreferrer" style={{ color: 'var(--blue)' }}>{s.website}</a> : '—'} />
<Info icon={<Building2 size={13} />} label="地址" value={s.address || '—'} />
</div>
{s.mainProducts && <div className="field"><span className="field-label">主营产品</span><div style={{ fontSize: 12.5 }}>{s.mainProducts}</div></div>}
{s.notes && <div className="field"><span className="field-label">备注</span><div style={{ fontSize: 12.5, whiteSpace: 'pre-wrap' }}>{s.notes}</div></div>}
<div className="field">
<span className="field-label">厂家资料PDF / 图片等</span>
<div style={{ display: 'flex', gap: 8, alignItems: 'stretch', flexWrap: 'wrap', marginBottom: 8 }}>
<label
className={`dropzone ${fileDragActive ? 'drag-active' : ''}`}
onDragOver={onFileDragOver} onDragLeave={onFileDragLeave} onDrop={onFileDrop}
>
<input
key={fileInputKey} type="file"
onChange={e => onPickAttachment(e.target.files && e.target.files[0])}
style={{ display: 'none' }}
/>
<Paperclip size={15} style={{ marginBottom: 4 }} />
<div>{fileDragActive ? '松开鼠标完成选择' : attachmentFileName ? `已选择:${attachmentFileName}` : '点击选择文件,或把文件拖到这里'}</div>
</label>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: '1 1 160px' }}>
<input
className="input" placeholder="备注可选2026版价格表"
value={attachmentNote} onChange={e => setAttachmentNote(e.target.value)}
/>
<button className="btn btn-sm" disabled={uploading} onClick={onUploadAttachment} style={{ justifyContent: 'center' }}>
<Plus size={12} />{uploading ? '上传中…' : '上传'}
</button>
</div>
</div>
</div>
<CollapsibleSection title="文件列表" count={attachments.length} defaultOpen={attachments.length > 0}>
{attachments.length === 0 ? (
<div className="row-sub" style={{ padding: '8px 0' }}>暂无文件</div>
) : attachments.map((a, i) => (
<div key={a.fileName + i} style={{ display: 'flex', gap: 10, padding: '8px 0', borderBottom: '1px solid var(--line)', alignItems: 'flex-start' }}>
<Paperclip size={13} color="var(--ink-soft)" style={{ marginTop: 2, flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}>
<span className="crm-mono" style={{ fontSize: 11.5, color: 'var(--ink-soft)' }}>{a.date}</span>
<div style={{ fontSize: 12.5, wordBreak: 'break-word', marginTop: 2 }}>
<a href={`/uploads/${s.id}/${a.fileName}`} target="_blank" rel="noreferrer" download={a.originalName} style={{ color: 'var(--blue)' }}>{a.originalName}</a>
{a.note && <span style={{ color: 'var(--ink-soft)' }}> {a.note}</span>}
</div>
</div>
<button className="btn btn-sm btn-ghost btn-danger" onClick={() => onDeleteAttachment(a.fileName)}><Trash2 size={12} /></button>
</div>
))}
</CollapsibleSection>
</div>
</div>
);
}
function CustomerForm({ draft, setDraft, onSave, onCancel }) { function CustomerForm({ draft, setDraft, onSave, onCancel }) {
const set = (patch) => setDraft(d => ({ ...d, ...patch })); const set = (patch) => setDraft(d => ({ ...d, ...patch }));