<?php
declare(strict_types=1);

// Keep fatal startup failures actionable in production while the detailed
// error remains in cPanel's PHP error log. This boundary is registered before
// any application dependency is loaded.
ob_start();
register_shutdown_function(static function (): void {
    $error = error_get_last();
    if (!$error || !in_array((int)$error['type'], [E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR,E_USER_ERROR], true)) return;
    $incident = strtoupper(substr(hash('sha256',(string)$error['message'].'|'.(string)$error['file'].'|'.(string)$error['line']),0,10));
    error_log('Soda ZW Finance Centre '.$incident.': '.$error['message'].' in '.$error['file'].':'.$error['line']);
    while (ob_get_level() > 0) ob_end_clean();
    if (!headers_sent()) {
        http_response_code(500);
        header('Content-Type: text/html; charset=utf-8');
    }
    echo '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Finance Centre recovery</title><style>body{margin:0;background:#f1f6fa;color:#082d4d;font:16px/1.55 system-ui,sans-serif}.box{max-width:680px;margin:10vh auto;padding:36px;background:#fff;border:1px solid #d7e2ea;border-top:6px solid #ff5a16;border-radius:20px;box-shadow:0 20px 60px #082d4d18}h1{margin:.2em 0}a{display:inline-block;margin-top:14px;padding:12px 18px;border-radius:10px;background:#082d4d;color:#fff;text-decoration:none;font-weight:700}code{color:#c2410c}</style></head><body><main class="box"><small>SODA ZW BILLING</small><h1>Finance Centre needs a controlled repair</h1><p>The page stopped before it could finish loading. No finance record was changed.</p><p>Incident code: <code>'.htmlspecialchars($incident,ENT_QUOTES,'UTF-8').'</code></p><a href="upgrade.php?force=1">Open Finance Centre repair</a></main></body></html>';
});
require_once __DIR__.'/app/bootstrap.php';
require_auth();

// Validate the complete Finance Centre dependency set instead of trusting a
// migration marker left by an interrupted cPanel upgrade.
$financeSchemaGaps = billing_finance_schema_gaps();
if ($financeSchemaGaps) {
    if ((user()['role'] ?? '') === 'owner') {
        $_SESSION['finance_boot_error'] = 'Finance Centre preflight found incomplete database components: '.implode(', ',array_slice($financeSchemaGaps,0,8));
        redirect('upgrade.php?return=finance');
    }
    http_response_code(503);
    header('Content-Type: text/plain; charset=utf-8');
    exit('Finance Centre is being configured. Ask the account owner to complete the controlled database upgrade.');
}

require_current_schema();
require_once __DIR__.'/app/layout.php';

$views=['overview','credits','reconciliation','reminders','receiving','access','health','reports'];
$view=in_array($_GET['view']??'overview',$views,true)?(string)$_GET['view']:'overview';
$viewPermissions=['credits'=>'credits.manage','reconciliation'=>'reconciliation.manage','reminders'=>'reminders.manage','receiving'=>'stock.receive','access'=>'users.manage','health'=>'backups.manage','reports'=>'reports.view'];
if(isset($viewPermissions[$view]))require_permission($viewPermissions[$view]);

if(($_GET['download']??'')==='reconciliation-template'){
    require_permission('reconciliation.manage');header('Content-Type: text/csv; charset=utf-8');header('Content-Disposition: attachment; filename="sodazw-reconciliation-template.csv"');$out=fopen('php://output','w');fputcsv($out,['transaction_date','direction','amount','description','reference']);fputcsv($out,[date('Y-m-d'),'in','35.00','EcoCash payment example','DELETE-THIS-ROW']);fclose($out);exit;
}

if(($_GET['download']??'')==='management-report'){
    require_permission('reports.view');
    $currency=(string)(settings()['currency_code']??'USD');$from=(string)($_GET['from']??date('Y-01-01'));$to=(string)($_GET['to']??date('Y-m-d'));
    $summary=management_report_summary($currency,$from,$to);
    header('Content-Type: text/csv; charset=utf-8');
    header('Content-Disposition: attachment; filename="sodazw-management-report-'.$from.'-to-'.$to.'.csv"');
    $out=fopen('php://output','w');fputcsv($out,['Soda ZW management report',$from.' to '.$to,$currency]);fputcsv($out,[]);fputcsv($out,['Metric','Amount','Currency']);
    $labels=['gross_sales'=>'Gross invoiced sales','credits'=>'Credit notes','net_sales'=>'Net invoiced sales','tax_invoiced'=>'Tax invoiced','net_sales_ex_tax'=>'Net sales excluding tax','cash_received'=>'Cash received','refunds'=>'Refunds and reversals','net_cash'=>'Net cash received','expenses'=>'Recorded operating expenses','gross_product_margin_before_credits'=>'Gross product margin before credits','stock_value'=>'Current stock valuation','operating_contribution'=>'Operating contribution'];
    foreach($labels as $key=>$label)fputcsv($out,[$label,number_format((float)$summary[$key],2,'.',''),$currency]);fclose($out);exit;
}

