update: CRM customers, orders, device detail, and status system changes
- CustomerList, CustomerForm, CustomerDetail: various updates - Orders: removed OrderDetail and OrderForm, updated OrderList and index - DeviceDetail: updates - index.css: added new styles - CRM_STATUS_SYSTEM_PLAN.md: new planning document - Added customer-status assets and CustomerDetail subfolder Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
426
frontend/src/crm/customers/CustomerDetail/FinancialsTab.jsx
Normal file
426
frontend/src/crm/customers/CustomerDetail/FinancialsTab.jsx
Normal file
@@ -0,0 +1,426 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import api from "../../../api/client";
|
||||
import {
|
||||
FLOW_LABELS, PAYMENT_TYPE_LABELS, CATEGORY_LABELS,
|
||||
ORDER_STATUS_LABELS, fmtDate,
|
||||
} from "./shared";
|
||||
|
||||
const labelStyle = { fontSize: 11, fontWeight: 600, color: "var(--text-muted)", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 4 };
|
||||
|
||||
// Greek number format: 12500800.70 → "12.500.800,70"
|
||||
function fmtEuro(amount, currency = "EUR") {
|
||||
const n = Number(amount || 0);
|
||||
const [intPart, decPart] = n.toFixed(2).split(".");
|
||||
const intFormatted = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ".");
|
||||
const suffix = currency && currency !== "EUR" ? ` ${currency}` : "";
|
||||
return `€${intFormatted},${decPart}${suffix}`;
|
||||
}
|
||||
const inputStyle = { backgroundColor: "var(--bg-input)", borderColor: "var(--border-primary)", color: "var(--text-primary)", border: "1px solid var(--border-primary)", borderRadius: 6, padding: "6px 10px", fontSize: 13, width: "100%" };
|
||||
|
||||
const TERMINAL = new Set(["declined", "complete"]);
|
||||
|
||||
const emptyTxn = () => ({
|
||||
date: new Date().toISOString().slice(0, 16),
|
||||
flow: "payment",
|
||||
payment_type: "cash",
|
||||
category: "full_payment",
|
||||
amount: "",
|
||||
currency: "EUR",
|
||||
invoice_ref: "",
|
||||
order_ref: "",
|
||||
recorded_by: "",
|
||||
note: "",
|
||||
});
|
||||
|
||||
function TransactionModal({ initialData, orders, customerId, onClose, onSaved, user, editIndex, outstandingBalance }) {
|
||||
const [form, setForm] = useState(() => ({
|
||||
...emptyTxn(),
|
||||
recorded_by: user?.name || "",
|
||||
...initialData,
|
||||
}));
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const set = (k, v) => setForm((f) => ({ ...f, [k]: v }));
|
||||
|
||||
const isInvoice = form.flow === "invoice";
|
||||
|
||||
// Auto-fill amount when flow=payment + category=full_payment
|
||||
// - No order_ref: fill total outstanding across all orders
|
||||
// - With order_ref: fill balance due for that specific order
|
||||
// - Any other flow/category: clear amount
|
||||
const isFullPayment = form.flow === "payment" && form.category === "full_payment";
|
||||
useEffect(() => {
|
||||
if (initialData) return; // don't touch edits
|
||||
if (!isFullPayment) {
|
||||
setForm((f) => ({ ...f, amount: "" }));
|
||||
return;
|
||||
}
|
||||
if (form.order_ref) {
|
||||
// Find balance due for this specific order
|
||||
const order = (orders || []).find((o) => o.id === form.order_ref);
|
||||
const due = order?.payment_status?.balance_due ?? 0;
|
||||
if (due > 0) setForm((f) => ({ ...f, amount: Number(due).toFixed(2) }));
|
||||
} else {
|
||||
// Total outstanding across all orders
|
||||
if (outstandingBalance != null && outstandingBalance > 0) {
|
||||
setForm((f) => ({ ...f, amount: outstandingBalance.toFixed(2) }));
|
||||
}
|
||||
}
|
||||
}, [isFullPayment, form.order_ref]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.amount || !form.flow) {
|
||||
alert("Amount and Flow are required.");
|
||||
return;
|
||||
}
|
||||
// Category is required only for non-invoice flows
|
||||
if (!isInvoice && !form.category) {
|
||||
alert("Category is required.");
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
const payload = {
|
||||
...form,
|
||||
amount: parseFloat(form.amount) || 0,
|
||||
date: form.date ? new Date(form.date).toISOString() : new Date().toISOString(),
|
||||
invoice_ref: form.invoice_ref || null,
|
||||
order_ref: form.order_ref || null,
|
||||
payment_type: isInvoice ? null : (form.payment_type || null),
|
||||
// For invoices, category defaults to "full_payment" as a neutral placeholder
|
||||
category: isInvoice ? "full_payment" : (form.category || "full_payment"),
|
||||
};
|
||||
let updated;
|
||||
if (editIndex !== undefined && editIndex !== null) {
|
||||
updated = await api.patch(`/crm/customers/${customerId}/transactions/${editIndex}`, payload);
|
||||
} else {
|
||||
updated = await api.post(`/crm/customers/${customerId}/transactions`, payload);
|
||||
}
|
||||
onSaved(updated);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: "fixed", inset: 0, backgroundColor: "rgba(0,0,0,0.6)",
|
||||
display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000,
|
||||
}}>
|
||||
<div style={{
|
||||
backgroundColor: "var(--bg-card)", borderRadius: 12, padding: 24,
|
||||
width: "100%", maxWidth: 520, border: "1px solid var(--border-primary)",
|
||||
}}>
|
||||
<h3 style={{ fontSize: 15, fontWeight: 700, color: "var(--text-heading)", marginBottom: 18 }}>
|
||||
{editIndex !== null && editIndex !== undefined ? "Edit Transaction" : "Record Transaction"}
|
||||
</h3>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
|
||||
<div>
|
||||
<div style={labelStyle}>Date</div>
|
||||
<input type="datetime-local" value={form.date} onChange={(e) => set("date", e.target.value)} style={inputStyle} />
|
||||
</div>
|
||||
<div>
|
||||
<div style={labelStyle}>Flow</div>
|
||||
<select value={form.flow} onChange={(e) => set("flow", e.target.value)} style={inputStyle}>
|
||||
{Object.entries(FLOW_LABELS).map(([v, l]) => <option key={v} value={v}>{l}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
{/* Payment type — hidden for invoices */}
|
||||
{!isInvoice && (
|
||||
<div>
|
||||
<div style={labelStyle}>Payment Type</div>
|
||||
<select value={form.payment_type || ""} onChange={(e) => set("payment_type", e.target.value)} style={inputStyle}>
|
||||
<option value="">—</option>
|
||||
{Object.entries(PAYMENT_TYPE_LABELS).map(([v, l]) => <option key={v} value={v}>{l}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{/* Category — hidden for invoices */}
|
||||
{!isInvoice && (
|
||||
<div>
|
||||
<div style={labelStyle}>Category</div>
|
||||
<select value={form.category} onChange={(e) => set("category", e.target.value)} style={inputStyle}>
|
||||
{Object.entries(CATEGORY_LABELS).map(([v, l]) => <option key={v} value={v}>{l}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div style={labelStyle}>Amount</div>
|
||||
<input type="number" min="0" step="0.01" value={form.amount} onChange={(e) => set("amount", e.target.value)} style={inputStyle} placeholder="0.00" />
|
||||
</div>
|
||||
<div>
|
||||
<div style={labelStyle}>Currency</div>
|
||||
<select value={form.currency} onChange={(e) => set("currency", e.target.value)} style={inputStyle}>
|
||||
{["EUR", "USD", "GBP"].map((c) => <option key={c} value={c}>{c}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<div style={labelStyle}>Invoice Ref</div>
|
||||
<input type="text" value={form.invoice_ref || ""} onChange={(e) => set("invoice_ref", e.target.value)} style={inputStyle} placeholder="e.g. INV-2026-001" />
|
||||
</div>
|
||||
<div>
|
||||
<div style={labelStyle}>Order Ref</div>
|
||||
<select value={form.order_ref || ""} onChange={(e) => set("order_ref", e.target.value)} style={inputStyle}>
|
||||
<option value="">— None —</option>
|
||||
{(orders || []).map((o) => (
|
||||
<option key={o.id} value={o.id}>{o.order_number || o.id.slice(0, 8)}{o.title ? ` — ${o.title}` : ""}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ gridColumn: "1 / -1" }}>
|
||||
<div style={labelStyle}>Recorded By</div>
|
||||
<input type="text" value={form.recorded_by} onChange={(e) => set("recorded_by", e.target.value)} style={inputStyle} />
|
||||
</div>
|
||||
<div style={{ gridColumn: "1 / -1" }}>
|
||||
<div style={labelStyle}>Note</div>
|
||||
<textarea rows={2} value={form.note} onChange={(e) => set("note", e.target.value)} style={{ ...inputStyle, resize: "vertical" }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-4 justify-end">
|
||||
<button type="button" onClick={onClose}
|
||||
style={{ fontSize: 13, padding: "6px 16px", borderRadius: 6, border: "1px solid var(--border-primary)", backgroundColor: "transparent", color: "var(--text-muted)", cursor: "pointer" }}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" onClick={handleSubmit} disabled={saving}
|
||||
style={{ fontSize: 13, fontWeight: 600, padding: "6px 18px", borderRadius: 6, border: "none", backgroundColor: "var(--btn-primary)", color: "#fff", cursor: "pointer", opacity: saving ? 0.6 : 1 }}>
|
||||
{saving ? "Saving..." : "Save"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DeleteConfirmModal({ onConfirm, onCancel, deleting }) {
|
||||
return (
|
||||
<div style={{ position: "fixed", inset: 0, backgroundColor: "rgba(0,0,0,0.6)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1001 }}>
|
||||
<div style={{ backgroundColor: "var(--bg-card)", borderRadius: 10, padding: 24, width: 340, border: "1px solid var(--border-primary)" }}>
|
||||
<p style={{ fontSize: 14, color: "var(--text-primary)", marginBottom: 18 }}>Delete this transaction? This cannot be undone.</p>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<button type="button" onClick={onCancel} style={{ fontSize: 13, padding: "5px 14px", borderRadius: 6, border: "1px solid var(--border-primary)", backgroundColor: "transparent", color: "var(--text-muted)", cursor: "pointer" }}>Cancel</button>
|
||||
<button type="button" onClick={onConfirm} disabled={deleting}
|
||||
style={{ fontSize: 13, fontWeight: 600, padding: "5px 14px", borderRadius: 6, border: "none", backgroundColor: "var(--danger)", color: "#fff", cursor: "pointer", opacity: deleting ? 0.6 : 1 }}>
|
||||
{deleting ? "Deleting..." : "Delete"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Small stat box used in the financial summary sections
|
||||
function StatBox({ label, value, color, note }) {
|
||||
return (
|
||||
<div style={{ backgroundColor: "var(--bg-primary)", padding: "10px 14px", borderRadius: 8, border: "1px solid var(--border-secondary)" }}>
|
||||
<div style={{ ...labelStyle, marginBottom: 4 }}>{label}</div>
|
||||
<div style={{ fontSize: 16, fontWeight: 700, color: color || "var(--text-primary)" }}>{value}</div>
|
||||
{note && <div style={{ fontSize: 11, color: "var(--text-muted)", marginTop: 2 }}>{note}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FinancialsTab({ customer, orders, canEdit, onCustomerUpdated, onReloadOrders, user, onTabChange }) {
|
||||
const [showTxnModal, setShowTxnModal] = useState(false);
|
||||
const [editTxnIndex, setEditTxnIndex] = useState(null);
|
||||
const [deleteTxnIndex, setDeleteTxnIndex] = useState(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
const txns = [...(customer.transaction_history || [])].sort((a, b) => (b.date || "").localeCompare(a.date || ""));
|
||||
|
||||
// Overall totals (all orders combined)
|
||||
const totalInvoiced = (customer.transaction_history || []).filter((t) => t.flow === "invoice").reduce((s, t) => s + (t.amount || 0), 0);
|
||||
const totalPaid = (customer.transaction_history || []).filter((t) => t.flow === "payment").reduce((s, t) => s + (t.amount || 0), 0);
|
||||
const outstanding = totalInvoiced - totalPaid;
|
||||
const totalOrders = orders.length;
|
||||
|
||||
// Active orders (not completed/declined) — for the per-order payment status section
|
||||
const activeOrders = orders.filter((o) => !TERMINAL.has(o.status));
|
||||
|
||||
const handleDelete = async () => {
|
||||
setDeleting(true);
|
||||
try {
|
||||
const updated = await api.delete(`/crm/customers/${customer.id}/transactions/${deleteTxnIndex}`);
|
||||
onCustomerUpdated(updated);
|
||||
setDeleteTxnIndex(null);
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* ── Overall Customer Financial Status ── */}
|
||||
<div className="ui-section-card mb-4">
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: "var(--text-heading)", marginBottom: 14 }}>Overall Financial Status</div>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(130px, 1fr))", gap: 12 }}>
|
||||
<StatBox label="Total Invoiced" value={fmtEuro(totalInvoiced)} />
|
||||
<StatBox
|
||||
label="Total Received"
|
||||
value={fmtEuro(totalPaid)}
|
||||
color="var(--crm-rel-active-text)"
|
||||
/>
|
||||
<StatBox
|
||||
label="Balance Due"
|
||||
value={fmtEuro(outstanding)}
|
||||
color={outstanding > 0 ? "var(--crm-ord-awaiting_payment-text)" : "var(--crm-rel-active-text)"}
|
||||
/>
|
||||
<StatBox label="Total Orders" value={totalOrders} color="var(--text-heading)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Active Orders Payment Status ── */}
|
||||
{activeOrders.length > 0 && (
|
||||
<div className="ui-section-card mb-4">
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: "var(--text-heading)", marginBottom: 14 }}>Active Orders Status</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
|
||||
{activeOrders.map((order) => {
|
||||
const ps = order.payment_status || {};
|
||||
return (
|
||||
<div key={order.id}>
|
||||
{/* Order label */}
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: "var(--text-muted)", marginBottom: 8, display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<span style={{ fontFamily: "monospace", color: "var(--text-heading)" }}>{order.order_number}</span>
|
||||
{order.title && <span>— {order.title}</span>}
|
||||
<span style={{ padding: "1px 7px", borderRadius: 8, fontSize: 10, backgroundColor: "var(--bg-card-hover)", color: "var(--text-secondary)" }}>
|
||||
{ORDER_STATUS_LABELS[order.status] || order.status}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(120px, 1fr))", gap: 10 }}>
|
||||
<StatBox label="Required" value={fmtEuro(ps.required_amount)} />
|
||||
<StatBox
|
||||
label="Received"
|
||||
value={fmtEuro(ps.received_amount)}
|
||||
color="var(--crm-rel-active-text)"
|
||||
/>
|
||||
<StatBox
|
||||
label="Balance Due"
|
||||
value={fmtEuro(ps.balance_due)}
|
||||
color={(ps.balance_due || 0) > 0 ? "var(--crm-ord-awaiting_payment-text)" : "var(--crm-rel-active-text)"}
|
||||
/>
|
||||
<div style={{ backgroundColor: "var(--bg-primary)", padding: "10px 14px", borderRadius: 8, border: "1px solid var(--border-secondary)" }}>
|
||||
<div style={{ ...labelStyle, marginBottom: 4 }}>Payment</div>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: ps.payment_complete ? "var(--crm-rel-active-text)" : "var(--text-muted)" }}>
|
||||
{ps.payment_complete ? "Complete" : (ps.required_amount || 0) === 0 ? "No Invoice" : "Pending"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Separator between orders */}
|
||||
{activeOrders.indexOf(order) < activeOrders.length - 1 && (
|
||||
<div style={{ borderBottom: "1px solid var(--border-secondary)", marginTop: 14 }} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Transaction History ── */}
|
||||
<div className="ui-section-card">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span style={{ fontSize: 14, fontWeight: 600, color: "var(--text-heading)" }}>Transaction History</span>
|
||||
{canEdit && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setEditTxnIndex(null); setShowTxnModal(true); }}
|
||||
style={{ fontSize: 12, fontWeight: 600, padding: "5px 14px", borderRadius: 6, border: "none", backgroundColor: "var(--btn-primary)", color: "#fff", cursor: "pointer" }}
|
||||
>
|
||||
+ Add Transaction
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{txns.length === 0 ? (
|
||||
<p style={{ fontSize: 13, color: "var(--text-muted)", fontStyle: "italic" }}>No transactions recorded.</p>
|
||||
) : (
|
||||
<div style={{ overflowX: "auto" }}>
|
||||
<table className="w-full text-sm" style={{ borderCollapse: "collapse", minWidth: 700 }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: "1px solid var(--border-primary)" }}>
|
||||
{["Date", "Flow", "Amount", "Method", "Category", "Order Ref", "Note", "By", ""].map((h) => (
|
||||
<th key={h} style={{ padding: "6px 10px", textAlign: "left", ...labelStyle, marginBottom: 0 }}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{txns.map((t, i) => {
|
||||
const origIdx = (customer.transaction_history || []).findIndex(
|
||||
(x) => x.date === t.date && x.amount === t.amount && x.note === t.note && x.recorded_by === t.recorded_by
|
||||
);
|
||||
const linkedOrder = t.order_ref ? orders.find((o) => o.id === t.order_ref) : null;
|
||||
return (
|
||||
<tr key={i} style={{ borderBottom: "1px solid var(--border-secondary)" }}
|
||||
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = "var(--bg-card-hover)"}
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = "transparent"}>
|
||||
<td style={{ padding: "7px 10px", color: "var(--text-muted)", whiteSpace: "nowrap" }}>{fmtDate(t.date)}</td>
|
||||
<td style={{ padding: "7px 10px" }}>
|
||||
<span style={{
|
||||
padding: "2px 8px", borderRadius: 10, fontSize: 11, fontWeight: 600,
|
||||
backgroundColor: t.flow === "payment" ? "var(--crm-rel-active-bg)" : t.flow === "invoice" ? "var(--badge-blue-bg)" : "var(--bg-card-hover)",
|
||||
color: t.flow === "payment" ? "var(--crm-rel-active-text)" : t.flow === "invoice" ? "var(--badge-blue-text)" : "var(--text-secondary)",
|
||||
}}>
|
||||
{FLOW_LABELS[t.flow] || t.flow}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: "7px 10px", fontWeight: 600, color: t.flow === "payment" ? "var(--crm-rel-active-text)" : "var(--text-primary)", whiteSpace: "nowrap" }}>
|
||||
{t.flow === "payment" ? "+" : t.flow === "invoice" ? "-" : ""}{fmtEuro(t.amount, t.currency)}
|
||||
</td>
|
||||
<td style={{ padding: "7px 10px", color: "var(--text-muted)" }}>{PAYMENT_TYPE_LABELS[t.payment_type] || "—"}</td>
|
||||
<td style={{ padding: "7px 10px", color: "var(--text-muted)" }}>
|
||||
{t.flow === "invoice" ? "—" : (CATEGORY_LABELS[t.category] || t.category)}
|
||||
</td>
|
||||
<td style={{ padding: "7px 10px" }}>
|
||||
{linkedOrder ? (
|
||||
<button type="button"
|
||||
onClick={() => onTabChange?.("Orders", linkedOrder.id)}
|
||||
style={{ fontSize: 11, fontFamily: "monospace", color: "var(--accent)", background: "none", border: "none", cursor: "pointer", padding: 0, textDecoration: "underline" }}>
|
||||
{linkedOrder.order_number}
|
||||
</button>
|
||||
) : (t.order_ref ? <span style={{ fontSize: 10, color: "var(--text-muted)", fontStyle: "italic" }}>{t.order_ref.slice(0, 8)}</span> : "—")}
|
||||
</td>
|
||||
<td style={{ padding: "7px 10px", color: "var(--text-muted)", maxWidth: 160, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{t.note || "—"}</td>
|
||||
<td style={{ padding: "7px 10px", color: "var(--text-muted)" }}>{t.recorded_by || "—"}</td>
|
||||
{canEdit && (
|
||||
<td style={{ padding: "7px 10px", whiteSpace: "nowrap" }}>
|
||||
<button type="button" onClick={() => { setEditTxnIndex(origIdx); setShowTxnModal(true); }}
|
||||
style={{ fontSize: 11, padding: "2px 8px", borderRadius: 4, border: "1px solid var(--border-primary)", backgroundColor: "transparent", color: "var(--text-secondary)", cursor: "pointer", marginRight: 4 }}>
|
||||
Edit
|
||||
</button>
|
||||
<button type="button" onClick={() => setDeleteTxnIndex(origIdx)}
|
||||
style={{ fontSize: 11, padding: "2px 8px", borderRadius: 4, border: "1px solid var(--danger)", backgroundColor: "transparent", color: "var(--danger)", cursor: "pointer" }}>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showTxnModal && (
|
||||
<TransactionModal
|
||||
customerId={customer.id}
|
||||
orders={orders}
|
||||
user={user}
|
||||
editIndex={editTxnIndex}
|
||||
initialData={editTxnIndex !== null && editTxnIndex !== undefined ? (customer.transaction_history || [])[editTxnIndex] : undefined}
|
||||
outstandingBalance={outstanding}
|
||||
onClose={() => { setShowTxnModal(false); setEditTxnIndex(null); }}
|
||||
onSaved={(updated) => { onCustomerUpdated(updated); onReloadOrders?.(); }}
|
||||
/>
|
||||
)}
|
||||
{deleteTxnIndex !== null && (
|
||||
<DeleteConfirmModal onConfirm={handleDelete} onCancel={() => setDeleteTxnIndex(null)} deleting={deleting} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user