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 (
{editIndex !== null && editIndex !== undefined ? "Edit Transaction" : "Record Transaction"}
Flow
{/* Payment type — hidden for invoices */}
{!isInvoice && (
Payment Type
)}
{/* Category — hidden for invoices */}
{!isInvoice && (
Category
)}
Currency
Order Ref
);
}
function DeleteConfirmModal({ onConfirm, onCancel, deleting }) {
return (
Delete this transaction? This cannot be undone.
);
}
// Small stat box used in the financial summary sections
function StatBox({ label, value, color, note }) {
return (
{label}
{value}
{note &&
{note}
}
);
}
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 (
{/* ── Overall Customer Financial Status ── */}
Overall Financial Status
0 ? "var(--crm-ord-awaiting_payment-text)" : "var(--crm-rel-active-text)"}
/>
{/* ── Active Orders Payment Status ── */}
{activeOrders.length > 0 && (
Active Orders Status
{activeOrders.map((order) => {
const ps = order.payment_status || {};
return (
{/* Order label */}
{order.order_number}
{order.title && — {order.title}}
{ORDER_STATUS_LABELS[order.status] || order.status}
0 ? "var(--crm-ord-awaiting_payment-text)" : "var(--crm-rel-active-text)"}
/>
Payment
{ps.payment_complete ? "Complete" : (ps.required_amount || 0) === 0 ? "No Invoice" : "Pending"}
{/* Separator between orders */}
{activeOrders.indexOf(order) < activeOrders.length - 1 && (
)}
);
})}
)}
{/* ── Transaction History ── */}
Transaction History
{canEdit && (
)}
{txns.length === 0 ? (
No transactions recorded.
) : (
{["Date", "Flow", "Amount", "Method", "Category", "Order Ref", "Note", "By", ""].map((h) => (
| {h} |
))}
{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 (
e.currentTarget.style.backgroundColor = "var(--bg-card-hover)"}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = "transparent"}>
| {fmtDate(t.date)} |
{FLOW_LABELS[t.flow] || t.flow}
|
{t.flow === "payment" ? "+" : t.flow === "invoice" ? "-" : ""}{fmtEuro(t.amount, t.currency)}
|
{PAYMENT_TYPE_LABELS[t.payment_type] || "—"} |
{t.flow === "invoice" ? "—" : (CATEGORY_LABELS[t.category] || t.category)}
|
{linkedOrder ? (
) : (t.order_ref ? {t.order_ref.slice(0, 8)} : "—")}
|
{t.note || "—"} |
{t.recorded_by || "—"} |
{canEdit && (
|
)}
);
})}
)}
{showTxnModal && (
{ setShowTxnModal(false); setEditTxnIndex(null); }}
onSaved={(updated) => { onCustomerUpdated(updated); onReloadOrders?.(); }}
/>
)}
{deleteTxnIndex !== null && (
setDeleteTxnIndex(null)} deleting={deleting} />
)}
);
}