if(($_SERVER['REQUEST_METHOD']??'GET')==='POST'){
    verify_csrf();$action=(string)($_POST['finance_action']??'');$returnView=(string)($_POST['return_view']??$view);
    try{
        if(!billing_schema_is_current())throw new RuntimeException('The owner must run the controlled database upgrade before finance records can be changed.');
        if($action==='create_credit_note'){
            require_permission('credits.manage');$invoiceId=(int)($_POST['invoice_id']??0);$amount=max(0,decimal($_POST['amount']??0));$reason=trim((string)($_POST['reason']??''));if(!$invoiceId||$amount<=0||$reason==='')throw new RuntimeException('Select an invoice, enter the credit amount and explain the reason.');
            db()->beginTransaction();$stmt=db()->prepare('SELECT d.*,c.name customer FROM documents d JOIN customers c ON c.id=d.customer_id WHERE d.id=? AND d.type="invoice" AND d.status NOT IN ("draft","cancelled") FOR UPDATE');$stmt->execute([$invoiceId]);$invoice=$stmt->fetch();if(!$invoice)throw new RuntimeException('The invoice cannot be credited.');$financials=invoice_financials($invoiceId);$creditable=max(0,(float)$invoice['total']-(float)$financials['credited']);if($amount>$creditable+.009)throw new RuntimeException('The credit cannot exceed the remaining uncredited invoice value.');
            $creditTax=(float)$invoice['total']>0?round($amount*((float)$invoice['tax_total']/(float)$invoice['total']),2):0.0;$creditSubtotal=round($amount-$creditTax,2);$creditTaxRate=$creditSubtotal>0?round(($creditTax/$creditSubtotal)*100,4):0.0;
            $number=reserve_unique_number('credit_note');$token=bin2hex(random_bytes(32));$insert=db()->prepare('INSERT INTO documents (type,number,customer_id,source_document_id,issue_date,due_date,status,sent_at,currency_code,subtotal,discount_type,discount_value,discount_total,tax_total,delivery_fee,total,notes,terms,order_source,fulfilment_method,share_token,share_enabled,created_by) VALUES ("credit_note",?,?,?,?,?,"sent",NOW(),?,? ,"fixed",0,0,?,0,?,?,?,?,?,1,?)');
            $insert->execute([$number,$invoice['customer_id'],$invoiceId,date('Y-m-d'),date('Y-m-d'),$invoice['currency_code'],$creditSubtotal,$creditTax,$amount,$reason,'Credit issued against '.$invoice['number'],'Accounts','Pickup',$token,user()['id']]);$creditId=(int)db()->lastInsertId();db()->prepare('INSERT INTO document_items (document_id,description,quantity,unit_price,unit_cost,tax_rate,line_subtotal,line_tax,line_total,sort_order) VALUES (?, ?,1,?,0,?,?,?,?,0)')->execute([$creditId,$reason,$creditSubtotal,$creditTaxRate,$creditSubtotal,$creditTax,$amount]);db()->prepare('INSERT INTO credit_note_applications (credit_note_id,invoice_id,amount,reason,created_by) VALUES (?,?,?,?,?)')->execute([$creditId,$invoiceId,$amount,$reason,user()['id']]);
            if(isset($_POST['restore_stock'])){if(abs($amount-(float)$invoice['total'])>.009)throw new RuntimeException('Stock can only be restored automatically for a full-invoice credit.');if(empty($invoice['stock_processed_at']))throw new RuntimeException('Stock was never deducted for this invoice, so it must not be added back. Issue the credit without selecting Restore stock.');$items=db()->prepare('SELECT product_id,quantity FROM document_items WHERE document_id=? AND product_id IS NOT NULL');$items->execute([$invoiceId]);foreach($items as $item){$product=db()->prepare('SELECT track_stock FROM products WHERE id=? FOR UPDATE');$product->execute([$item['product_id']]);if((int)$product->fetchColumn()===1){db()->prepare('UPDATE products SET stock_quantity=stock_quantity+? WHERE id=?')->execute([$item['quantity'],$item['product_id']]);db()->prepare('INSERT INTO inventory_movements (product_id,movement_type,quantity_change,reference_type,reference_id,notes,created_by) VALUES (? ,"return",?,"credit_note",?,"Stock restored by full credit note",?)')->execute([$item['product_id'],$item['quantity'],$creditId,user()['id']]);}}}
            db()->prepare('INSERT INTO document_events (document_id,event_type,from_status,to_status,notes,created_by) VALUES (?,"created",NULL,"sent",?,?)')->execute([$creditId,'Credit note created for '.$invoice['number'],user()['id']]);update_invoice_status($invoiceId);db()->commit();audit('credit_note_created','credit_note',$creditId,$number);flash('success','Credit note '.$number.' was issued and applied to '.$invoice['number'].'.');
        }elseif($action==='reverse_payment'){
            require_permission('credits.manage');$paymentId=(int)($_POST['payment_id']??0);$amount=max(0,decimal($_POST['amount']??0));$reason=trim((string)($_POST['reason']??''));if(!$paymentId||$amount<=0||$reason==='')throw new RuntimeException('Select a payment, enter the refund amount and reason.');db()->beginTransaction();$stmt=db()->prepare('SELECT p.*,d.number FROM payments p JOIN documents d ON d.id=p.invoice_id WHERE p.id=? FOR UPDATE');$stmt->execute([$paymentId]);$payment=$stmt->fetch();if(!$payment)throw new RuntimeException('Payment not found.');$used=db()->prepare('SELECT COALESCE(SUM(amount),0) FROM payment_reversals WHERE payment_id=?');$used->execute([$paymentId]);$available=(float)$payment['amount']-(float)$used->fetchColumn();if($amount>$available+.009)throw new RuntimeException('The refund cannot exceed the unreversed payment amount.');db()->prepare('INSERT INTO payment_reversals (payment_id,reversal_date,amount,reason,reference,created_by) VALUES (?,?,?,?,?,?)')->execute([$paymentId,$_POST['reversal_date']??date('Y-m-d'),$amount,$reason,trim((string)($_POST['reference']??'')),user()['id']]);update_invoice_status((int)$payment['invoice_id']);db()->commit();audit('payment_reversed','payment',$paymentId,$reason);flash('success','The refund or payment reversal was recorded.');
        }elseif($action==='import_reconciliation'){
            require_permission('reconciliation.manage');$accountId=(int)($_POST['account_id']??0);if(!$accountId||empty($_FILES['statement_csv']['tmp_name']))throw new RuntimeException('Choose the payment account and CSV statement.');$handle=fopen($_FILES['statement_csv']['tmp_name'],'r');$header=array_map(fn($v)=>strtolower(trim((string)$v)),fgetcsv($handle)?:[]);$required=['transaction_date','direction','amount','description','reference'];foreach($required as $column)if(!in_array($column,$header,true))throw new RuntimeException('Use the Soda ZW reconciliation template without changing its headings.');$insert=db()->prepare('INSERT INTO reconciliation_entries (account_id,transaction_date,direction,amount,description,reference,source,created_by) VALUES (?,?,?,?,?,? ,"csv",?)');$count=0;while(($values=fgetcsv($handle))!==false&&$count<5000){$row=[];foreach($header as $i=>$key)$row[$key]=trim((string)($values[$i]??''));if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$row['transaction_date']??'')||!in_array($row['direction']??'', ['in','out'],true)||!is_numeric($row['amount']??null)||($row['description']??'')==='')continue;$insert->execute([$accountId,$row['transaction_date'],$row['direction'],abs((float)$row['amount']),substr($row['description'],0,255),substr($row['reference']??'',0,120),user()['id']]);$count++;}fclose($handle);audit('statement_imported','reconciliation',null,$count.' entries');flash('success',$count.' statement transactions were imported for matching.');
        }elseif($action==='add_reconciliation'){
            require_permission('reconciliation.manage');$accountId=(int)($_POST['account_id']??0);$amount=max(0,decimal($_POST['amount']??0));$description=trim((string)($_POST['description']??''));if(!$accountId||$amount<=0||$description==='')throw new RuntimeException('Enter the account, amount and transaction description.');db()->prepare('INSERT INTO reconciliation_entries (account_id,transaction_date,direction,amount,description,reference,source,created_by) VALUES (?,?,?,?,?,? ,"manual",?)')->execute([$accountId,$_POST['transaction_date']??date('Y-m-d'),($_POST['direction']??'in')==='out'?'out':'in',$amount,$description,trim((string)($_POST['reference']??'')),user()['id']]);flash('success','Transaction added to the reconciliation workspace.');
        }elseif($action==='match_reconciliation'){
            require_permission('reconciliation.manage');$entryId=(int)($_POST['entry_id']??0);$matchType=(string)($_POST['match_type']??'');$matchId=(int)($_POST['match_id']??0);if(!$entryId||!in_array($matchType,['payment','bill_payment','expense','ignored'],true))throw new RuntimeException('Choose a valid matching record.');$columns=['payment'=>'matched_payment_id','bill_payment'=>'matched_bill_payment_id','expense'=>'matched_expense_id'];$status=$matchType==='ignored'?'ignored':'matched';$sql='UPDATE reconciliation_entries SET status=?,matched_payment_id=NULL,matched_bill_payment_id=NULL,matched_expense_id=NULL'.($matchType!=='ignored'?','.$columns[$matchType].'=?':'').' WHERE id=? AND status="unmatched"';$params=$matchType==='ignored'?[$status,$entryId]:[$status,$matchId,$entryId];db()->prepare($sql)->execute($params);flash('success',$status==='ignored'?'Transaction marked as reviewed.':'Transaction matched successfully.');
        }elseif($action==='queue_reminder'){
            require_permission('reminders.manage');$id=queue_invoice_reminder((int)($_POST['invoice_id']??0),(string)($_POST['channel']??'manual'),user()['id']);audit('invoice_reminder_queued','reminder',$id);flash('success','The customer reminder is ready to send.');
        }elseif($action==='mark_reminder_sent'){
            require_permission('reminders.manage');$id=(int)($_POST['reminder_id']??0);db()->prepare('UPDATE reminder_log SET status="sent",sent_at=NOW() WHERE id=?')->execute([$id]);flash('success','Reminder marked as sent.');
        }elseif($action==='receive_stock'){
            require_permission('stock.receive');$supplierId=(int)($_POST['supplier_id']??0)?:null;$reference=trim((string)($_POST['supplier_reference']??''));$items=is_array($_POST['items']??null)?$_POST['items']:[];$clean=[];$total=0.0;foreach($items as $item){$productId=(int)($item['product_id']??0);$qty=max(0,decimal($item['quantity']??0));$cost=max(0,decimal($item['unit_cost']??0));if($productId&&$qty>0){$clean[]=[$productId,$qty,$cost];$total+=round($qty*$cost,2);}}if(!$clean)throw new RuntimeException('Add at least one product quantity received.');db()->beginTransaction();$receipt='GRN-'.date('Ymd').'-'.strtoupper(bin2hex(random_bytes(3)));$billId=null;if(isset($_POST['create_bill'])&&$supplierId&&$total>0){$billNumber=$reference?:$receipt;db()->prepare('INSERT INTO bills (supplier_id,number,bill_date,due_date,description,currency_code,total,status,reference,notes,created_by) VALUES (?,?,?,?,? ,"USD",?,"unpaid",?,?,?)')->execute([$supplierId,$billNumber,$_POST['receipt_date']??date('Y-m-d'),$_POST['due_date']??date('Y-m-d',strtotime('+14 days')),'Stock received '.$receipt,$total,$reference,'Created automatically from goods receipt',user()['id']]);$billId=(int)db()->lastInsertId();}db()->prepare('INSERT INTO stock_receipts (receipt_number,supplier_id,bill_id,receipt_date,supplier_reference,total_cost,notes,created_by) VALUES (?,?,?,?,?,?,?,?)')->execute([$receipt,$supplierId,$billId,$_POST['receipt_date']??date('Y-m-d'),$reference,$total,trim((string)($_POST['notes']??'')),user()['id']]);$receiptId=(int)db()->lastInsertId();foreach($clean as [$productId,$qty,$cost]){$p=db()->prepare('SELECT stock_quantity,cost_price FROM products WHERE id=? FOR UPDATE');$p->execute([$productId]);$product=$p->fetch();if(!$product)throw new RuntimeException('One of the selected products no longer exists.');$oldQty=(float)$product['stock_quantity'];$newQty=$oldQty+$qty;$weighted=$newQty>0?round((($oldQty*(float)$product['cost_price'])+($qty*$cost))/$newQty,2):$cost;db()->prepare('UPDATE products SET stock_quantity=?,cost_price=?,track_stock=1 WHERE id=?')->execute([$newQty,$weighted,$productId]);db()->prepare('INSERT INTO stock_receipt_items (stock_receipt_id,product_id,quantity,unit_cost,line_total) VALUES (?,?,?,?,?)')->execute([$receiptId,$productId,$qty,$cost,round($qty*$cost,2)]);db()->prepare('INSERT INTO inventory_movements (product_id,movement_type,quantity_change,reference_type,reference_id,notes,created_by) VALUES (? ,"purchase",?,"stock_receipt",?,?,?)')->execute([$productId,$qty,$receiptId,$receipt,user()['id']]);}db()->commit();audit('stock_received','stock_receipt',$receiptId,$receipt);flash('success','Goods receipt '.$receipt.' posted. Stock quantities and costs are updated.');
        }elseif($action==='save_permissions'){
            require_permission('users.manage');$target=(int)($_POST['user_id']??0);if(!$target)throw new RuntimeException('Choose a team member.');$keys=array_keys(permission_defaults('owner'));db()->beginTransaction();$save=db()->prepare('INSERT INTO user_permissions (user_id,permission_key,allowed,updated_by) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE allowed=VALUES(allowed),updated_by=VALUES(updated_by)');foreach($keys as $key)$save->execute([$target,$key,isset($_POST['permissions'][$key])?1:0,user()['id']]);db()->prepare('UPDATE users SET session_version=session_version+1 WHERE id=?')->execute([$target]);db()->commit();audit('permissions_updated','user',$target);flash('success','Team permissions updated. Existing sessions for this user were revoked.');
        }elseif($action==='save_automation'){
            require_permission('settings.manage');$template=trim((string)($_POST['reminder_template_name']??''));if($template!==''&&!preg_match('/^[a-z0-9_]+$/',$template))throw new RuntimeException('The WhatsApp template name may only use lowercase letters, numbers and underscores.');db()->prepare('UPDATE settings SET auto_invoice_web_orders=?,auto_reminders=?,reminder_days_before=?,reminder_days_overdue=?,reminder_template_name=? WHERE id=1')->execute([isset($_POST['auto_invoice_web_orders'])?1:0,isset($_POST['auto_reminders'])?1:0,max(0,(int)($_POST['reminder_days_before']??2)),max(1,(int)($_POST['reminder_days_overdue']??3)),$template?:null]);settings(true);flash('success','Automation settings saved.');
        }else throw new RuntimeException('Unknown finance action.');
        redirect('finance-control.php?view='.urlencode($returnView));
    }catch(Throwable $e){if(db()->inTransaction())db()->rollBack();flash('error',$e->getMessage());redirect('finance-control.php?view='.urlencode($returnView));}
}

