Files
itflow/agent/income.php
johnnyq 47969cd3c2 Income Import Deposits: bring the money in on a bank statement in as payments on invoices, other income or transfers from the Income list's menu, reading the same CSV as the expense statement import with the columns kept on the account for both sides and a link from each side's review to the other without uploading again; each deposit is matched by an invoice number in its text or reference, the payer remembered from last time, a client's name in it, the only invoice or the oldest of one client's invoices owing the amount or everything one client owes together, a payment provider payout or a transfer from another account, and anything else as other income for a category and client; payments pay the client's oldest invoices first with one deposit paying several and a short one leaving the last part paid, picking a client ticks the invoices it pays and each line says what it pays off, two deposits on one invoice don't pay it twice and more than they owe blocks the import; deposits imported before are locked, ones matching a payment, other income, transfer or a day's payments entered by hand within 4 days are left out and linked when left out, and a payout into the payment provider's own account counts as recorded; every ticked deposit is checked before any is saved, invoices get their status and history, receipts can be emailed, the payment method is read from the text, and payments and other income say they came from a bank statement on their quick look; payments need write Sales access and client scope applies; remembered payers window with forget; the statement columns step moved to a shared include and the sign guess now reads deposit and charge words on the lines; new functions/income_import.php, agent/income_import.php, js/income_import.js and agent/post/income_import.php; database 3.0.6 adds payment_import_key, revenue_import_key and the income_import_rules table; CHANGELOG updated
2026-10-16 21:44:14 -04:00

440 lines
30 KiB
PHP

