@@ -473,11 +473,26 @@ export default function ReportsV2() {
return map ;
} , [ paymentsByEvent ] ) ;
// Yoco processing fee totals per registration, from the per-payment snapshot (see
// Payment.feeAmount in the backend) — never recomputed from current settings, so this
// always reflects what was actually estimated at payment time.
const feeByReg = useMemo ( ( ) = > {
const map = new Map < string , number > ( ) ;
Object . keys ( paymentsByEvent ) . forEach ( evId = > {
( paymentsByEvent [ evId ] || [ ] ) . forEach ( ( p : any ) = > {
if ( p . registrationId && p . feeAmount != null ) {
map . set ( p . registrationId , ( map . get ( p . registrationId ) || 0 ) + ( p . feeAmount || 0 ) ) ;
}
} ) ;
} ) ;
return map ;
} , [ paymentsByEvent ] ) ;
const revDetRows = useMemo ( ( ) = > {
if ( report !== "revenueDetailed" ) return [ ] as any [ ] ;
const df = payFrom ? new Date ( payFrom ) . getTime ( ) : null ;
const dt = payTo ? new Date ( payTo ) . getTime ( ) : null ;
type Row = { eventId : string ; eventTitle : string ; userName : string ; userEmail? : string ; totalPaid : number ; status? : string ; registrationId : string ; outstanding : number } ;
type Row = { eventId : string ; eventTitle : string ; userName : string ; userEmail? : string ; totalPaid : number ; status? : string ; registrationId : string ; outstanding : number ; fee : number ; netAfterFee : number } ;
const rows : Row [ ] = [ ] ;
// Iterate registrations to include those with zero payments and show each once
Object . keys ( registrationsByEvent ) . forEach ( evId = > {
@@ -489,6 +504,7 @@ export default function ReportsV2() {
if ( dt && created && created > dt ) return ;
const totalPaid = paidByReg . get ( r . id ) || 0 ;
const outstanding = outstandingByReg . get ( r . id ) || 0 ;
const fee = feeByReg . get ( r . id ) || 0 ;
rows . push ( {
eventId : evId ,
eventTitle : evTitle ,
@@ -498,12 +514,14 @@ export default function ReportsV2() {
status : r.status ,
registrationId : r.id ,
outstanding ,
fee ,
netAfterFee : totalPaid - fee ,
} ) ;
} ) ;
} ) ;
rows . sort ( ( a , b ) = > ( a . eventTitle . localeCompare ( b . eventTitle ) || ( a . userName || '' ) . localeCompare ( b . userName || '' ) ) ) ;
return rows ;
} , [ report , registrationsByEvent , filteredEvents , payFrom , payTo , paidByReg , outstandingByReg ] ) ;
} , [ report , registrationsByEvent , filteredEvents , payFrom , payTo , paidByReg , outstandingByReg , feeByReg ] ) ;
// Derived for attendees (layered) for a single event
const attendeesLayer = useMemo ( ( ) = > {
@@ -631,6 +649,7 @@ export default function ReportsV2() {
totalPaid : paidByReg.get ( r . id ) || 0 ,
outstanding : outstandingByReg.get ( r . id ) || 0 ,
donations : donationsByUser.get ( r . user ? . id || r . userId ) || 0 ,
fee : feeByReg.get ( r . id ) || 0 ,
__prices : { } as Record < string , number > // 👈 hidden helper
} ;
@@ -660,7 +679,7 @@ export default function ReportsV2() {
return rows ;
} , [ report , registrationsByEvent , filteredEvents , masterOptions , paidByReg , outstandingByReg ] ) ;
} , [ report , registrationsByEvent , filteredEvents , masterOptions , paidByReg , outstandingByReg , feeByReg ] ) ;
//Master Report Totals
const masterTotals = useMemo ( ( ) = > {
@@ -671,6 +690,7 @@ export default function ReportsV2() {
totalPaid : 0 ,
outstanding : 0 ,
donations : 0 ,
fee : 0 ,
} ;
// 👇 initialise dynamic option totals
@@ -683,6 +703,7 @@ export default function ReportsV2() {
totals . orderTotal += row . orderTotal ;
totals . totalPaid += row . totalPaid ;
totals . outstanding += row . outstanding ;
totals . fee += row . fee || 0 ;
masterOptions . forEach ( opt = > {
const qty = row [ opt . name ] || 0 ;
@@ -761,6 +782,8 @@ export default function ReportsV2() {
Email : r.userEmail || "" ,
Status : r.status || "" ,
TotalPaid : r.totalPaid ,
Fee : r.fee ,
NetAfterFee : r.netAfterFee ,
Outstanding : r.outstanding ,
RegistrationId : r.registrationId
} ) ) ) ;
@@ -793,7 +816,7 @@ export default function ReportsV2() {
return downloadCsv ( ` donations_ ${ new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) } ` , rows ) ;
}
if ( report === "cashup" ) {
const cols = [ "Event" , "Section" , "Detail" , "Income" , "CostsFromMethod" , "ExpectedCash " , "Actual" , "Variance" ] ;
const cols = [ "Event" , "Section" , "Detail" , "Income" , "CostsFromMethod" , "ExpectedBeforeFees" , "YocoFee" , "ExpectedAfterFees ", "Actual" , "Variance" ] ;
const rows : any [ ] = [ ] ;
Object . keys ( financialsByEvent ) . forEach ( evId = > {
const f = financialsByEvent [ evId ] ;
@@ -802,15 +825,15 @@ export default function ReportsV2() {
const title = ev ? . title || evId ;
METHOD_KEYS . forEach ( m = > {
const r = f . reconciled ? . byMethod ? . [ m ] ;
rows . push ( { Event : title , Section : "Method" , Detail : METHOD_LABEL [ m ] , Income : f.paymentsByMethod?. [ m ] || 0 , CostsFromMethod : f.costsByMethod?. [ m ] || 0 , ExpectedCash : f.expectedCashByMethod?. [ m ] || 0 , Actual : r?.actual ? ? "" , Variance : r?.variance ? ? "" } ) ;
rows . push ( { Event : title , Section : "Method" , Detail : METHOD_LABEL [ m ] , Income : f.paymentsByMethod?. [ m ] || 0 , CostsFromMethod : f.costsByMethod?. [ m ] || 0 , ExpectedBeforeFees : f.expectedCashByMethod?. [ m ] || 0 , YocoFee : f.feesByMethod?. [ m ] || 0 , ExpectedAfterFees : f.expectedInBankByMethod?. [ m ] || 0 , Actual : r?.actual ? ? "" , Variance : r?.variance ? ? "" } ) ;
if ( m === "cash" ) {
( r ? . denominations || [ ] ) . forEach ( ( d : any ) = > rows . push ( { Event : title , Section : "Denomination" , Detail : denomLabel ( d . value ) + " × " + d . count , Income : "" , CostsFromMethod : "" , ExpectedCash : "" , Actual : d.value * d . count , Variance : "" } ) ) ;
( r ? . denominations || [ ] ) . forEach ( ( d : any ) = > rows . push ( { Event : title , Section : "Denomination" , Detail : denomLabel ( d . value ) + " × " + d . count , Income : "" , CostsFromMethod : "" , ExpectedBeforeFees : "" , YocoFee : "" , ExpectedAfterFees : "" , Actual : d.value * d . count , Variance : "" } ) ) ;
}
} ) ;
if ( ( f . untaggedCostsTotal || 0 ) > 0 ) {
rows . push ( { Event : title , Section : "Untagged costs" , Detail : "Not tied to a specific method" , Income : "" , CostsFromMethod : f.untaggedCostsTotal , ExpectedCash : "" , Actual : "" , Variance : "" } ) ;
rows . push ( { Event : title , Section : "Untagged costs" , Detail : "Not tied to a specific method" , Income : "" , CostsFromMethod : f.untaggedCostsTotal , ExpectedBeforeFees : "" , YocoFee : "" , ExpectedAfterFees : "" , Actual : "" , Variance : "" } ) ;
}
rows . push ( { Event : title , Section : "Donations counted as profit" , Detail : "Unallocated donations" , Income : "" , CostsFromMethod : "" , ExpectedCash : "" , Actual : f.unallocatedDonationsTotal || 0 , Variance : "" } ) ;
rows . push ( { Event : title , Section : "Donations counted as profit" , Detail : "Unallocated donations" , Income : "" , CostsFromMethod : "" , ExpectedBeforeFees : "" , YocoFee : "" , ExpectedAfterFees : "" , Actual : f.unallocatedDonationsTotal || 0 , Variance : "" } ) ;
} ) ;
return downloadCsv ( ` cashup_ ${ new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) } ` , rows , cols . map ( c = > ( { key : c , label : c } ) ) ) ;
}
@@ -833,7 +856,8 @@ export default function ReportsV2() {
} ) ;
( f . costs || [ ] ) . forEach ( ( c : any ) = > rows . push ( { Event : title , Section : "Costs" , Detail : ` ${ c . label } ${ c . paidFromMethod ? ` (paid via ${ METHOD_LABEL [ c . paidFromMethod ] } ) ` : "" } ` , Amount : - ( c . total ? ? c . amount ) } ) ) ;
rows . push ( { Event : title , Section : "Donations counted as profit" , Detail : "Unallocated donations" , Amount : f.unallocatedDonationsTotal || 0 } ) ;
rows . push ( { Event : title , Section : "Net profit " , Detail : "" , Amount : f.netProfit || 0 } ) ;
rows . push ( { Event : title , Section : "Fees " , Detail : "Yoco processing fees" , Amount : - ( f . totalFees || 0 ) } ) ;
rows . push ( { Event : title , Section : "Net profit" , Detail : "After fees" , Amount : f.netProfitAfterFees || 0 } ) ;
} ) ;
return downloadCsv ( ` finance_report_ ${ new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) } ` , rows , cols . map ( c = > ( { key : c , label : c } ) ) ) ;
}
@@ -841,20 +865,20 @@ export default function ReportsV2() {
const rows = Object . keys ( financialsByEvent ) . map ( evId = > {
const f = financialsByEvent [ evId ] ;
const ev = filteredEvents . find ( e = > e . id === evId ) ;
return { Event : ev?.title || evId , Revenue : f?.effectiveTotalRevenue || 0 , Costs : f?.totalCosts || 0 , NetProfit : f?.netProfit || 0 } ;
return { Event : ev?.title || evId , Revenue : f?.effectiveTotalRevenue || 0 , Costs : f?.totalCosts || 0 , Fees : f?.totalFees || 0 , NetProfit : f?.netProfitAfterFees || 0 } ;
} ) ;
return downloadCsv ( ` profit_report_ ${ new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) } ` , rows ) ;
}
if ( report === "cashupAudit" ) {
const cols = [ "Event" , "Action" , "PerformedBy" , "Date" , "Method" , "Expected" , "Actual" , "Variance" , "DeltaVsPreviousClose" , "DonationsCountedAsProfit" , "Notes" ] ;
const cols = [ "Event" , "Action" , "PerformedBy" , "Date" , "Method" , "ExpectedBeforeFees" , "YocoFee" , "ExpectedAfterFees ", "Actual" , "Variance" , "DeltaVsPreviousClose" , "DonationsCountedAsProfit" , "Notes" ] ;
const rows : any [ ] = [ ] ;
auditRows . forEach ( ( r : any ) = > {
const base = { Event : r.event?.title || r . eventId , Action : actionLabel ( r . action ) , PerformedBy : r.performedBy?.name || "" , Date : new Date ( r . createdAt ) . toLocaleString ( ) } ;
if ( r . action === "reopened" ) {
rows . push ( { . . . base , Method : "" , Expected : "" , Actual : "" , Variance : "" , DeltaVsPreviousClose : "" , DonationsCountedAsProfit : "" , Notes : r.notes || "" } ) ;
rows . push ( { . . . base , Method : "" , ExpectedBeforeFees : "" , YocoFee : "" , ExpectedAfterFees : "" , Actual : "" , Variance : "" , DeltaVsPreviousClose : "" , DonationsCountedAsProfit : "" , Notes : r.notes || "" } ) ;
} else {
( r . lines || [ ] ) . forEach ( ( l : any ) = > rows . push ( {
. . . base , Method : METHOD_LABEL [ l . method ] || l . method , Expected : l.expectedAmount , Actual : l.actualAmount ? ? "" , Variance : l.variance ? ? "" ,
. . . base , Method : METHOD_LABEL [ l . method ] || l . method , ExpectedBeforeFees : ( l . expectedAmount || 0 ) + ( l . feeAmount || 0 ) , YocoFee : l.feeAmount ? ? 0 , ExpectedAfterFees : l.expectedAmount , Actual : l.actualAmount ? ? "" , Variance : l.variance ? ? "" ,
DeltaVsPreviousClose : r._deltaVsPrevious?. [ l . method ] ? ? "" , DonationsCountedAsProfit : r.unallocatedDonationsTotal || 0 , Notes : l.notes || r . notes || ""
} ) ) ;
}
@@ -874,6 +898,7 @@ export default function ReportsV2() {
OrderTotal : r.orderTotal ,
Paid : r.totalPaid ,
Fee : r.fee ,
Outstanding : r.outstanding ,
Donations : r.donations
} ) ) ;
@@ -891,6 +916,7 @@ export default function ReportsV2() {
OrderTotal : masterTotals?.orderTotal ? ? 0 ,
Paid : masterTotals?.totalPaid ? ? 0 ,
Fee : masterTotals?.fee ? ? 0 ,
Outstanding : masterTotals?.outstanding ? ? 0 ,
Donations : masterTotals?.donations ? ? 0 ,
} ) ;
@@ -1006,6 +1032,8 @@ export default function ReportsV2() {
r . userEmail || "" ,
r . status || "" ,
Number ( ( r . totalPaid || 0 ) . toFixed ( 2 ) ) ,
Number ( ( r . fee || 0 ) . toFixed ( 2 ) ) ,
Number ( ( r . netAfterFee || 0 ) . toFixed ( 2 ) ) ,
Number ( ( r . outstanding || 0 ) . toFixed ( 2 ) ) ,
r . registrationId || "" ,
] ) ;
@@ -1013,7 +1041,7 @@ export default function ReportsV2() {
title : 'Revenue Detailed' ,
kind : 'table' ,
orientation : 'landscape' ,
table : { columns : [ "Event" , "Name" , "Email" , "Status" , "Total paid" , "Outstanding" , "RegistrationId" ] , rows }
table : { columns : [ "Event" , "Name" , "Email" , "Status" , "Total paid" , "Fee" , "Net after fee" , " Outstanding", "RegistrationId" ] , rows }
} ) ;
return ;
}
@@ -1042,15 +1070,15 @@ export default function ReportsV2() {
const title = ev ? . title || evId ;
METHOD_KEYS . forEach ( m = > {
const r = f . reconciled ? . byMethod ? . [ m ] ;
rows . push ( [ title , METHOD_LABEL [ m ] , Number ( ( f . paymentsByMethod ? . [ m ] || 0 ) . toFixed ( 2 ) ) , Number ( ( f . costsByMethod ? . [ m ] || 0 ) . toFixed ( 2 ) ) , Number ( ( f . expectedCashByMethod ? . [ m ] || 0 ) . toFixed ( 2 ) ) , r ? . actual != null ? Number ( r . actual . toFixed ( 2 ) ) : "not reconciled" , r ? . variance != null ? Number ( r . variance . toFixed ( 2 ) ) : "" ] ) ;
rows . push ( [ title , METHOD_LABEL [ m ] , Number ( ( f . paymentsByMethod ? . [ m ] || 0 ) . toFixed ( 2 ) ) , Number ( ( f . costsByMethod ? . [ m ] || 0 ) . toFixed ( 2 ) ) , Number ( ( f . expectedCashByMethod ? . [ m ] || 0 ) . toFixed ( 2 ) ) , Number ( ( f . feesByMethod ? . [ m ] || 0 ) . toFixed ( 2 ) ) , Number ( ( f . expectedInBankByMethod ? . [ m ] || 0 ) . toFixed ( 2 ) ) , r ? . actual != null ? Number ( r . actual . toFixed ( 2 ) ) : "not reconciled" , r ? . variance != null ? Number ( r . variance . toFixed ( 2 ) ) : "" ] ) ;
if ( m === "cash" ) {
( r ? . denominations || [ ] ) . forEach ( ( d : any ) = > rows . push ( [ title , ` ${ denomLabel ( d . value ) } × ${ d . count } ` , "" , "" , "" , Number ( ( d . value * d . count ) . toFixed ( 2 ) ) , "" ] ) ) ;
( r ? . denominations || [ ] ) . forEach ( ( d : any ) = > rows . push ( [ title , ` ${ denomLabel ( d . value ) } × ${ d . count } ` , "" , "" , "" , "" , "" , Number ( ( d . value * d . count ) . toFixed ( 2 ) ) , "" ] ) ) ;
}
} ) ;
if ( ( f . untaggedCostsTotal || 0 ) > 0 ) rows . push ( [ title , "Untagged costs" , "" , Number ( f . untaggedCostsTotal . toFixed ( 2 ) ) , "" , "" , "" ] ) ;
rows . push ( [ title , "Donations counted as profit" , "" , "" , "" , Number ( ( f . unallocatedDonationsTotal || 0 ) . toFixed ( 2 ) ) , "" ] ) ;
if ( ( f . untaggedCostsTotal || 0 ) > 0 ) rows . push ( [ title , "Untagged costs" , "" , Number ( f . untaggedCostsTotal . toFixed ( 2 ) ) , "" , "" , "" , "" , "" ] ) ;
rows . push ( [ title , "Donations counted as profit" , "" , "" , "" , "" , "" , Number ( ( f . unallocatedDonationsTotal || 0 ) . toFixed ( 2 ) ) , "" ] ) ;
} ) ;
await downloadReportPdf ( API_BASE , token , { title : 'Cashup Report' , kind : 'table' , orientation : 'landscape' , table : { columns : [ "Event" , "Method" , "Income" , "Costs from method" , "Expected cash " , "Actual" , "Variance" ] , rows } } ) ;
await downloadReportPdf ( API_BASE , token , { title : 'Cashup Report' , kind : 'table' , orientation : 'landscape' , table : { columns : [ "Event" , "Method" , "Income" , "Costs from method" , "Expected (before fees)" , "Yoco fee" , "Expected (after fees) ", "Actual" , "Variance" ] , rows } } ) ;
return ;
}
if ( report === "financeReport" ) {
@@ -1073,7 +1101,8 @@ export default function ReportsV2() {
rows . push ( [ title , "Costs" , "" , "" ] ) ;
( f . costs || [ ] ) . forEach ( ( c : any ) = > rows . push ( [ title , "" , ` ${ c . label } ${ c . paidFromMethod ? ` (via ${ METHOD_LABEL [ c . paidFromMethod ] } ) ` : "" } ` , Number ( ( - ( c . total ? ? c . amount ) ) . toFixed ( 2 ) ) ] ) ) ;
rows . push ( [ title , "" , "Donations counted as profit" , Number ( ( f . unallocatedDonationsTotal || 0 ) . toFixed ( 2 ) ) ] ) ;
rows . push ( [ title , "" , "Net profit " , Number ( ( f . netProfit || 0 ) . toFixed ( 2 ) ) ] ) ;
rows . push ( [ title , "Fees " , "Yoco processing fees " , Number ( ( - ( f . totalFees || 0 ) ) . toFixed ( 2 ) ) ] ) ;
rows . push ( [ title , "" , "Net profit (after fees)" , Number ( ( f . netProfitAfterFees || 0 ) . toFixed ( 2 ) ) ] ) ;
} ) ;
await downloadReportPdf ( API_BASE , token , { title : 'Finance Report' , kind : 'table' , orientation : 'landscape' , table : { columns : [ "Event" , "Section" , "Detail" , "Amount" ] , rows } } ) ;
return ;
@@ -1082,9 +1111,9 @@ export default function ReportsV2() {
const rows : ( string | number ) [ ] [ ] = Object . keys ( financialsByEvent ) . map ( evId = > {
const f = financialsByEvent [ evId ] ;
const ev = filteredEvents . find ( e = > e . id === evId ) ;
return [ ev ? . title || evId , Number ( ( f ? . effectiveTotalRevenue || 0 ) . toFixed ( 2 ) ) , Number ( ( f ? . totalCosts || 0 ) . toFixed ( 2 ) ) , Number ( ( f ? . netProfit || 0 ) . toFixed ( 2 ) ) ] ;
return [ ev ? . title || evId , Number ( ( f ? . effectiveTotalRevenue || 0 ) . toFixed ( 2 ) ) , Number ( ( f ? . totalCosts || 0 ) . toFixed ( 2 ) ) , Number ( ( f ? . totalFees || 0 ) . toFixed ( 2 ) ) , Number ( ( f ? . netProfitAfterFees || 0 ) . toFixed ( 2 ) ) ] ;
} ) ;
await downloadReportPdf ( API_BASE , token , { title : 'Profit Report' , kind : 'table' , orientation : 'portrait' , table : { columns : [ "Event" , "Revenue" , "Costs" , "Net profit " ] , rows } } ) ;
await downloadReportPdf ( API_BASE , token , { title : 'Profit Report' , kind : 'table' , orientation : 'portrait' , table : { columns : [ "Event" , "Revenue" , "Costs" , "Fees" , "Net profit (after fees) "] , rows } } ) ;
return ;
}
if ( report === "cashupAudit" ) {
@@ -1092,13 +1121,13 @@ export default function ReportsV2() {
auditRows . forEach ( ( r : any ) = > {
const base = [ r . event ? . title || r . eventId , actionLabel ( r . action ) , r . performedBy ? . name || "" , new Date ( r . createdAt ) . toLocaleString ( ) ] ;
if ( r . action === "reopened" ) {
rows . push ( [ . . . base , "" , "" , "" , "" , "" , r . notes || "" ] ) ;
rows . push ( [ . . . base , "" , "" , "" , "" , "" , "" , "" , r . notes || "" ] ) ;
} else {
( r . lines || [ ] ) . forEach ( ( l : any ) = > rows . push ( [ . . . base , METHOD_LABEL [ l . method ] || l . method , Number ( ( l . expectedAmount || 0 ) . toFixed ( 2 ) ) , l . actualAmount != null ? Number ( l . actualAmount . toFixed ( 2 ) ) : "" , l . variance != null ? Number ( l . variance . toFixed ( 2 ) ) : "" , r . _deltaVsPrevious ? . [ l . method ] != null ? Number ( r . _deltaVsPrevious [ l . method ] . toFixed ( 2 ) ) : "" , l . notes || "" ] ) ) ;
if ( ! r . lines || r . lines . length === 0 ) rows . push ( [ . . . base , "" , "" , "" , "" , "" , ` Donations counted as profit: ${ money2 ( r . unallocatedDonationsTotal ) } ` ] ) ;
( r . lines || [ ] ) . forEach ( ( l : any ) = > rows . push ( [ . . . base , METHOD_LABEL [ l . method ] || l . method , Number ( ( ( l . expectedAmount || 0 ) + ( l . feeAmount || 0 ) ) . toFixed ( 2 ) ) , Number ( ( l . feeAmount || 0 ) . toFixed ( 2 ) ) , Number ( ( l . expectedAmount || 0 ) . toFixed ( 2 ) ) , l . actualAmount != null ? Number ( l . actualAmount . toFixed ( 2 ) ) : "" , l . variance != null ? Number ( l . variance . toFixed ( 2 ) ) : "" , r . _deltaVsPrevious ? . [ l . method ] != null ? Number ( r . _deltaVsPrevious [ l . method ] . toFixed ( 2 ) ) : "" , l . notes || "" ] ) ) ;
if ( ! r . lines || r . lines . length === 0 ) rows . push ( [ . . . base , "" , "" , "" , "" , "" , "" , "" , ` Donations counted as profit: ${ money2 ( r . unallocatedDonationsTotal ) } ` ] ) ;
}
} ) ;
await downloadReportPdf ( API_BASE , token , { title : 'Cashup Audit Trail' , kind : 'table' , orientation : 'landscape' , table : { columns : [ "Event" , "Action" , "Performed by" , "Date" , "Method" , "Expected" , "Actual" , "Variance" , "Δ vs previous close" , "Notes" ] , rows } } ) ;
await downloadReportPdf ( API_BASE , token , { title : 'Cashup Audit Trail' , kind : 'table' , orientation : 'landscape' , table : { columns : [ "Event" , "Action" , "Performed by" , "Date" , "Method" , "Expected (before fees)" , "Yoco fee" , "Expected (after fees) ", "Actual" , "Variance" , "Δ vs previous close" , "Notes" ] , rows } } ) ;
return ;
}
if ( report === "masterOrders" ) {
@@ -1111,6 +1140,7 @@ export default function ReportsV2() {
Number ( r . orderTotal . toFixed ( 2 ) ) ,
Number ( r . totalPaid . toFixed ( 2 ) ) ,
Number ( ( r . fee || 0 ) . toFixed ( 2 ) ) ,
Number ( r . outstanding . toFixed ( 2 ) )
] ) ;
@@ -1124,6 +1154,7 @@ export default function ReportsV2() {
Number ( ( masterTotals ? . orderTotal ? ? 0 ) . toFixed ( 2 ) ) ,
Number ( ( masterTotals ? . totalPaid ? ? 0 ) . toFixed ( 2 ) ) ,
Number ( ( masterTotals ? . fee ? ? 0 ) . toFixed ( 2 ) ) ,
Number ( ( masterTotals ? . outstanding ? ? 0 ) . toFixed ( 2 ) )
] ) ;
@@ -1137,6 +1168,7 @@ export default function ReportsV2() {
` R ${ ( masterTotals ? . [ ` ${ opt . name } _revenue ` ] ? ? 0 ) . toFixed ( 2 ) } `
) ,
"" ,
"" ,
"" ,
""
@@ -1156,6 +1188,7 @@ export default function ReportsV2() {
"Order Total" ,
"Paid" ,
"Fee" ,
"Outstanding"
] ,
rows : bodyRows
@@ -1217,8 +1250,8 @@ export default function ReportsV2() {
} ) ;
await emailReportPdf ( API_BASE , token , { title : 'Revenue Summary' , kind : 'table' , orientation : 'portrait' , table : { columns : [ "Event" , "Method" , "Amount" , "Event Total" ] , rows } , subject , body } ) ;
} else if ( report === 'revenueDetailed' ) {
const rows : ( string | number ) [ ] [ ] = ( revDetRows as any [ ] ) . map ( r = > [ r . eventTitle , r . userName , r . userEmail || "" , r . status || "" , Number ( ( r . totalPaid || 0 ) . toFixed ( 2 ) ) , Number ( ( r . outstanding || 0 ) . toFixed ( 2 ) ) , r . registrationId || "" ] ) ;
await emailReportPdf ( API_BASE , token , { title : 'Revenue Detailed' , kind : 'table' , orientation : 'landscape' , table : { columns : [ "Event" , "Name" , "Email" , "Status" , "Total paid" , "Outstanding" , "RegistrationId" ] , rows } , subject , body } ) ;
const rows : ( string | number ) [ ] [ ] = ( revDetRows as any [ ] ) . map ( r = > [ r . eventTitle , r . userName , r . userEmail || "" , r . status || "" , Number ( ( r . totalPaid || 0 ) . toFixed ( 2 ) ) , Number ( ( r . fee || 0 ) . toFixed ( 2 ) ) , Number ( ( r . netAfterFee || 0 ) . toFixed ( 2 ) ) , Number ( ( r . outstanding || 0 ) . toFixed ( 2 ) ) , r . registrationId || "" ] ) ;
await emailReportPdf ( API_BASE , token , { title : 'Revenue Detailed' , kind : 'table' , orientation : 'landscape' , table : { columns : [ "Event" , "Name" , "Email" , "Status" , "Total paid" , "Fee" , "Net after fee" , " Outstanding", "RegistrationId" ] , rows } , subject , body } ) ;
} else if ( report === 'regStatus' ) {
const rows : ( string | number ) [ ] [ ] = [ ] ;
Object . keys ( registrationsByEvent ) . forEach ( evId = > { const ev = filteredEvents . find ( e = > e . id === evId ) ; const regs = registrationsByEvent [ evId ] || [ ] ; const counts : Record < string , number > = { pending : 0 , partial_paid : 0 , paid : 0 , cancelled : 0 } ; regs . forEach ( ( r : any ) = > { if ( ! statusIncludeCancelled && r . status === 'cancelled' ) return ; counts [ r . status ] = ( counts [ r . status ] || 0 ) + 1 ; } ) ; rows . push ( [ ev ? . title || evId , counts . pending || 0 , counts . partial_paid || 0 , counts . paid || 0 , counts . cancelled || 0 ] ) ; } ) ;
@@ -1230,13 +1263,13 @@ export default function ReportsV2() {
const title = ev ? . title || evId ;
METHOD_KEYS . forEach ( m = > {
const r = f . reconciled ? . byMethod ? . [ m ] ;
rows . push ( [ title , METHOD_LABEL [ m ] , Number ( ( f . paymentsByMethod ? . [ m ] || 0 ) . toFixed ( 2 ) ) , Number ( ( f . costsByMethod ? . [ m ] || 0 ) . toFixed ( 2 ) ) , Number ( ( f . expectedCashByMethod ? . [ m ] || 0 ) . toFixed ( 2 ) ) , r ? . actual != null ? Number ( r . actual . toFixed ( 2 ) ) : "not reconciled" , r ? . variance != null ? Number ( r . variance . toFixed ( 2 ) ) : "" ] ) ;
if ( m === "cash" ) ( r ? . denominations || [ ] ) . forEach ( ( d : any ) = > rows . push ( [ title , ` ${ denomLabel ( d . value ) } × ${ d . count } ` , "" , "" , "" , Number ( ( d . value * d . count ) . toFixed ( 2 ) ) , "" ] ) ) ;
rows . push ( [ title , METHOD_LABEL [ m ] , Number ( ( f . paymentsByMethod ? . [ m ] || 0 ) . toFixed ( 2 ) ) , Number ( ( f . costsByMethod ? . [ m ] || 0 ) . toFixed ( 2 ) ) , Number ( ( f . expectedCashByMethod ? . [ m ] || 0 ) . toFixed ( 2 ) ) , Number ( ( f . feesByMethod ? . [ m ] || 0 ) . toFixed ( 2 ) ) , Number ( ( f . expectedInBankByMethod ? . [ m ] || 0 ) . toFixed ( 2 ) ) , r ? . actual != null ? Number ( r . actual . toFixed ( 2 ) ) : "not reconciled" , r ? . variance != null ? Number ( r . variance . toFixed ( 2 ) ) : "" ] ) ;
if ( m === "cash" ) ( r ? . denominations || [ ] ) . forEach ( ( d : any ) = > rows . push ( [ title , ` ${ denomLabel ( d . value ) } × ${ d . count } ` , "" , "" , "" , "" , "" , Number ( ( d . value * d . count ) . toFixed ( 2 ) ) , "" ] ) ) ;
} ) ;
if ( ( f . untaggedCostsTotal || 0 ) > 0 ) rows . push ( [ title , "Untagged costs" , "" , Number ( f . untaggedCostsTotal . toFixed ( 2 ) ) , "" , "" , "" ] ) ;
rows . push ( [ title , "Donations counted as profit" , "" , "" , "" , Number ( ( f . unallocatedDonationsTotal || 0 ) . toFixed ( 2 ) ) , "" ] ) ;
if ( ( f . untaggedCostsTotal || 0 ) > 0 ) rows . push ( [ title , "Untagged costs" , "" , Number ( f . untaggedCostsTotal . toFixed ( 2 ) ) , "" , "" , "" , "" , "" ] ) ;
rows . push ( [ title , "Donations counted as profit" , "" , "" , "" , "" , "" , Number ( ( f . unallocatedDonationsTotal || 0 ) . toFixed ( 2 ) ) , "" ] ) ;
} ) ;
await emailReportPdf ( API_BASE , token , { title : 'Cashup Report' , kind : 'table' , orientation : 'landscape' , table : { columns : [ "Event" , "Method" , "Income" , "Costs from method" , "Expected cash " , "Actual" , "Variance" ] , rows } , subject , body } ) ;
await emailReportPdf ( API_BASE , token , { title : 'Cashup Report' , kind : 'table' , orientation : 'landscape' , table : { columns : [ "Event" , "Method" , "Income" , "Costs from method" , "Expected (before fees)" , "Yoco fee" , "Expected (after fees) ", "Actual" , "Variance" ] , rows } , subject , body } ) ;
} else if ( report === 'financeReport' ) {
const rows : ( string | number ) [ ] [ ] = [ ] ;
Object . keys ( financialsByEvent ) . forEach ( evId = > {
@@ -1255,26 +1288,27 @@ export default function ReportsV2() {
rows . push ( [ title , "Costs" , "" , "" ] ) ;
( f . costs || [ ] ) . forEach ( ( c : any ) = > rows . push ( [ title , "" , ` ${ c . label } ${ c . paidFromMethod ? ` (via ${ METHOD_LABEL [ c . paidFromMethod ] } ) ` : "" } ` , Number ( ( - ( c . total ? ? c . amount ) ) . toFixed ( 2 ) ) ] ) ) ;
rows . push ( [ title , "" , "Donations counted as profit" , Number ( ( f . unallocatedDonationsTotal || 0 ) . toFixed ( 2 ) ) ] ) ;
rows . push ( [ title , "" , "Net profit " , Number ( ( f . netProfit || 0 ) . toFixed ( 2 ) ) ] ) ;
rows . push ( [ title , "Fees " , "Yoco processing fees " , Number ( ( - ( f . totalFees || 0 ) ) . toFixed ( 2 ) ) ] ) ;
rows . push ( [ title , "" , "Net profit (after fees)" , Number ( ( f . netProfitAfterFees || 0 ) . toFixed ( 2 ) ) ] ) ;
} ) ;
await emailReportPdf ( API_BASE , token , { title : 'Finance Report' , kind : 'table' , orientation : 'landscape' , table : { columns : [ "Event" , "Section" , "Detail" , "Amount" ] , rows } , subject , body } ) ;
} else if ( report === 'profitReport' ) {
const rows : ( string | number ) [ ] [ ] = Object . keys ( financialsByEvent ) . map ( evId = > {
const f = financialsByEvent [ evId ] ; const ev = filteredEvents . find ( e = > e . id === evId ) ;
return [ ev ? . title || evId , Number ( ( f ? . effectiveTotalRevenue || 0 ) . toFixed ( 2 ) ) , Number ( ( f ? . totalCosts || 0 ) . toFixed ( 2 ) ) , Number ( ( f ? . netProfit || 0 ) . toFixed ( 2 ) ) ] ;
return [ ev ? . title || evId , Number ( ( f ? . effectiveTotalRevenue || 0 ) . toFixed ( 2 ) ) , Number ( ( f ? . totalCosts || 0 ) . toFixed ( 2 ) ) , Number ( ( f ? . totalFees || 0 ) . toFixed ( 2 ) ) , Number ( ( f ? . netProfitAfterFees || 0 ) . toFixed ( 2 ) ) ] ;
} ) ;
await emailReportPdf ( API_BASE , token , { title : 'Profit Report' , kind : 'table' , orientation : 'portrait' , table : { columns : [ "Event" , "Revenue" , "Costs" , "Net profit " ] , rows } , subject , body } ) ;
await emailReportPdf ( API_BASE , token , { title : 'Profit Report' , kind : 'table' , orientation : 'portrait' , table : { columns : [ "Event" , "Revenue" , "Costs" , "Fees" , "Net profit (after fees) "] , rows } , subject , body } ) ;
} else if ( report === 'cashupAudit' ) {
const rows : ( string | number ) [ ] [ ] = [ ] ;
auditRows . forEach ( ( r : any ) = > {
const base = [ r . event ? . title || r . eventId , actionLabel ( r . action ) , r . performedBy ? . name || "" , new Date ( r . createdAt ) . toLocaleString ( ) ] ;
if ( r . action === "reopened" ) {
rows . push ( [ . . . base , "" , "" , "" , "" , "" , r . notes || "" ] ) ;
rows . push ( [ . . . base , "" , "" , "" , "" , "" , "" , "" , r . notes || "" ] ) ;
} else {
( r . lines || [ ] ) . forEach ( ( l : any ) = > rows . push ( [ . . . base , METHOD_LABEL [ l . method ] || l . method , Number ( ( l . expectedAmount || 0 ) . toFixed ( 2 ) ) , l . actualAmount != null ? Number ( l . actualAmount . toFixed ( 2 ) ) : "" , l . variance != null ? Number ( l . variance . toFixed ( 2 ) ) : "" , r . _deltaVsPrevious ? . [ l . method ] != null ? Number ( r . _deltaVsPrevious [ l . method ] . toFixed ( 2 ) ) : "" , l . notes || "" ] ) ) ;
( r . lines || [ ] ) . forEach ( ( l : any ) = > rows . push ( [ . . . base , METHOD_LABEL [ l . method ] || l . method , Number ( ( ( l . expectedAmount || 0 ) + ( l . feeAmount || 0 ) ) . toFixed ( 2 ) ) , Number ( ( l . feeAmount || 0 ) . toFixed ( 2 ) ) , Number ( ( l . expectedAmount || 0 ) . toFixed ( 2 ) ) , l . actualAmount != null ? Number ( l . actualAmount . toFixed ( 2 ) ) : "" , l . variance != null ? Number ( l . variance . toFixed ( 2 ) ) : "" , r . _deltaVsPrevious ? . [ l . method ] != null ? Number ( r . _deltaVsPrevious [ l . method ] . toFixed ( 2 ) ) : "" , l . notes || "" ] ) ) ;
}
} ) ;
await emailReportPdf ( API_BASE , token , { title : 'Cashup Audit Trail' , kind : 'table' , orientation : 'landscape' , table : { columns : [ "Event" , "Action" , "Performed by" , "Date" , "Method" , "Expected" , "Actual" , "Variance" , "Δ vs previous close" , "Notes" ] , rows } , subject , body } ) ;
await emailReportPdf ( API_BASE , token , { title : 'Cashup Audit Trail' , kind : 'table' , orientation : 'landscape' , table : { columns : [ "Event" , "Action" , "Performed by" , "Date" , "Method" , "Expected (before fees)" , "Yoco fee" , "Expected (after fees) ", "Actual" , "Variance" , "Δ vs previous close" , "Notes" ] , rows } , subject , body } ) ;
}
alert ( 'Email sent with PDF attachment' ) ;
} catch ( e : any ) {
@@ -1710,6 +1744,8 @@ export default function ReportsV2() {
< th className = "text-left" > Email < / th >
< th className = "text-left" > Status < / th >
< th className = "text-left" > Total paid < / th >
< th className = "text-left" > Fee < / th >
< th className = "text-left" > Net after fee < / th >
< th className = "text-left" > Outstanding < / th >
< th className = "text-left" > Registration < / th >
< / tr >
@@ -1721,6 +1757,8 @@ export default function ReportsV2() {
< td > { r . userEmail || '' } < / td >
< td className = "capitalize" > { r . status || '' } < / td >
< td > R { Number ( r . totalPaid || 0 ) . toFixed ( 2 ) } < / td >
< td > R { Number ( r . fee || 0 ) . toFixed ( 2 ) } < / td >
< td > R { Number ( r . netAfterFee || 0 ) . toFixed ( 2 ) } < / td >
< td > R { Number ( r . outstanding || 0 ) . toFixed ( 2 ) } < / td >
< td className = "font-mono text-xs" > { r . registrationId || '' } < / td >
< / tr >
@@ -1823,6 +1861,7 @@ export default function ReportsV2() {
< span className = "font-medium" > Totals : < / span >
< span className = "ml-3" > Order : R { masterTotals ? . orderTotal . toFixed ( 2 ) } < / span >
< span className = "ml-3" > Paid : R { masterTotals ? . totalPaid . toFixed ( 2 ) } < / span >
< span className = "ml-3" > Fee : R { ( masterTotals ? . fee ? ? 0 ) . toFixed ( 2 ) } < / span >
< span className = "ml-3" > Outstanding : R { masterTotals ? . outstanding . toFixed ( 2 ) } < / span >
< / div >
@@ -1840,6 +1879,7 @@ export default function ReportsV2() {
< th > Order Total < / th >
< th > Paid < / th >
< th > Fee < / th >
< th > Outstanding < / th >
< th > Donations < / th >
< / tr >
@@ -1859,6 +1899,7 @@ export default function ReportsV2() {
< td > R { r . orderTotal . toFixed ( 2 ) } < / td >
< td > R { r . totalPaid . toFixed ( 2 ) } < / td >
< td > R { ( r . fee || 0 ) . toFixed ( 2 ) } < / td >
< td > R { r . outstanding . toFixed ( 2 ) } < / td >
< td > R { r . donations . toFixed ( 2 ) } < / td >
< / tr >
@@ -1874,6 +1915,7 @@ export default function ReportsV2() {
< td > R { masterTotals ? . orderTotal . toFixed ( 2 ) } < / td >
< td > R { masterTotals ? . totalPaid . toFixed ( 2 ) } < / td >
< td > R { ( masterTotals ? . fee ? ? 0 ) . toFixed ( 2 ) } < / td >
< td > R { masterTotals ? . outstanding . toFixed ( 2 ) } < / td >
< td > R { masterTotals ? . donations . toFixed ( 2 ) } < / td >
< / tr >
@@ -1886,7 +1928,7 @@ export default function ReportsV2() {
< / td >
) ) }
< td colSpan = { 4 } > < / td >
< td colSpan = { 5 } > < / td >
< / tr >
< / tbody >
< / table >
@@ -1912,7 +1954,7 @@ export default function ReportsV2() {
< / div >
< div className = "overflow-auto" >
< table className = "min-w-[640px] text-sm" >
< thead > < tr > < th className = "text-left" > Method < / th > < th className = "text-left" > Income < / th > < th className = "text-left" > Costs from method < / th > < th className = "text-left" > Expected cash < / th > < th className = "text-left" > Actual < / th > < th className = "text-left" > Variance < / th > < / tr > < / thead >
< thead > < tr > < th className = "text-left" > Method < / th > < th className = "text-left" > Income < / th > < th className = "text-left" > Costs from method < / th > < th className = "text-left" > Expected ( before fees ) < / th > < th className = "text-left" > Yoco fee < / th > < th className = "text-left" > Expected ( after fees ) < / th > < th className = "text-left" > Actual < / th > < th className = "text-left" > Variance < / th > < / tr > < / thead >
< tbody >
{ METHOD_KEYS . map ( m = > {
const r = f . reconciled ? . byMethod ? . [ m ] ;
@@ -1923,13 +1965,15 @@ export default function ReportsV2() {
< td > R { ( f . paymentsByMethod ? . [ m ] || 0 ) . toFixed ( 2 ) } < / td >
< td > R { ( f . costsByMethod ? . [ m ] || 0 ) . toFixed ( 2 ) } < / td >
< td > R { ( f . expectedCashByMethod ? . [ m ] || 0 ) . toFixed ( 2 ) } < / td >
< td > R { ( f . feesByMethod ? . [ m ] || 0 ) . toFixed ( 2 ) } < / td >
< td > R { ( f . expectedInBankByMethod ? . [ m ] || 0 ) . toFixed ( 2 ) } < / td >
< td > { r ? . actual != null ? ` R ${ r . actual . toFixed ( 2 ) } ` : < span className = "text-gray-400" > not reconciled < / span > } < / td >
< td className = { r ? . variance != null && r . variance !== 0 ? ( r . variance < 0 ? "text-red-600" : "text-emerald-600" ) : "" } > { r ? . variance != null ? ` R ${ r . variance . toFixed ( 2 ) } ` : "—" } < / td >
< / tr >
{ m === "cash" && ( r ? . denominations || [ ] ) . length > 0 && ( r ? . denominations || [ ] ) . map ( ( d : any ) = > (
< tr key = { d . id } className = "text-xs text-gray-500" >
< td className = "pl-4" > { denomLabel ( d . value ) } × { d . count } < / td >
< td colSpan = { 4 } > < / td >
< td colSpan = { 6 } > < / td >
< td > R { ( d . value * d . count ) . toFixed ( 2 ) } < / td >
< / tr >
) ) }
@@ -2011,7 +2055,8 @@ export default function ReportsV2() {
< / div >
< div className = "text-sm flex items-center justify-between border-t pt-1" > < span > Donations counted as profit < / span > < span > R { ( f . unallocatedDonationsTotal || 0 ) . toFixed ( 2 ) } < / span > < / div >
< div className = "text-sm font-semibold flex items-center justify-between" > < span > Net profit < / span > < span > R { ( f . netProfit || 0 ) . toFixed ( 2 ) } < / span > < / div >
< div className = "text-sm flex items-center justify-between" > < span > Yoco processing fees < / span > < span > - R { ( f . totalFees || 0 ) . toFixed ( 2 ) } < / span > < / div >
< div className = "text-sm font-semibold flex items-center justify-between" > < span > Net profit ( after fees ) < / span > < span > R { ( f . netProfitAfterFees || 0 ) . toFixed ( 2 ) } < / span > < / div >
< / div >
) ;
} )
@@ -2024,7 +2069,7 @@ export default function ReportsV2() {
< div className = "text-sm text-gray-500" > No data available . Select events and view . < / div >
) : (
< table className = "min-w-[600px] text-sm" >
< thead > < tr > < th className = "text-left" > Event < / th > < th className = "text-left" > Revenue < / th > < th className = "text-left" > Costs < / th > < th className = "text-left" > Net profit < / th > < / tr > < / thead >
< thead > < tr > < th className = "text-left" > Event < / th > < th className = "text-left" > Revenue < / th > < th className = "text-left" > Costs < / th > < th className = "text-left" > Fees < / th > < th className = "text-left" > Net profit ( after fees ) < / th > < / tr > < / thead >
< tbody >
{ Object . keys ( financialsByEvent ) . map ( evId = > {
const f = financialsByEvent [ evId ] ;
@@ -2034,7 +2079,8 @@ export default function ReportsV2() {
< td > { ev ? . title || evId } < / td >
< td > R { ( f ? . effectiveTotalRevenue || 0 ) . toFixed ( 2 ) } < / td >
< td > R { ( f ? . totalCosts || 0 ) . toFixed ( 2 ) } < / td >
< td className = "font-medium" > R { ( f ? . netProfit || 0 ) . toFixed ( 2 ) } < / td >
< td > R { ( f ? . totalFees || 0 ) . toFixed ( 2 ) } < / td >
< td className = "font-medium" > R { ( f ? . netProfitAfterFees || 0 ) . toFixed ( 2 ) } < / td >
< / tr >
) ;
} ) }
@@ -2063,11 +2109,13 @@ export default function ReportsV2() {
{ ( r . action === "closed" || r . action === "quick_closed" ) && (
< div className = "overflow-auto mt-2" >
< table className = "min-w-[560px] text-sm" >
< thead > < tr > < th className = "text-left" > Method < / th > < th className = "text-left" > Expected < / th > < th className = "text-left" > Actual < / th > < th className = "text-left" > Variance < / th > < th className = "text-left" > Δ vs previous close < / th > < / tr > < / thead >
< thead > < tr > < th className = "text-left" > Method < / th > < th className = "text-left" > Expected ( before fees ) < / th > < th className = "text-left" > Yoco fee < / th > < th className = "text-left" > Expected ( after fees ) < / th > < th className = "text-left" > Actual < / th > < th className = "text-left" > Variance < / th > < th className = "text-left" > Δ vs previous close < / th > < / tr > < / thead >
< tbody >
{ ( r . lines && r . lines . length > 0 ? r.lines : METHOD_KEYS.map ( m = > ( { method : m , expectedAmount : null , actualAmount : null , variance : null } ) ) ) . map ( ( l : any ) = > (
{ ( r . lines && r . lines . length > 0 ? r.lines : METHOD_KEYS.map ( m = > ( { method : m , expectedAmount : null , feeAmount : null , actualAmount : null , variance : null } ) ) ) . map ( ( l : any ) = > (
< tr key = { l . method } >
< td > { METHOD_LABEL [ l . method ] || l . method } < / td >
< td > { l . expectedAmount != null ? ` R ${ ( l . expectedAmount + ( l . feeAmount || 0 ) ) . toFixed ( 2 ) } ` : "—" } < / td >
< td > { l . feeAmount != null ? ` R ${ l . feeAmount . toFixed ( 2 ) } ` : "—" } < / td >
< td > { l . expectedAmount != null ? ` R ${ l . expectedAmount . toFixed ( 2 ) } ` : "—" } < / td >
< td > { l . actualAmount != null ? ` R ${ l . actualAmount . toFixed ( 2 ) } ` : < span className = "text-gray-400" > not reconciled < / span > } < / td >
< td > { l . variance != null ? ` R ${ l . variance . toFixed ( 2 ) } ` : "—" } < / td >