$financeBootError = null;
try {
$baseCurrency=settings()['currency_code']??'USD';$balanceSql=finance_document_balance_sql('d');
$openInvoices=db()->query('SELECT d.id,d.number,d.issue_date,d.due_date,d.total,d.currency_code,c.name customer,'.$balanceSql.' balance FROM documents d JOIN customers c ON c.id=d.customer_id WHERE d.type="invoice" AND d.status NOT IN ("draft","cancelled") HAVING balance>.009 ORDER BY d.due_date,d.id')->fetchAll();$creditableInvoices=db()->query('SELECT d.id,d.number,d.total,d.currency_code,c.name customer,GREATEST(d.total-COALESCE((SELECT SUM(ca.amount) FROM credit_note_applications ca WHERE ca.invoice_id=d.id),0),0) creditable FROM documents d JOIN customers c ON c.id=d.customer_id WHERE d.type="invoice" AND d.status NOT IN ("draft","cancelled") HAVING creditable>.009 ORDER BY d.issue_date DESC,d.id DESC')->fetchAll();
$credits=db()->query('SELECT cn.id,cn.number,cn.issue_date,cn.total,cn.currency_code,i.number invoice,c.name customer FROM credit_note_applications ca JOIN documents cn ON cn.id=ca.credit_note_id JOIN documents i ON i.id=ca.invoice_id JOIN customers c ON c.id=cn.customer_id ORDER BY cn.id DESC LIMIT 50')->fetchAll();
$payments=db()->query('SELECT p.id,p.payment_date,p.amount,p.method,p.reference,d.id invoice_id,d.number,c.name customer,COALESCE((SELECT SUM(r.amount) FROM payment_reversals r WHERE r.payment_id=p.id),0) reversed FROM payments p JOIN documents d ON d.id=p.invoice_id JOIN customers c ON c.id=d.customer_id ORDER BY p.id DESC LIMIT 100')->fetchAll();
$accounts=db()->query('SELECT * FROM payment_accounts WHERE active=1 ORDER BY currency_code,name')->fetchAll();$suppliers=db()->query('SELECT id,name FROM suppliers WHERE active=1 ORDER BY name')->fetchAll();$products=db()->query('SELECT id,sku,name,category,stock_quantity,cost_price,unit_price FROM products WHERE active=1 ORDER BY category,name')->fetchAll();$users=db()->query('SELECT id,name,email,role,active FROM users ORDER BY FIELD(role,"owner","admin","staff"),name')->fetchAll();
$unmatched=(int)db()->query('SELECT COUNT(*) FROM reconciliation_entries WHERE status="unmatched"')->fetchColumn();$queuedReminders=(int)db()->query('SELECT COUNT(*) FROM reminder_log WHERE status="queued"')->fetchColumn();$lowStock=(int)db()->query('SELECT COUNT(*) FROM products WHERE active=1 AND track_stock=1 AND stock_quantity<=reorder_level')->fetchColumn();$lastBackup=db()->query('SELECT created_at FROM backup_events WHERE status="completed" ORDER BY id DESC LIMIT 1')->fetchColumn();
} catch (Throwable $financeBootError) {
    if ((user()['role'] ?? '') === 'owner') {
        $_SESSION['finance_boot_error'] = 'Finance Centre startup query failed: '.$financeBootError->getMessage();
        redirect('upgrade.php?return=finance&force=1');
    }
    http_response_code(503);
    header('Content-Type: text/plain; charset=utf-8');
    exit('Finance Centre is temporarily unavailable while the owner completes a database repair.');
}
$tabs=['overview'=>'Overview','credits'=>'Credits & refunds','reconciliation'=>'Reconciliation','reminders'=>'Reminders','receiving'=>'Receive stock','access'=>'Access','health'=>'System health','reports'=>'Management reports'];
render_header('Finance control centre','finance');
?>
<div class="finance-hero"><div><span class="eyebrow">FINANCE OPERATIONS</span><h2>One place to control money, stock and risk</h2><p>Review exceptions, complete sensitive accounting tasks and keep Soda ZW records audit-ready.</p></div><div class="finance-hero-score"><small>Control health</small><strong><?=($unmatched+$queuedReminders+$lowStock)===0?'Clear':'Needs review'?></strong><span><?=$unmatched+$queuedReminders+$lowStock?> open items</span></div></div>
<nav class="finance-tabs" aria-label="Finance control sections"><?php foreach($tabs as $key=>$label):?><?php if(isset($viewPermissions[$key])&&!has_permission($viewPermissions[$key]))continue;?><a class="<?=$view===$key?'active':''?>" href="finance-control.php?view=<?=$key?>"><?=e($label)?></a><?php endforeach;?></nav>