<?php
/*
* Income - the money that comes in: payments against invoices, and other
* income that isn't for an invoice (a deposit, a rebate, interest, a
* referral fee). functions/income.php has what counts and how.
*
* Along the top: this month against the same days of last month, the year
* so far with its monthly average, what the running recurring invoices bill
* a month, and what is still owed on invoices. Pills cut the list to
* invoice payments, other income, what was paid online and what has no
* category; filters by client, category, account, payment method and date.
* A row opens its quick look - a payment's with its invoice, other income's
* with Edit and Copy.
*
* Takes ?client= (or the client pages' ?client_id=), ?category=,
* ?account=, ?method=, ?show= (payments, other, online, uncategorised; the
* older ?type=payment and revenue too), the date filter and ?open=
* (payment-12 or revenue-7) to open one.
*/
// Default Column Sortby/Order Filter
$sort = "income_date";
$order = "DESC";
// If client_id is in URI then show client Side Bar and client header
if (isset($_GET['client_id'])) {
require_once "includes/inc_all_client.php";
$client_page = true;
} else {
require_once "includes/inc_all.php";
$client_page = false;
$client_id = 0;
}
enforceUserPermission('module_financial');
$can_edit = incomeCanEdit();
$can_delete = incomeCanDelete();
$can_bulk = $can_edit;
$can_sales = lookupUserPermission('module_sales') >= 1;
$client_filter = $client_page ? intval($client_id) : intval($_GET['client'] ?? 0);
$category_filter = intval($_GET['category'] ?? 0);
$account_filter = intval($_GET['account'] ?? 0);
$method_filter = (string) ($_GET['method'] ?? '');
$shows = incomeShows();
$show = incomeShowFrom($_GET);
$more_filters = $account_filter || $method_filter !== '';
// Only these sort - filter_header.php hands through any lowercase word
$sortable_columns = ['income_date' => 'income.income_date', 'income_amount' => 'income.income_amount', 'client_name' => 'income.client_name',
'category_name' => 'income.category_name', 'account_name' => 'income.account_name', 'income_type' => 'income.income_type'];
if (!isset($sortable_columns[$sort])) {
$sort = 'income_date';
}
// Everything but the pill, then the pill - the pill counts are over what the rest leave
$filter_src = array_merge($_GET, ['dtf' => $dtf, 'dtt' => $dtt, 'client' => $client_filter, 'show' => $show, 'type' => '']);
$where = incomeFiltersSql($filter_src, $summary, false);
$show_query = $show !== '' ? 'AND ' . $shows[$show]['where'] : '';
$union = incomeUnionSql();
$sql = mysqli_query($mysqli, "SELECT SQL_CALC_FOUND_ROWS income.* FROM ($union) AS income
WHERE $where $show_query
ORDER BY {$sortable_columns[$sort]} $order, income.income_created_at DESC, income.income_id DESC LIMIT $record_from, $record_to");
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
$rows = [];
while ($row = mysqli_fetch_assoc($sql)) {
$rows[] = $row;
}
// The pills, and the total of what is listed
$count_sql = [];
foreach ($shows as $key => $s) {
$count_sql[] = $key === '' ? 'COUNT(*)' : "COALESCE(SUM({$s['where']}), 0)";
}
$row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT " . implode(', ', $count_sql) . ", COALESCE(SUM(CASE WHEN 1 $show_query THEN income.income_amount END), 0)
FROM ($union) AS income WHERE $where"));
$counts = [];
foreach (array_keys($shows) as $i => $key) {
$counts[$key] = intval($row[$i]);
}
$listed_total = floatval($row[count($shows)]);
$stats = incomeStats($client_page ? $client_id : 0);
$money = function ($amount, $currency = '') {
return escapeHtml(billingMoney($amount, $currency));
};
$filter_url = function ($changes) use ($client_page, $client_id) {
$params = array_merge($_GET, $changes);
unset($params['page'], $params['type']);
if ($client_page) {
$params['client_id'] = $client_id;
}
return '?' . http_build_query(array_filter($params, function ($v) { return $v !== '' && $v !== null; }));
};
$date_url = function ($from, $to, $changes = []) use ($filter_url) {
return $filter_url(array_merge(['canned_date' => 'custom', 'dtf' => $from, 'dtt' => $to], $changes));
};
$any_filter = $q || ($client_filter && !$client_page) || $category_filter || $account_filter || $method_filter !== '' || $show !== '' || $dtf !== '1970-01-01';
$add_url = 'modals/revenue/revenue_add.php' . ($client_page ? "?client_id=$client_id" : '');
$invoices_url = 'invoices.php?' . ($client_page ? "client_id=$client_id&" : '') . 'status=Unpaid';
?>
<!-- The money in -->
<div class="row g-3 mb-3">
<div class="col-sm-6 col-xl-3">
<a class="itflow-billing-stat <?= $dtf === $stats['month_start'] && $show === '' ? 'active' : '' ?>" href="<?= escapeHtml($date_url($stats['month_start'], date('Y-m-t'), ['show' => ''])) ?>">
<div class="itflow-billing-stat-label"><i class="fas fa-calendar-alt me-1"></i>This month</div>
<div class="itflow-billing-stat-value text-success-emphasis"><?= $money($stats['month']) ?></div>
<div class="itflow-billing-stat-note">
<?php if ($stats['change'] !== null) { ?>
<span class="<?= $stats['change'] >= 0 ? 'text-success-emphasis' : 'text-warning-emphasis' ?>"><i class="fas fa-arrow-<?= $stats['change'] >= 0 ? 'up' : 'down' ?> me-1"></i><?= abs($stats['change']) ?>%</span> on the same days of <?= date('F', strtotime($stats['last_start'])) ?>
<?php } else { ?>
Nothing in by now last month
<?php } ?>
</div>
</a>
</div>
<div class="col-sm-6 col-xl-3">
<a class="itflow-billing-stat <?= $dtf === $stats['year_start'] && $show === '' ? 'active' : '' ?>" href="<?= escapeHtml($date_url($stats['year_start'], date('Y-12-31'), ['show' => ''])) ?>">
<div class="itflow-billing-stat-label"><i class="fas fa-chart-line me-1"></i><?= date('Y') ?> so far</div>
<div class="itflow-billing-stat-value"><?= $money($stats['year']) ?></div>
<div class="itflow-billing-stat-note"><?= $money($stats['year_average']) ?> a month on average</div>
</a>
</div>
<div class="col-sm-6 col-xl-3">
<a class="itflow-billing-stat" href="<?= $can_sales ? 'recurring_invoices.php' . ($client_page ? "?client_id=$client_id" : '') : '#' ?>">
<div class="itflow-billing-stat-label"><i class="fas fa-redo-alt me-1"></i>Recurring</div>
<div class="itflow-billing-stat-value"><?= $money($stats['recurring']) ?><span class="fs-6 fw-normal text-body-secondary"> a month</span></div>
<div class="itflow-billing-stat-note"><?= $stats['recurring_count'] ? 'From ' . $stats['recurring_count'] . ' recurring invoice' . ($stats['recurring_count'] === 1 ? '' : 's') : 'No recurring invoices running' ?></div>
</a>
</div>
<div class="col-sm-6 col-xl-3">
<a class="itflow-billing-stat" href="<?= $can_sales ? escapeHtml($invoices_url) : '#' ?>">
<div class="itflow-billing-stat-label"><i class="fas fa-hourglass-half me-1"></i>Still owed</div>
<div class="itflow-billing-stat-value <?= $stats['overdue'] > 0 ? 'text-danger-emphasis' : '' ?>"><?= $money($stats['owed']) ?></div>
<div class="itflow-billing-stat-note">
<?= $stats['owed_count'] ? 'On ' . $stats['owed_count'] . ' invoice' . ($stats['owed_count'] === 1 ? '' : 's') : 'Every invoice is paid' ?><?= $stats['overdue'] > 0 ? ' &middot; <span class="text-danger-emphasis">' . $money($stats['overdue']) . ' late</span>' : '' ?>
</div>
</a>
</div>
</div>
<div class="card">
<div class="card-header bg-dark py-2">
<h3 class="card-title mt-2"><i class="fas fa-fw fa-hand-holding-usd me-2"></i>Income</h3>
<div class="card-tools">
<div class="btn-group">
<?php if ($can_edit) { ?>
<button type="button" class="btn btn-primary ajax-modal" data-modal-size="lg" data-modal-url="<?= $add_url ?>"><i class="fas fa-plus me-2"></i>New Income</button>
<?php } ?>
<button type="button" class="btn <?= $can_edit ? 'btn-primary dropdown-toggle-split' : 'btn-light' ?> dropdown-toggle" data-bs-toggle="dropdown" aria-label="More"><?= $can_edit ? '' : '<i class="fas fa-ellipsis-h"></i>' ?></button>
<div class="dropdown-menu dropdown-menu-end">
<?php if ($can_edit) { ?>
<a class="dropdown-item" href="income_import.php"><i class="fa fa-fw fa-file-import text-secondary me-2"></i>Import Deposits</a>
<?php } ?>
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="<?= buildExportModalUrl('modals/income/income_export.php', ['client_id', 'client', 'category', 'account', 'method', 'show', 'type', 'q'], ['dtf' => $dtf, 'dtt' => $dtt]) ?>">
<i class="fa fa-fw fa-download text-secondary me-2"></i>Export
</a>
<?php if (lookupUserPermission('module_reporting') >= 1) { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="reports/income_summary.php"><i class="fa fa-fw fa-chart-bar text-secondary me-2"></i>Income Summary</a>
<a class="dropdown-item" href="reports/income_by_client.php<?= $client_page ? "?client_id=$client_id" : '' ?>"><i class="fa fa-fw fa-users text-secondary me-2"></i>Income by Client</a>
<?php } ?>
</div>
</div>
</div>
</div>
<div class="card-header py-3">
<form autocomplete="off">
<input type="hidden" name="show" value="<?= escapeHtml($show) ?>">
<?php if ($client_page) { ?>
<input type="hidden" name="client_id" value="<?= $client_id ?>">
<?php } ?>
<div class="row g-2 align-items-center">
<div class="col-lg-4 col-md-6">
<div class="input-group">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search income" title="What for, invoice number, reference, amount, client, category, account">
<button class="btn btn-outline-secondary <?= $more_filters ? 'active' : '' ?>" title="More filters" type="button" data-bs-toggle="collapse" data-bs-target="#incomeMoreFilters"><i class="fas fa-filter"></i></button>
<button class="btn btn-dark" title="Search"><i class="fa fa-search"></i></button>
</div>
</div>
<div class="col-lg-2 col-md-6">
<input type="text" id="dateFilter" class="form-control" autocomplete="off" title="Date it came in" aria-label="Date it came in">
<input type="hidden" name="canned_date" id="canned_date" value="<?= escapeHtml($_GET['canned_date'] ?? '') ?>">
<input type="hidden" name="dtf" id="dtf" value="<?= escapeHtml($dtf ?? '') ?>">
<input type="hidden" name="dtt" id="dtt" value="<?= escapeHtml($dtt ?? '') ?>">
</div>
<div class="col-lg-<?= $client_page ? 6 : 3 ?> col-md-6">
<div class="d-flex gap-2">
<div class="flex-grow-1 min-w-0">
<select class="form-select select2" name="category" onchange="this.form.submit()" aria-label="Category">
<option value="">- Any category -</option>
<?php foreach (incomeCategories($category_filter) as $option_id => $category) { ?>
<option <?= $category_filter === $option_id ? 'selected' : '' ?> value="<?= $option_id ?>"<?= selectOptionColor($category['category_color'] ?? '', $category['category_icon'] ?? '') ?>><?= escapeHtml($category['category_name']) ?></option>
<?php } ?>
</select>
</div>
<?php if ($client_page && $can_bulk) { ?>
<?php require __DIR__ . '/includes/income_bulk_menu.php'; ?>
<?php } ?>
</div>
</div>
<?php if (!$client_page) { ?>
<div class="col-lg-3 col-md-6">
<div class="d-flex gap-2">
<div class="flex-grow-1 min-w-0">
<select class="form-select select2" name="client" onchange="this.form.submit()" aria-label="Client">
<option value="">- Any client -</option>
<?php
$sql_filter = mysqli_query($mysqli, "SELECT client_id, client_name, client_abbreviation FROM clients
WHERE (EXISTS (SELECT 1 FROM invoices JOIN payments ON payment_invoice_id = invoice_id WHERE invoice_client_id = client_id)
OR EXISTS (SELECT 1 FROM revenues WHERE revenue_client_id = client_id)) " . clientScopeSql('clients.client_id') . " ORDER BY client_name ASC");
while ($row = mysqli_fetch_assoc($sql_filter)) {
$option_id = intval($row['client_id']); ?>
<option <?= $client_filter === $option_id ? 'selected' : '' ?> value="<?= $option_id ?>"<?= selectOptionClient($row['client_id'], $row['client_name'], $row['client_abbreviation'] ?? '') ?>><?= escapeHtml($row['client_name']) ?></option>
<?php } ?>
</select>
</div>
<?php if ($can_bulk) { ?>
<?php require __DIR__ . '/includes/income_bulk_menu.php'; ?>
<?php } ?>
</div>
</div>
<?php } ?>
</div>
<div class="collapse <?= $more_filters ? 'show' : '' ?>" id="incomeMoreFilters">
<div class="row g-2 mt-0">
<div class="col-lg-3 col-md-6">
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-piggy-bank"></i></span>
<select class="form-select select2" name="account" onchange="this.form.submit()" aria-label="Paid into">
<option value="">- Any account -</option>
<?php foreach (paymentAccounts($account_filter) as $option_id => $account) { ?>
<option <?= $account_filter === $option_id ? 'selected' : '' ?> value="<?= $option_id ?>"><?= escapeHtml($account['account_name']) ?></option>
<?php } ?>
</select>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-credit-card"></i></span>
<select class="form-select select2" name="method" onchange="this.form.submit()" aria-label="Paid by">
<option value="">- Any payment method -</option>
<?php
$sql_filter = mysqli_query($mysqli, "SELECT DISTINCT payment_method AS method FROM payments WHERE COALESCE(payment_method, '') <> ''
UNION SELECT DISTINCT revenue_payment_method FROM revenues WHERE COALESCE(revenue_payment_method, '') <> '' ORDER BY method ASC");
while ($row = mysqli_fetch_row($sql_filter)) { ?>
<option <?= $method_filter === $row[0] ? 'selected' : '' ?> value="<?= escapeHtml($row[0]) ?>"><?= escapeHtml($row[0]) ?></option>
<?php } ?>
</select>
</div>
</div>
</div>
</div>
</form>
<!-- Cut the list - counted over what the filters above leave -->
<nav class="nav nav-pills itflow-asset-tabs mt-2 flex-nowrap overflow-auto">
<?php foreach ($shows as $key => $s) {
$alarm = $key === 'uncategorised' && $counts[$key]; ?>
<a class="nav-link text-nowrap <?= $show === $key ? 'active' : '' ?>" href="<?= escapeHtml($filter_url(['show' => $key])) ?>"<?= !empty($s['title']) ? ' title="' . escapeHtml($s['title']) . '"' : '' ?>>
<?php if (!empty($s['icon'])) { ?><i class="fas fa-fw <?= $s['icon'] ?> me-1"></i><?php } ?><?= $s['label'] ?>
<span class="badge rounded-pill ms-1 <?= $alarm ? 'text-bg-warning' : '' ?>"><?= $counts[$key] ?></span>
</a>
<?php } ?>
</nav>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 itflow-billing-table">
<thead class="<?= $num_rows[0] ? '' : 'd-none' ?> text-nowrap table-light">
<tr>
<?php if ($can_bulk) { ?>
<th class="checkbox-column border-end">
<div class="form-check">
<input class="form-check-input" id="selectAllCheckbox" type="checkbox" onclick="checkAll(this)" title="Select all on this page">
</div>
</th>
<?php } ?>
<th><a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=income_date&order=<?= $disp ?>">Date <?php if ($sort == 'income_date') { echo $order_icon; } ?></a></th>
<th><a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=income_type&order=<?= $disp ?>">From <?php if ($sort == 'income_type') { echo $order_icon; } ?></a></th>
<th><a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=category_name&order=<?= $disp ?>">Category <?php if ($sort == 'category_name') { echo $order_icon; } ?></a></th>
<?php if (!$client_page) { ?>
<th><a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=client_name&order=<?= $disp ?>">Client <?php if ($sort == 'client_name') { echo $order_icon; } ?></a></th>
<?php } ?>
<th><a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=account_name&order=<?= $disp ?>">Paid into <?php if ($sort == 'account_name') { echo $order_icon; } ?></a></th>
<th class="text-end"><a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=income_amount&order=<?= $disp ?>">Amount <?php if ($sort == 'income_amount') { echo $order_icon; } ?></a></th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $row) {
$is_payment = $row['income_type'] === 'payment';
$income_id = intval($row['income_id']);
$ref = $row['income_type'] . '-' . $income_id;
$row_open = 'data-modal-size="lg" data-modal-url="modals/' . ($is_payment ? 'payment/payment' : 'revenue/revenue') . '.php?id=' . $income_id . '"';
$row_amount = floatval($row['income_amount']);
$row_currency = (string) $row['income_currency_code'];
$row_client_id = intval($row['income_client_id']);
$first_line = trim(strtok((string) $row['income_description'], "\n"));
?>
<tr>
<?php if ($can_bulk) { ?>
<td class="checkbox-column bg-light border-end">
<div class="form-check">
<input class="form-check-input bulk-select" type="checkbox" name="income_ids[]" value="<?= $ref ?>" aria-label="Select this">
</div>
</td>
<?php } ?>
<td class="text-nowrap"><a class="fw-semibold ajax-modal" href="#" <?= $row_open ?>><?= escapeHtml($row['income_date']) ?></a></td>
<td class="itflow-income-from">
<?php if ($is_payment) { ?>
<div class="text-truncate">
<i class="fas fa-file-invoice-dollar text-body-secondary me-1" title="Invoice payment"></i>
<?php if ($row['invoice_number'] !== null) { ?>
<?php if ($can_sales) { ?><a href="invoice.php?invoice_id=<?= intval($row['income_invoice_id']) ?>"><?= escapeHtml($row['invoice_prefix'] . $row['invoice_number']) ?></a><?php } else { ?><?= escapeHtml($row['invoice_prefix'] . $row['invoice_number']) ?><?php } ?>
<?php if ($first_line !== '') { ?><span class="text-body-secondary"> - <?= escapeHtml($first_line) ?></span><?php } ?>
<?php } else { ?>
<span class="text-body-secondary">An invoice since deleted</span>
<?php } ?>
</div>
<?php } else { ?>
<div class="text-truncate"><i class="fas fa-hand-holding-usd text-body-secondary me-1" title="Other income"></i><?= escapeHtml($first_line !== '' ? $first_line : ($row['category_name'] ?: 'Other income')) ?></div>
<?php } ?>
<div class="small text-body-secondary text-truncate">
<?= escapeHtml(implode(' · ', array_filter([(string) $row['income_method'], $row['income_reference'] ? '#' . $row['income_reference'] : '']))) ?: '&nbsp;' ?>
<?php if (intval($row['income_online'])) { ?><span class="badge rounded-pill bg-info-subtle text-info-emphasis fw-normal ms-1"><i class="fas fa-globe me-1"></i>Online</span><?php } ?>
</div>
</td>
<td>
<?php if (intval($row['income_category_id']) && $row['category_name'] !== null) { ?>
<div class="text-truncate" style="max-width: 10rem;" title="<?= escapeHtml($row['category_name']) ?>"><?= expenseCategoryDot($row['category_color']) ?><?= escapeHtml($row['category_name']) ?></div>
<?php } else { ?>
<span class="text-warning-emphasis small" title="Not in a category - the reports show it as No category">No category</span>
<?php } ?>
</td>
<?php if (!$client_page) { ?>
<td>
<?php if ($row_client_id && $row['client_name'] !== null) { ?>
<div class="text-truncate" style="max-width: 12rem;"><a class="text-reset" href="<?= escapeHtml($filter_url(['client' => $row_client_id])) ?>" title="<?= escapeHtml($row['client_name']) ?> - what came in from them"><?= escapeHtml($row['client_name']) ?></a></div>
<?php } else { ?>
<span class="text-body-secondary">-</span>
<?php } ?>
</td>
<?php } ?>
<td>
<div class="text-truncate" style="max-width: 10rem;"><?= escapeHtml($row['account_name'] ?: '-') ?></div>
<?php if ($row['account_archived_at']) { ?><div class="small text-body-secondary">Archived</div><?php } ?>
</td>
<td class="text-end itflow-money text-nowrap">
<span class="fw-semibold <?= $row_amount < 0 ? 'text-danger-emphasis' : '' ?>"><?= $money($row_amount, $row_currency) ?></span>
</td>
<td class="text-end">
<div class="dropdown dropstart">
<button class="btn btn-sm btn-light" title="More actions" type="button" data-bs-toggle="dropdown"><i class="fas fa-fw fa-ellipsis-v"></i></button>
<div class="dropdown-menu">
<a class="dropdown-item ajax-modal" href="#" <?= $row_open ?>><i class="fas fa-fw fa-eye text-secondary me-2"></i>Open</a>
<?php if ($is_payment) { ?>
<?php if ($can_sales && $row['invoice_number'] !== null) { ?>
<a class="dropdown-item" href="invoice.php?invoice_id=<?= intval($row['income_invoice_id']) ?>"><i class="fas fa-fw fa-file-invoice text-secondary me-2"></i>Open Invoice</a>
<?php } ?>
<?php if (incomeCanEditPayments() && $row['invoice_number'] !== null) { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/payment/payment_edit.php?id=<?= $income_id ?>"><i class="fas fa-fw fa-edit text-secondary me-2"></i>Edit</a>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_payment=<?= $income_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"
data-confirm-text="The payment of <?= $money($row_amount, $row_currency) ?> comes off <?= escapeHtml($row['invoice_prefix'] . $row['invoice_number']) ?>, which is owed again.">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
<?php } ?>
<?php } else { ?>
<?php if ($can_edit) { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/revenue/revenue_edit.php?id=<?= $income_id ?>"><i class="fas fa-fw fa-edit text-secondary me-2"></i>Edit</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/revenue/revenue_copy.php?id=<?= $income_id ?>"><i class="fas fa-fw fa-copy text-secondary me-2"></i>Copy</a>
<?php } ?>
<?php if ($can_delete) { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_revenue=<?= $income_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"
data-confirm-text="<?= escapeHtml(($first_line !== '' ? $first_line : 'This income') . ' - ' . billingMoney($row_amount, $row_currency)) ?> goes for good.">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
<?php } ?>
<?php } ?>
</div>
</div>
</td>
</tr>
<?php } ?>
</tbody>
<?php if ($num_rows[0]) { ?>
<tfoot>
<tr class="table-light">
<td colspan="<?= ($can_bulk ? 5 : 4) + ($client_page ? 0 : 1) ?>" class="text-body-secondary small">
<?php $listed = intval($num_rows[0]); ?>
<?= $listed ?> <?= $show === 'payments' || $show === 'online' ? ($listed === 1 ? 'payment' : 'payments') : ($show === 'other' ? 'other income' : ($listed === 1 ? 'payment or other income' : 'payments and other income')) ?><?= $listed > count($rows) ? ' - the total is for all of them, not just this page' : '' ?>
</td>
<td class="text-end fw-semibold text-nowrap itflow-money"><?= $money($listed_total) ?></td>
<td></td>
</tr>
</tfoot>
<?php } ?>
</table>
</div>
<?php if (!$num_rows[0]) { ?>
<div class="itflow-asset-empty">
<i class="fas fa-hand-holding-usd fa-2x mb-2 opacity-50"></i>
<div><?= $any_filter ? 'Nothing matches.' : 'Nothing has come in yet.' ?></div>
<?php if (!$any_filter) { ?>
<div class="small mt-1">A payment is added on its invoice. Money in that isn't for an invoice is New Income here<?= $can_edit ? ', and a bank statement\'s deposits come in with <a href="income_import.php">Import Deposits</a>' : '' ?>.</div>
<?php } ?>
</div>
<?php } ?>
<?php require_once "../includes/filter_footer.php"; ?>
</div>
<?php if (preg_match('/^(payment|revenue)-(\d+)$/i', (string) ($_GET['open'] ?? ''), $open)) { ?>
<script>
// Opened from elsewhere - straight to its quick look
document.addEventListener('DOMContentLoaded', function () {
var open = document.createElement('a');
open.className = 'ajax-modal';
open.href = '#';
open.dataset.modalSize = 'lg';
open.dataset.modalUrl = 'modals/<?= strtolower($open[1]) === 'payment' ? 'payment/payment' : 'revenue/revenue' ?>.php?id=<?= intval($open[2]) ?>';
document.body.appendChild(open);
open.click();
open.remove();
});
</script>
<?php } ?>
<script src="../js/bulk_actions.js"></script>
<?php require_once "../includes/footer.php";