Fix supervisor payments mobile tabs, reconcile donor, report method buckets, and refund/donation accounting
- Supervisor Payments: mode tabs now wrap on mobile instead of overflowing off-screen. - Reconciling a Yoco transaction as a donation now lets staff pick who it's from. - Reports payment-method breakdown now buckets into Cash/Card/EFT/Other everywhere, folding Apple Pay, Google Pay, and Yoco-portal payments into Card. - Refunds now net against their original method's bucket (Cashup/Finance/Profit reports, My Payments filtering/display) instead of vanishing or falling into "Other". - Refund form's method dropdown mirrors the real payment methods and auto-fills from the payment being refunded, replacing an ambiguous generic "Refund" option. - Fixed donation remaining/unallocated balance inflating instead of shrinking when a donation is refunded (assign-donation endpoint, cashup reports, donations reports, and the Assign Donation panel all summed refund legs with the wrong sign). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -336,12 +336,17 @@ export default function ReportsV2({ onBack }: { onBack?: () => void } = {}) {
|
||||
}
|
||||
};
|
||||
|
||||
// Payment method label mapping
|
||||
// Payment method label mapping — bucket into the same 4 categories used by the finance/cashup
|
||||
// reports (cash, card, eft, other). Card-network wallets (Apple Pay, Google Pay) and Yoco
|
||||
// checkout-portal payments settle exactly like a card — no separate float to reconcile — so
|
||||
// they fold into "Card" rather than showing as their own categories. Mirrors bucketForMethod
|
||||
// in backend/src/utils/cashupUtils.js.
|
||||
const getPaymentMethod = (p: any) => {
|
||||
try {
|
||||
if (p?.externalId) return "Yoco Portal";
|
||||
} catch {}
|
||||
return p?.method || "Unknown";
|
||||
const m = String(p?.method || "").toLowerCase();
|
||||
if (m.includes("cash")) return "Cash";
|
||||
if (m.includes("eft")) return "EFT";
|
||||
if (m.includes("card") || m.includes("yoco") || m.includes("pay") || p?.externalId) return "Card";
|
||||
return "Other";
|
||||
};
|
||||
|
||||
// Derived rows for payments (apply date filter)
|
||||
@@ -474,9 +479,12 @@ export default function ReportsV2({ onBack }: { onBack?: () => void } = {}) {
|
||||
const evTitle = ev?.title || evId;
|
||||
const evPayments = paymentsByEvent[evId] || [];
|
||||
evPayments.filter((p: any) => p.isDonation).forEach((donation: any) => {
|
||||
// A refund of the donation itself also creates a leg (originalPaymentId -> donation),
|
||||
// with a negative amount — Math.abs() so it reduces "used" (money no longer available)
|
||||
// instead of a raw signed sum subtracting a negative and inflating "unused" below.
|
||||
const used = evPayments
|
||||
.filter((leg: any) => !leg.isDonation && leg.originalPaymentId === donation.id)
|
||||
.reduce((s: number, leg: any) => s + (leg.amount || 0), 0);
|
||||
.reduce((s: number, leg: any) => s + Math.abs(leg.amount || 0), 0);
|
||||
rows.push({
|
||||
eventId: evId,
|
||||
eventTitle: evTitle,
|
||||
@@ -858,9 +866,11 @@ export default function ReportsV2({ onBack }: { onBack?: () => void } = {}) {
|
||||
};
|
||||
}
|
||||
if (report === "donations") {
|
||||
// Math.abs(): a refund of the donation itself also creates a leg (originalPaymentId ->
|
||||
// donation) with a negative amount, which must reduce "used" rather than inflate "unused".
|
||||
const usedFor = (evId: string, donationId: string) =>
|
||||
(paymentsByEvent[evId] || []).filter((leg: any) => !leg.isDonation && leg.originalPaymentId === donationId)
|
||||
.reduce((s: number, leg: any) => s + (leg.amount || 0), 0);
|
||||
.reduce((s: number, leg: any) => s + Math.abs(leg.amount || 0), 0);
|
||||
const rows: (string|number)[][] = [];
|
||||
let grandTotal = 0, grandUsed = 0;
|
||||
Object.keys(paymentsByEvent).forEach(evId => {
|
||||
@@ -1531,10 +1541,12 @@ export default function ReportsV2({ onBack }: { onBack?: () => void } = {}) {
|
||||
// sum of every leg (isDonation:false, originalPaymentId -> the donation)
|
||||
// referencing it, which — since a donation and its legs share the event
|
||||
// it was logged against in the common case — are already present in the
|
||||
// same per-event payments list.
|
||||
// same per-event payments list. Math.abs(): a refund of the donation itself
|
||||
// also creates such a leg, with a negative amount, which must reduce "used"
|
||||
// rather than inflate "unused" via a raw signed sum.
|
||||
const usedFor = (evId: string, donationId: string) =>
|
||||
(paymentsByEvent[evId] || []).filter((leg: any) => !leg.isDonation && leg.originalPaymentId === donationId)
|
||||
.reduce((s: number, leg: any) => s + (leg.amount || 0), 0);
|
||||
.reduce((s: number, leg: any) => s + Math.abs(leg.amount || 0), 0);
|
||||
let grandTotal = 0, grandUsed = 0, grandCount = 0;
|
||||
const perEvent = Object.keys(paymentsByEvent).map(evId => {
|
||||
const ev = filteredEvents.find(e => e.id === evId);
|
||||
|
||||
Reference in New Issue
Block a user