<?php if($view==='overview'):?>
<section class="finance-kpis"><article><span>Open receivables</span><strong><?=money(array_sum(array_column($openInvoices,'balance')),$baseCurrency)?></strong><small><?=count($openInvoices)?> customer invoices</small></article><article><span>Unmatched transactions</span><strong><?=$unmatched?></strong><small>Bank, cash and mobile money</small></article><article><span>Reminder queue</span><strong><?=$queuedReminders?></strong><small>Ready for customer follow-up</small></article><article><span>Low stock</span><strong><?=$lowStock?></strong><small>At or below reorder level</small></article></section>
<section class="finance-grid"><article class="panel"><div class="panel-head"><div><h2>Priority controls</h2><p>Complete these before the next close.</p></div></div><div class="control-list"><a href="finance-control.php?view=reconciliation"><b>Reconcile incoming and outgoing money</b><span><?=$unmatched?> unmatched transactions</span></a><a href="finance-control.php?view=reminders"><b>Follow up outstanding customers</b><span><?=$queuedReminders?> reminders queued</span></a><a href="finance-control.php?view=receiving"><b>Receive supplier stock correctly</b><span>Updates quantity and weighted cost</span></a><a href="finance-control.php?view=health"><b>Protect the accounting database</b><span><?=e($lastBackup?'Last backup '.date('j M Y, H:i',strtotime($lastBackup)):'No verified backup yet')?></span></a></div></article><article class="panel"><div class="panel-head"><div><h2>Automation</h2><p>Owner-controlled defaults.</p></div></div><form method="post" class="finance-form"><?=csrf_field()?><input type="hidden" name="finance_action" value="save_automation"><input type="hidden" name="return_view" value="overview"><label class="switch-row"><input type="checkbox" name="auto_invoice_web_orders" value="1" <?=settings()['auto_invoice_web_orders']?'checked':''?>><span><b>Create a draft invoice for every website order</b><small>Delivery charges stay editable before issue.</small></span></label><label class="switch-row"><input type="checkbox" name="auto_reminders" value="1" <?=settings()['auto_reminders']?'checked':''?>><span><b>Prepare automatic payment reminders</b><small>Daily maintenance creates the queue.</small></span></label><div class="form-grid"><label>Days before due<input type="number" min="0" name="reminder_days_before" value="<?=e((string)settings()['reminder_days_before'])?>"></label><label>Days after due<input type="number" min="1" name="reminder_days_overdue" value="<?=e((string)settings()['reminder_days_overdue'])?>"></label><label class="full">Approved WhatsApp template<input name="reminder_template_name" value="<?=e((string)settings()['reminder_template_name'])?>" placeholder="invoice_payment_reminder"></label></div><button class="button primary">Save automation</button></form></article></section>

<?php elseif($view==='credits'):?>
<section class="finance-grid"><article class="panel"><div class="panel-head"><div><h2>Issue a credit note</h2><p>Reduce any issued invoice, including a fully paid invoice, without deleting its history.</p></div></div><form method="post" class="finance-form"><?=csrf_field()?><input type="hidden" name="finance_action" value="create_credit_note"><input type="hidden" name="return_view" value="credits"><label>Invoice<select name="invoice_id" required><option value="">Select invoice</option><?php foreach($creditableInvoices as $invoice):?><option value="<?=$invoice['id']?>"><?=e($invoice['number'].' · '.$invoice['customer'].' · up to '.money($invoice['creditable'],$invoice['currency_code']))?></option><?php endforeach;?></select></label><label>Credit amount<input type="number" step=".01" min=".01" name="amount" required></label><label>Reason<textarea name="reason" rows="3" required placeholder="Returned item, pricing correction, goodwill..."></textarea></label><label class="check"><input type="checkbox" name="restore_stock" value="1"> Restore stock for a full-invoice return</label><button class="button primary">Issue credit note</button></form></article><article class="panel"><div class="panel-head"><div><h2>Record a refund or reversal</h2><p>Preserves the original receipt and creates a traceable reversal.</p></div></div><form method="post" class="finance-form"><?=csrf_field()?><input type="hidden" name="finance_action" value="reverse_payment"><input type="hidden" name="return_view" value="credits"><label>Original payment<select name="payment_id" required><option value="">Select payment</option><?php foreach($payments as $payment):?><?php $available=(float)$payment['amount']-(float)$payment['reversed'];if($available>.009):?><option value="<?=$payment['id']?>"><?=e($payment['number'].' · '.$payment['customer'].' · '.money($available))?></option><?php endif;?><?php endforeach;?></select></label><label>Refund date<input type="date" name="reversal_date" value="<?=date('Y-m-d')?>" required></label><label>Amount<input type="number" step=".01" min=".01" name="amount" required></label><label>Reference<input name="reference" placeholder="EcoCash, bank or cash reference"></label><label>Reason<textarea name="reason" rows="3" required></textarea></label><button class="button danger">Record refund / reversal</button></form></article></section>
<section class="panel"><div class="panel-head"><div><h2>Recent credit notes</h2><p>Every credit remains linked to its original invoice.</p></div></div><div class="table-wrap"><table><thead><tr><th>Credit note</th><th>Date</th><th>Customer</th><th>Invoice</th><th>Amount</th><th></th></tr></thead><tbody><?php foreach($credits as $credit):?><tr><td><strong><?=e($credit['number'])?></strong></td><td><?=e($credit['issue_date'])?></td><td><?=e($credit['customer'])?></td><td><?=e($credit['invoice'])?></td><td><?=money($credit['total'],$credit['currency_code'])?></td><td><a class="button secondary small" href="index.php?page=document-view&id=<?=$credit['id']?>">Open</a></td></tr><?php endforeach;?><?php if(!$credits):?><tr><td colspan="6" class="empty">No credit notes issued yet.</td></tr><?php endif;?></tbody></table></div></section>

<?php elseif($view==='reconciliation'):?>
<?php $entries=db()->query('SELECT r.*,a.name account,a.currency_code FROM reconciliation_entries r JOIN payment_accounts a ON a.id=r.account_id ORDER BY FIELD(r.status,"unmatched","matched","ignored"),r.transaction_date DESC,r.id DESC LIMIT 250')->fetchAll();$candidatePayments=db()->query('SELECT p.id,p.payment_date,p.amount,p.reference,d.number,c.name customer FROM payments p JOIN documents d ON d.id=p.invoice_id JOIN customers c ON c.id=d.customer_id ORDER BY p.payment_date DESC,p.id DESC LIMIT 200')->fetchAll();$billPayments=db()->query('SELECT bp.id,bp.payment_date,bp.amount,bp.reference,b.number,s.name supplier FROM bill_payments bp JOIN bills b ON b.id=bp.bill_id JOIN suppliers s ON s.id=b.supplier_id ORDER BY bp.payment_date DESC,bp.id DESC LIMIT 200')->fetchAll();$expenses=db()->query('SELECT id,expense_date,amount,description,reference FROM expenses ORDER BY expense_date DESC,id DESC LIMIT 200')->fetchAll();?>
<section class="finance-grid"><article class="panel"><div class="panel-head"><div><h2>Import a statement</h2><p>Works with bank, EcoCash and other payment exports.</p></div><a class="button secondary small" href="finance-control.php?view=reconciliation&download=reconciliation-template">CSV template</a></div><form method="post" enctype="multipart/form-data" class="finance-form"><?=csrf_field()?><input type="hidden" name="finance_action" value="import_reconciliation"><input type="hidden" name="return_view" value="reconciliation"><label>Account<select name="account_id" required><option value="">Select account</option><?php foreach($accounts as $account):?><option value="<?=$account['id']?>"><?=e($account['name'].' · '.$account['currency_code'])?></option><?php endforeach;?></select></label><label>Statement CSV<input type="file" name="statement_csv" accept=".csv,text/csv" required></label><button class="button primary">Import statement</button></form></article><article class="panel"><div class="panel-head"><div><h2>Add one transaction</h2><p>For cash counts or missing entries.</p></div></div><form method="post" class="finance-form"><?=csrf_field()?><input type="hidden" name="finance_action" value="add_reconciliation"><input type="hidden" name="return_view" value="reconciliation"><div class="form-grid"><label>Account<select name="account_id" required><option value="">Select</option><?php foreach($accounts as $account):?><option value="<?=$account['id']?>"><?=e($account['name'])?></option><?php endforeach;?></select></label><label>Date<input type="date" name="transaction_date" value="<?=date('Y-m-d')?>" required></label><label>Direction<select name="direction"><option value="in">Money in</option><option value="out">Money out</option></select></label><label>Amount<input type="number" step=".01" min=".01" name="amount" required></label><label class="full">Description<input name="description" required></label><label class="full">Reference<input name="reference"></label></div><button class="button secondary">Add transaction</button></form></article></section>
<section class="panel"><div class="panel-head"><div><h2>Matching workspace</h2><p>Connect each statement line to the record already in Soda ZW.</p></div><span class="control-count"><?=$unmatched?> unmatched</span></div><div class="reconciliation-list"><?php foreach($entries as $entry):?><article class="reconciliation-row <?=$entry['status']?>"><div><span><?=e($entry['transaction_date'])?> · <?=e($entry['account'])?></span><strong><?=e($entry['description'])?></strong><small><?=e($entry['reference']?:'No reference')?> · <?=e(ucfirst($entry['status']))?></small></div><b class="<?=$entry['direction']==='in'?'money-in':'money-out'?>"><?=e($entry['direction']==='in'?'+':'-')?><?=money($entry['amount'],$entry['currency_code'])?></b><?php if($entry['status']==='unmatched'):?><form method="post" class="match-form"><?=csrf_field()?><input type="hidden" name="finance_action" value="match_reconciliation"><input type="hidden" name="return_view" value="reconciliation"><input type="hidden" name="entry_id" value="<?=$entry['id']?>"><select name="match_type" data-match-type><option value="payment">Customer payment</option><option value="bill_payment">Supplier payment</option><option value="expense">Expense</option><option value="ignored">Reviewed / no match</option></select><select name="match_id" data-match-id><optgroup label="Customer payments"><?php foreach($candidatePayments as $item):?><option data-type="payment" value="<?=$item['id']?>"><?=e($item['payment_date'].' · '.$item['number'].' · '.$item['customer'].' · '.money($item['amount']))?></option><?php endforeach;?></optgroup><optgroup label="Supplier payments"><?php foreach($billPayments as $item):?><option data-type="bill_payment" value="<?=$item['id']?>"><?=e($item['payment_date'].' · '.$item['number'].' · '.$item['supplier'].' · '.money($item['amount']))?></option><?php endforeach;?></optgroup><optgroup label="Expenses"><?php foreach($expenses as $item):?><option data-type="expense" value="<?=$item['id']?>"><?=e($item['expense_date'].' · '.$item['description'].' · '.money($item['amount']))?></option><?php endforeach;?></optgroup></select><button class="button secondary small">Match</button></form><?php else:?><span class="reconciled-mark">✓ Reviewed</span><?php endif;?></article><?php endforeach;?><?php if(!$entries):?><div class="empty">Import a statement or add a transaction to begin.</div><?php endif;?></div></section>

<?php elseif($view==='reminders'):?>
<?php $reminders=db()->query('SELECT r.*,d.number,d.currency_code,c.name customer FROM reminder_log r JOIN documents d ON d.id=r.document_id JOIN customers c ON c.id=r.customer_id ORDER BY FIELD(r.status,"queued","failed","sent","opened"),r.created_at DESC LIMIT 150')->fetchAll();?>
<section class="finance-grid"><article class="panel"><div class="panel-head"><div><h2>Prepare a reminder</h2><p>Choose WhatsApp or email using the customer details already saved.</p></div></div><form method="post" class="finance-form"><?=csrf_field()?><input type="hidden" name="finance_action" value="queue_reminder"><input type="hidden" name="return_view" value="reminders"><label>Outstanding invoice<select name="invoice_id" required><option value="">Select invoice</option><?php foreach($openInvoices as $invoice):?><option value="<?=$invoice['id']?>"><?=e($invoice['number'].' · '.$invoice['customer'].' · '.money($invoice['balance'],$invoice['currency_code']))?></option><?php endforeach;?></select></label><label>Channel<select name="channel"><option value="whatsapp">WhatsApp</option><option value="email">Email</option><option value="manual">Manual call</option></select></label><button class="button primary">Prepare reminder</button></form></article><article class="panel reminder-guidance"><span class="eyebrow">COLLECTIONS STANDARD</span><h2>Friendly, consistent follow-up</h2><ol><li>Confirm the balance and customer contact.</li><li>Send the prepared wording without changing the amount.</li><li>Mark the reminder sent for a complete audit trail.</li><li>Record payment only after it reaches Soda ZW.</li></ol></article></section>
<section class="panel"><div class="panel-head"><div><h2>Reminder queue and history</h2><p>All collection contact in one timeline.</p></div></div><div class="table-wrap"><table><thead><tr><th>Status</th><th>Created</th><th>Customer</th><th>Invoice</th><th>Channel</th><th>Message</th><th>Action</th></tr></thead><tbody><?php foreach($reminders as $reminder):?><tr><td><?=status_badge($reminder['status'])?></td><td><?=e(date('j M Y, H:i',strtotime($reminder['created_at'])))?></td><td><?=e($reminder['customer'])?></td><td><?=e($reminder['number'])?></td><td><?=e(ucfirst($reminder['channel']))?></td><td class="reminder-message"><?=e($reminder['message'])?></td><td><?php if($reminder['status']==='queued'):?><?php if($reminder['channel']==='whatsapp'):?><a class="button green small" target="_blank" rel="noopener" href="https://wa.me/<?=e(whatsapp_phone((string)$reminder['recipient']))?>?text=<?=rawurlencode($reminder['message'])?>">Open WhatsApp</a><?php elseif($reminder['channel']==='email'):?><a class="button secondary small" href="mailto:<?=e($reminder['recipient'])?>?subject=<?=rawurlencode('Soda ZW invoice '.$reminder['number'])?>&body=<?=rawurlencode($reminder['message'])?>">Open email</a><?php endif;?><form method="post" class="inline-form"><?=csrf_field()?><input type="hidden" name="finance_action" value="mark_reminder_sent"><input type="hidden" name="return_view" value="reminders"><input type="hidden" name="reminder_id" value="<?=$reminder['id']?>"><button class="button secondary small">Mark sent</button></form><?php else:?><span class="paid-clear"><?=e(ucfirst($reminder['status']))?></span><?php endif;?></td></tr><?php endforeach;?><?php if(!$reminders):?><tr><td colspan="7" class="empty">No reminders have been prepared.</td></tr><?php endif;?></tbody></table></div></section>

<?php elseif($view==='receiving'):?>
<?php $receipts=db()->query('SELECT sr.*,s.name supplier,b.number bill_number FROM stock_receipts sr LEFT JOIN suppliers s ON s.id=sr.supplier_id LEFT JOIN bills b ON b.id=sr.bill_id ORDER BY sr.id DESC LIMIT 50')->fetchAll();?>
<section class="panel stock-receiving-studio"><div class="panel-head"><div><span class="eyebrow">GOODS RECEIVED</span><h2>Receive stock and update costs</h2><p>One posting updates quantities, weighted costs, inventory history and—optionally—the supplier bill.</p></div></div><form method="post" data-stock-receipt><?=csrf_field()?><input type="hidden" name="finance_action" value="receive_stock"><input type="hidden" name="return_view" value="receiving"><div class="form-grid"><label>Supplier<select name="supplier_id"><option value="">No supplier selected</option><?php foreach($suppliers as $supplier):?><option value="<?=$supplier['id']?>"><?=e($supplier['name'])?></option><?php endforeach;?></select></label><label>Receipt date<input type="date" name="receipt_date" value="<?=date('Y-m-d')?>" required></label><label>Supplier invoice/reference<input name="supplier_reference"></label><label>Supplier bill due date<input type="date" name="due_date" value="<?=date('Y-m-d',strtotime('+14 days'))?>"></label></div><div class="stock-receipt-items"><div class="stock-receipt-head"><span>Product</span><span>Quantity received</span><span>Unit cost</span><span>Line cost</span><span></span></div><div data-stock-lines></div><button class="button secondary small" type="button" data-add-stock-line>+ Add product</button></div><template data-stock-line-template><div class="stock-receipt-line"><select data-stock-product><option value="">Select product</option><?php foreach($products as $product):?><option value="<?=$product['id']?>" data-cost="<?=e((string)$product['cost_price'])?>"><?=e($product['name'].' · on hand '.$product['stock_quantity'])?></option><?php endforeach;?></select><input type="number" step=".01" min="0" data-stock-qty placeholder="0"><input type="number" step=".01" min="0" data-stock-cost placeholder="0.00"><strong data-stock-total>USD 0.00</strong><button type="button" class="icon-button" data-remove-stock-line aria-label="Remove">×</button></div></template><div class="stock-receipt-footer"><label class="check"><input type="checkbox" name="create_bill" value="1"> Create an unpaid supplier bill for this receipt</label><label>Internal notes<textarea name="notes" rows="2"></textarea></label><div><span>Total receipt cost</span><strong data-stock-grand-total>USD 0.00</strong></div><button class="button primary">Post goods receipt</button></div></form></section>
<section class="panel"><div class="panel-head"><div><h2>Recent goods receipts</h2><p>Trace every quantity added to stock.</p></div></div><div class="table-wrap"><table><thead><tr><th>Receipt</th><th>Date</th><th>Supplier</th><th>Reference</th><th>Cost</th><th>Bill</th></tr></thead><tbody><?php foreach($receipts as $receipt):?><tr><td><strong><?=e($receipt['receipt_number'])?></strong></td><td><?=e($receipt['receipt_date'])?></td><td><?=e($receipt['supplier']?:'—')?></td><td><?=e($receipt['supplier_reference']?:'—')?></td><td><?=money($receipt['total_cost'])?></td><td><?=e($receipt['bill_number']?:'Not created')?></td></tr><?php endforeach;?><?php if(!$receipts):?><tr><td colspan="6" class="empty">No stock receipts posted yet.</td></tr><?php endif;?></tbody></table></div></section>

<?php elseif($view==='access'):?>
<?php require_permission('users.manage');$permissionLabels=['customers.manage'=>'Manage customers','crm.manage'=>'Manage sales pipeline','documents.create'=>'Create invoices and quotations','documents.manage'=>'Issue and update documents','payments.record'=>'Record customer payments','website_orders.manage'=>'Manage website orders','whatsapp.manage'=>'Manage WhatsApp inbox','operations.manage'=>'Manage refills and deliveries','catalogue.manage'=>'Manage product catalogue','stock.adjust'=>'Adjust stock counts','purchasing.manage'=>'Manage suppliers and bills','expenses.manage'=>'Record operating expenses','credits.manage'=>'Credit notes, refunds and reversals','reconciliation.manage'=>'Bank and EcoCash reconciliation','reminders.manage'=>'Customer payment reminders','stock.receive'=>'Receive stock','reports.view'=>'View management reports','settings.manage'=>'Change automation and business settings','backups.manage'=>'Download and schedule backups','users.manage'=>'Manage user access','imports.manage'=>'Import business records'];?>
<section class="panel"><div class="panel-head"><div><h2>Role and permission controls</h2><p>Owners always retain full access. Configure each administrator or staff account individually.</p></div></div><div class="permission-users"><?php foreach($users as $teamUser):?><?php if($teamUser['role']==='owner')continue;$defaults=permission_defaults($teamUser['role']);$overrides=[];$s=db()->prepare('SELECT permission_key,allowed FROM user_permissions WHERE user_id=?');$s->execute([$teamUser['id']]);$overrides=$s->fetchAll(PDO::FETCH_KEY_PAIR);?><form method="post" class="permission-card"><?=csrf_field()?><input type="hidden" name="finance_action" value="save_permissions"><input type="hidden" name="return_view" value="access"><input type="hidden" name="user_id" value="<?=$teamUser['id']?>"><div><span class="team-avatar"><?=e(strtoupper(substr($teamUser['name'],0,1)))?></span><span><strong><?=e($teamUser['name'])?></strong><small><?=e(ucfirst($teamUser['role']).' · '.$teamUser['email'])?></small></span></div><div class="permission-grid"><?php foreach($permissionLabels as $key=>$label):?><?php $allowed=array_key_exists($key,$overrides)?(bool)$overrides[$key]:($defaults[$key]??false);?><label><input type="checkbox" name="permissions[<?=e($key)?>]" value="1" <?=$allowed?'checked':''?>><span><?=e($label)?></span></label><?php endforeach;?></div><button class="button secondary">Save <?=e($teamUser['name'])?> access</button></form><?php endforeach;?></div></section>

<?php elseif($view==='health'):?>
<?php $migrationCount=(int)db()->query('SELECT COUNT(*) FROM schema_migrations')->fetchColumn();$recentAudit=db()->query('SELECT a.*,u.name user_name FROM audit_logs a LEFT JOIN users u ON u.id=a.user_id ORDER BY a.id DESC LIMIT 15')->fetchAll();$backupEvents=db()->query('SELECT * FROM backup_events ORDER BY id DESC LIMIT 10')->fetchAll();$failedReminders=(int)db()->query('SELECT COUNT(*) FROM reminder_log WHERE status="failed"')->fetchColumn();?>
<section class="health-grid"><article class="health-card good"><span>Database</span><strong>Connected</strong><small><?=$migrationCount?> controlled upgrades applied</small></article><article class="health-card <?=$lastBackup?'good':'warning'?>"><span>Last backup</span><strong><?=e($lastBackup?date('j M Y',strtotime($lastBackup)):'Required')?></strong><small><?=e($lastBackup?date('H:i',strtotime($lastBackup)):'Download one now')?></small></article><article class="health-card <?=$failedReminders?'warning':'good'?>"><span>Failed reminders</span><strong><?=$failedReminders?></strong><small>Review contact details</small></article><article class="health-card <?=$unmatched?'warning':'good'?>"><span>Unreconciled</span><strong><?=$unmatched?></strong><small>Financial transactions</small></article></section>
<section class="finance-grid"><article class="panel"><div class="panel-head"><div><h2>Backup protection</h2><p>Download a full database copy before updates and at least weekly.</p></div></div><a class="button primary" href="backup.php">Download database backup</a><div class="cron-note"><b>Scheduled backup and reminders</b><p>Add the command shown in the upload guide to cPanel Cron Jobs. The token remains private in the database.</p></div><div class="table-wrap"><table><thead><tr><th>Time</th><th>Type</th><th>File</th><th>Status</th></tr></thead><tbody><?php foreach($backupEvents as $event):?><tr><td><?=e($event['created_at'])?></td><td><?=e(ucfirst($event['backup_type']))?></td><td><?=e($event['filename'])?></td><td><?=status_badge($event['status'])?></td></tr><?php endforeach;?><?php if(!$backupEvents):?><tr><td colspan="4" class="empty">No verified backup recorded.</td></tr><?php endif;?></tbody></table></div></article><article class="panel"><div class="panel-head"><div><h2>Recent security and audit activity</h2><p>Who changed what and when.</p></div></div><div class="audit-list"><?php foreach($recentAudit as $event):?><div><span><?=e($event['created_at'])?></span><strong><?=e(ucwords(str_replace('_',' ',$event['action'])))?></strong><small><?=e($event['user_name']?:'System')?><?=e($event['details']?' · '.$event['details']:'')?></small></div><?php endforeach;?></div></article></section>

<?php elseif($view==='reports'):?>
<?php $reportFromInput=(string)($_GET['from']??date('Y-01-01'));$reportToInput=(string)($_GET['to']??date('Y-m-d'));if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$reportFromInput)||!preg_match('/^\d{4}-\d{2}-\d{2}$/',$reportToInput)||$reportToInput<$reportFromInput){$_GET['from']=date('Y-01-01');$_GET['to']=date('Y-m-d');}?>
<?php require_permission('reports.view');$from=$_GET['from']??date('Y-01-01');$to=$_GET['to']??date('Y-m-d');$q=function(string $sql,array $params=[])use($from,$to){$s=db()->prepare($sql);$s->execute($params?:[$from,$to]);return $s->fetchColumn();};$periodParams=[$baseCurrency,$from,$to];$gross=(float)$q('SELECT COALESCE(SUM(total),0) FROM documents WHERE type="invoice" AND status<>"cancelled" AND currency_code=? AND issue_date BETWEEN ? AND ?',$periodParams);$creditValue=(float)$q('SELECT COALESCE(SUM(total),0) FROM documents WHERE type="credit_note" AND status<>"cancelled" AND currency_code=? AND issue_date BETWEEN ? AND ?',$periodParams);$received=(float)$q('SELECT COALESCE(SUM(p.amount),0) FROM payments p JOIN documents d ON d.id=p.invoice_id WHERE d.currency_code=? AND p.payment_date BETWEEN ? AND ?',$periodParams);$refunds=(float)$q('SELECT COALESCE(SUM(r.amount),0) FROM payment_reversals r JOIN payments p ON p.id=r.payment_id JOIN documents d ON d.id=p.invoice_id WHERE d.currency_code=? AND r.reversal_date BETWEEN ? AND ?',$periodParams);$expenses=(float)$q('SELECT COALESCE(SUM(amount),0) FROM expenses WHERE expense_date BETWEEN ? AND ?');$stockValue=(float)db()->query('SELECT COALESCE(SUM(stock_quantity*cost_price),0) FROM products WHERE active=1 AND track_stock=1')->fetchColumn();$marginStmt=db()->prepare('SELECT COALESCE(SUM(di.line_subtotal-(di.quantity*di.unit_cost)),0) FROM document_items di JOIN documents d ON d.id=di.document_id WHERE d.type="invoice" AND d.status<>"cancelled" AND d.currency_code=? AND d.issue_date BETWEEN ? AND ?');$marginStmt->execute($periodParams);$grossMargin=(float)$marginStmt->fetchColumn();$tax=(float)$q('SELECT COALESCE(SUM(tax_total),0) FROM documents WHERE type="invoice" AND status<>"cancelled" AND currency_code=? AND issue_date BETWEEN ? AND ?',$periodParams);$channels=db()->prepare('SELECT COALESCE(a.name,p.method,"Other") channel,SUM(p.amount-COALESCE((SELECT SUM(r.amount) FROM payment_reversals r WHERE r.payment_id=p.id),0)) total FROM payments p JOIN documents d ON d.id=p.invoice_id LEFT JOIN payment_accounts a ON a.id=p.account_id WHERE d.currency_code=? AND p.payment_date BETWEEN ? AND ? GROUP BY channel ORDER BY total DESC');$channels->execute($periodParams);$ageingStmt=db()->prepare('SELECT SUM(CASE WHEN age<=0 THEN balance ELSE 0 END) current_due,SUM(CASE WHEN age BETWEEN 1 AND 30 THEN balance ELSE 0 END) d30,SUM(CASE WHEN age BETWEEN 31 AND 60 THEN balance ELSE 0 END) d60,SUM(CASE WHEN age BETWEEN 61 AND 90 THEN balance ELSE 0 END) d90,SUM(CASE WHEN age>90 THEN balance ELSE 0 END) over90 FROM (SELECT DATEDIFF(CURDATE(),d.due_date) age,'.finance_document_balance_sql('d').' balance FROM documents d WHERE d.type="invoice" AND d.currency_code=? AND d.status NOT IN ("draft","cancelled")) x');$ageingStmt->execute([$baseCurrency]);$ageing=$ageingStmt->fetch();?>
<?php $summary=management_report_summary($baseCurrency,$from,$to);$gross=$summary['gross_sales'];$creditValue=$summary['credits'];$received=$summary['cash_received'];$refunds=$summary['refunds'];$expenses=$summary['expenses'];$stockValue=$summary['stock_value'];$grossMargin=$summary['gross_product_margin_before_credits'];$tax=$summary['tax_invoiced'];$channels=db()->prepare('SELECT channel,SUM(total) total FROM (SELECT COALESCE(a.name,p.method,"Other") channel,SUM(p.amount) total FROM payments p JOIN documents d ON d.id=p.invoice_id LEFT JOIN payment_accounts a ON a.id=p.account_id WHERE d.type="invoice" AND d.status<>"cancelled" AND d.currency_code=? AND p.payment_date BETWEEN ? AND ? GROUP BY channel UNION ALL SELECT COALESCE(a.name,p.method,"Other") channel,-SUM(r.amount) total FROM payment_reversals r JOIN payments p ON p.id=r.payment_id JOIN documents d ON d.id=p.invoice_id LEFT JOIN payment_accounts a ON a.id=p.account_id WHERE d.type="invoice" AND d.status<>"cancelled" AND d.currency_code=? AND r.reversal_date BETWEEN ? AND ? GROUP BY channel) flows GROUP BY channel HAVING ABS(SUM(total))>.009 ORDER BY total DESC');$channels->execute([$baseCurrency,$from,$to,$baseCurrency,$from,$to]);?>
<form class="toolbar"><input type="hidden" name="view" value="reports"><label>From<input type="date" name="from" value="<?=e($from)?>"></label><label>To<input type="date" name="to" value="<?=e($to)?>"></label><button class="button secondary">Apply period</button><a class="button primary" href="finance-control.php?view=reports&amp;download=management-report&amp;from=<?=e($from)?>&amp;to=<?=e($to)?>">Export CSV</a></form><section class="finance-kpis report-kpis"><article><span>Net invoiced sales</span><strong><?=money($gross-$creditValue,$baseCurrency)?></strong><small>Gross <?=money($gross,$baseCurrency)?> less credits</small></article><article><span>Net cash received</span><strong><?=money($received-$refunds,$baseCurrency)?></strong><small>After <?=money($refunds,$baseCurrency)?> refunds</small></article><article><span>Gross product margin</span><strong><?=money($grossMargin,$baseCurrency)?></strong><small>Before credit allocation and expenses</small></article><article><span>Stock valuation</span><strong><?=money($stockValue,$baseCurrency)?></strong><small>Current cost basis</small></article><article><span>Tax invoiced</span><strong><?=money($tax,$baseCurrency)?></strong><small>Excluded from operating contribution</small></article><article><span>Operating contribution</span><strong><?=money((($gross-$creditValue)-$tax)-$expenses,$baseCurrency)?></strong><small>Net sales excluding tax, less recorded expenses</small></article></section>
<section class="finance-grid"><article class="panel"><div class="panel-head"><div><h2>Receivables ageing</h2><p>Outstanding invoices by days overdue.</p></div></div><div class="ageing-bars"><?php foreach(['Current'=>(float)($ageing['current_due']??0),'1–30 days'=>(float)($ageing['d30']??0),'31–60 days'=>(float)($ageing['d60']??0),'61–90 days'=>(float)($ageing['d90']??0),'90+ days'=>(float)($ageing['over90']??0)] as $label=>$amount):?><div><span><?=e($label)?></span><progress max="<?=max(1,array_sum(array_map('floatval',$ageing?:[])))?>" value="<?=$amount?>"></progress><strong><?=money($amount)?></strong></div><?php endforeach;?></div></article><article class="panel"><div class="panel-head"><div><h2>Payment channels</h2><p>Net receipts by method.</p></div></div><div class="channel-list"><?php foreach($channels as $channel):?><div><span><?=e($channel['channel'])?></span><strong><?=money($channel['total'])?></strong></div><?php endforeach;?></div></article></section>
<?php endif;?>
<?php render_footer();
