Files
itflow/agent/recurring_invoice.php
johnnyq 755e488d6c Add and change lines on invoices, quotes and recurring invoices on the page itself, without a reload: the row of boxes under the lines is now one field - type a product and Enter takes the one picked out, adding it as a line with its description, price and tax, billed as the product usually is on a quote and counted per user or per device on a recurring invoice, or type anything and pick the Add row for a line of its own - and the new line opens with its quantity ready to type over, Enter moving on to a price it still needs and then back to the field; a line name, description, quantity, price and tax are changed where they sit, by a click or by Tab and Enter, saved on Enter or on leaving, put back by Escape, with the tax picked from the select picker and a counted quantity left to the Edit Line window; a quote line menu gains Billed, one time to yearly; agent/ajax.php gains billing_item_add and billing_item_set with the checks post.php billing_item_save makes, answering JSON, and functions/billing.php gains billingItemSetField, which replaces one value and hands the line to billingItemSave so stock, tax and totals follow as before, saves nothing when the value is the same, and gives the audit log what changed; js/billing_items.js is rewritten to send saves one at a time in order and then fetch the page again, taking from it the lines by id and every element marked data-billing-live - the totals and amount due in includes/billing_paper.php, the header card, tax warning and side column on the three pages - so nothing is drawn twice, a field in use is left alone, and a document no longer open for changes reloads; the paper carries the products as JSON and the tax options, and itflowBillingItems no longer takes products; js/autocomplete.js gains pickFirst and an extra row kept in view under the list, and opens above the field when the list does not fit below; getProductsForAutocomplete encodes with JSON_HEX_TAG and JSON_HEX_AMP; the guest and client portal pages are unchanged; add a 26.11.8 changelog section
2026-11-12 16:14:30 -05:00

428 lines
29 KiB
PHP

<?php
/*
* Recurring Invoice - an agreement billed every month, quarter or year.
*
* The header says whether it is running and when it next bills - and,
* when a run was missed, whether the nightly run still bills it or it is
* too far behind and needs Invoice Now or moving on. The template is on the left as
* the invoices it makes will look, its lines edited in place. On the
* right: the schedule (what it brings in a month, the period the next
* invoice covers and the dates after, whether invoices go out by
* themselves), its per-user and per-device lines against what the client
* has now, what it costs us (the recurring expenses it covers, and the
* margin left), the card it charges, the invoices it has made and whether
* they were paid, and its history. The contract it bills, when it is
* linked to one - it starts and stops with it (functions/contract_billing.php).
*/
// 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_url = "client_id=$client_id&";
} else {
require_once "includes/inc_all.php";
$client_url = '';
}
// Perms
enforceUserPermission('module_sales');
$recurring_invoice_id = intval($_GET['recurring_invoice_id'] ?? 0);
$recurring = billingRow('recurring', $recurring_invoice_id);
if (!$recurring || ($client_url && intval($recurring['client_id']) !== intval($client_id))) {
billingNotFound('recurring', $client_url);
require_once "../includes/footer.php";
exit;
}
$client_id = intval($recurring['client_id']);
enforceClientAccess();
$number = billingNumber('recurring', $recurring);
$active = intval($recurring['recurring_invoice_status']) === 1;
$frequency = (string) $recurring['recurring_invoice_frequency'];
$frequency_label = recurringInvoiceFrequencies()[$frequency] ?? ucfirst($frequency);
$currency = (string) $recurring['recurring_invoice_currency_code'];
$money = function ($amount) use ($currency) {
return escapeHtml(billingMoney($amount, $currency));
};
$items = billingItems('recurring', $recurring_invoice_id);
$totals = billingTotals('recurring', $recurring, $items);
$history = billingHistory('recurring', $recurring_invoice_id);
$generated = recurringInvoiceGenerated($recurring_invoice_id, 8);
$generated_count = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM invoices WHERE invoice_recurring_invoice_id = $recurring_invoice_id"))[0]);
$upcoming = $active ? recurringInvoiceUpcoming($recurring, 4) : [];
$late = $active && $recurring['recurring_invoice_next_date'] < date('Y-m-d');
$late_days = $late ? intval(round((strtotime(date('Y-m-d')) - strtotime($recurring['recurring_invoice_next_date'])) / 86400)) : 0;
$period_word = recurringInvoicePeriodWord($frequency);
$next_period = recurringInvoicePeriod($recurring, $recurring['recurring_invoice_next_date']);
$after_next = recurringInvoiceAfter($recurring, $recurring['recurring_invoice_next_date']);
$seat_lines = recurringSeatLines($recurring_invoice_id, $client_id);
$seat_drift = count(array_filter($seat_lines, function ($line) { return $line['seat_drift']; }));
$seat_sources = recurringSeatSources();
$auto_send = intval($recurring['recurring_invoice_auto_send']) === 1;
// The contract it bills - it starts and stops with it
$for_contract = contractForRecurring($recurring_invoice_id);
$email_notify = intval($recurring['recurring_invoice_email_notify']) === 1;
$can_edit = lookupUserPermission('module_sales') >= 2;
$can_delete = lookupUserPermission('module_sales') >= 3;
$csrf = $_SESSION['csrf_token'];
// What it costs us - the recurring expenses it covers (functions/expenses.php)
$can_see_costs = lookupUserPermission('module_financial') >= 1;
$can_add_costs = lookupUserPermission('module_financial') >= 2;
$costs = $can_see_costs ? recurringInvoiceCosts($recurring_invoice_id) : null;
// Cards on file the client could be charged on
$saved_methods = [];
$sql = mysqli_query($mysqli, "SELECT saved_payment_id, saved_payment_description, payment_provider_name, payment_provider_active FROM client_saved_payment_methods
LEFT JOIN payment_providers ON payment_provider_id = saved_payment_provider_id
WHERE saved_payment_client_id = $client_id ORDER BY saved_payment_description ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$saved_methods[intval($row['saved_payment_id'])] = $row;
}
$autopay_id = intval($recurring['recurring_payment_saved_payment_id'] ?? 0);
// The card fee autopay adds, when the client pays one (functions/billing_pay.php)
$autopay_fee = billingCardFeeFor($client_id);
// Override Tab Title // No Sanitizing needed as this var will only be used in the tab title
$tab_title = $recurring['client_name'];
$page_title = $number;
$paper = [
'type' => 'recurring',
'row' => $recurring,
'items' => $items,
'totals' => $totals,
'edit' => $can_edit,
'notes_edit' => $can_edit,
'taxes' => $can_edit ? billingTaxes($recurring['recurring_invoice_created_at']) : [],
'logo_url' => billingCompany()['logo_url'],
'csrf' => $csrf,
];
?>
<ol class="breadcrumb d-print-none">
<li class="breadcrumb-item"><a href="client_overview.php?client_id=<?= $client_id ?>"><?= escapeHtml($recurring['client_name']) ?></a></li>
<li class="breadcrumb-item"><a href="recurring_invoices.php?client_id=<?= $client_id ?>">Recurring Invoices</a></li>
<li class="breadcrumb-item active"><?= escapeHtml($number) ?></li>
</ol>
<!-- What it is, whether it is running, and when it bills next -->
<div class="card mb-3 d-print-none" data-billing-live="head">
<div class="card-body d-flex flex-wrap align-items-center gap-3">
<?= billingTile('recurring', 'lg') ?>
<div class="flex-grow-1 min-w-0">
<h1 class="h4 fw-semibold mb-1 d-flex flex-wrap align-items-center gap-2">
<span>Recurring Invoice <?= escapeHtml($number) ?></span>
<span class="fs-6"><span class="badge rounded-pill text-bg-<?= $active ? 'success' : 'secondary' ?>"><?= $active ? 'Active' : 'Paused' ?></span></span>
<?php if ($active) { ?><span class="fs-6"><?= recurringInvoiceNextPill($recurring) ?></span><?php } ?>
</h1>
<div class="text-body-secondary small d-flex flex-wrap column-gap-3 row-gap-1">
<span><i class="fas fa-fw fa-building me-1"></i><a class="text-reset" href="recurring_invoices.php?client_id=<?= $client_id ?>"><?= escapeHtml($recurring['client_name']) ?></a></span>
<?php if (trim((string) $recurring['recurring_invoice_scope']) !== '') { ?><span><i class="fas fa-fw fa-comment me-1"></i><?= escapeHtml($recurring['recurring_invoice_scope']) ?></span><?php } ?>
<?php if ($recurring['category_name']) { ?><span><i class="fas fa-fw fa-tag me-1"></i><?= escapeHtml($recurring['category_name']) ?></span><?php } ?>
<span><i class="fas fa-fw fa-sync-alt me-1"></i><?= escapeHtml($frequency_label) ?></span>
<?php if (intval($recurring['recurring_invoice_quote_id'] ?? 0) && ($from_quote = billingRow('quote', intval($recurring['recurring_invoice_quote_id'])))) { ?>
<span><i class="fas fa-fw fa-comment-dollar me-1"></i>From <a class="text-reset" href="quote.php?quote_id=<?= intval($from_quote['quote_id']) ?>"><?= escapeHtml(billingNumber('quote', $from_quote)) ?></a></span>
<?php } ?>
<?php if ($autopay_id && isset($saved_methods[$autopay_id])) { ?><span><i class="fas fa-fw fa-wallet me-1"></i>Autopay</span><?php } ?>
<?php if ($for_contract) { ?>
<span title="It starts and stops with the contract" data-recurring-contract><i class="fas fa-fw fa-file-contract me-1"></i>For <a class="text-reset" href="contract.php?client_id=<?= $client_id ?>&contract_id=<?= intval($for_contract['contract_id']) ?>"><?= escapeHtml($for_contract['contract_name']) ?></a></span>
<?php } ?>
</div>
</div>
<div class="d-flex flex-wrap gap-2">
<?php if ($can_edit) { ?>
<a class="btn btn-primary confirm-link" href="post.php?force_recurring=<?= $recurring_invoice_id ?>&run=<?= escapeHtml(urlencode($recurring['recurring_invoice_next_date'])) ?>&csrf_token=<?= $csrf ?>"
data-confirm-title="Make an invoice from <?= escapeHtml($number) ?> now?"
data-confirm-text="The invoice for the run due <?= escapeHtml($recurring['recurring_invoice_next_date']) ?> (<?= escapeHtml($next_period['{period}']) ?>) is made now, dated today<?= $auto_send && $email_notify && !empty($config_recurring_auto_send_invoice) && !empty($config_smtp_provider) ? ', and emailed' : ', as a draft to check' ?>. The next one moves to <?= escapeHtml($after_next) ?>."
data-confirm-button="Invoice Now">
<i class="fas fa-bolt me-2"></i>Invoice Now
</a>
<?php if ($active) { ?>
<a class="btn btn-outline-secondary confirm-link" href="post.php?recurring_invoice_set_status=<?= $recurring_invoice_id ?>&status=0&csrf_token=<?= $csrf ?>"
data-confirm-title="Pause <?= escapeHtml($number) ?>?" data-confirm-text="No invoices are made from it until it is resumed." data-confirm-button="Pause">
<i class="fas fa-pause me-2"></i>Pause
</a>
<?php } else { ?>
<a class="btn btn-outline-success" href="post.php?recurring_invoice_set_status=<?= $recurring_invoice_id ?>&status=1&csrf_token=<?= $csrf ?>">
<i class="fas fa-play me-2"></i>Resume
</a>
<?php } ?>
<button type="button" class="btn btn-outline-secondary ajax-modal" data-modal-url="modals/recurring_invoice/recurring_invoice_edit.php?id=<?= $recurring_invoice_id ?>">
<i class="fas fa-edit me-2"></i>Edit
</button>
<?php } ?>
<div class="dropdown dropstart">
<button class="btn btn-outline-secondary" 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" href="#" onclick="window.print(); return false;">
<i class="fas fa-fw fa-print text-secondary me-2"></i>Print
</a>
<a class="dropdown-item" href="invoices.php?client_id=<?= $client_id ?>&recurring_invoice_id=<?= $recurring_invoice_id ?>">
<i class="fas fa-fw fa-file-invoice-dollar text-secondary me-2"></i>Invoices Made <span class="text-body-secondary">(<?= $generated_count ?>)</span>
</a>
<?php if ($can_edit && $auto_send) { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="post.php?recurring_invoice_email_notify=<?= $email_notify ? 0 : 1 ?>&recurring_invoice_id=<?= $recurring_invoice_id ?>&csrf_token=<?= $csrf ?>">
<i class="fas fa-fw <?= $email_notify ? 'fa-bell-slash' : 'fa-bell' ?> text-secondary me-2"></i><?= $email_notify ? "Don't email the invoices" : 'Email the invoices' ?>
</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_recurring_invoice=<?= $recurring_invoice_id ?>&csrf_token=<?= $csrf ?>"
data-confirm-text="<?= escapeHtml($number) ?> goes for good, with its lines, autopay and history. The invoices it made stay.">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
<?php } ?>
</div>
</div>
</div>
</div>
</div>
<?php if ($late) { ?>
<div class="alert alert-danger d-flex flex-wrap align-items-center gap-2 d-print-none">
<i class="fas fa-exclamation-triangle"></i>
<?php if ($late_days <= RECURRING_INVOICE_GRACE_DAYS) { ?>
<span class="me-auto">Its run on <?= escapeHtml($recurring['recurring_invoice_next_date']) ?> was missed - the next nightly run bills it. Invoice Now bills it straight away.</span>
<?php } else { ?>
<span class="me-auto">Its next date, <?= escapeHtml($recurring['recurring_invoice_next_date']) ?>, is <?= $late_days ?> days gone - too far behind to bill by itself. Use Invoice Now for each run it missed, or move it on without billing them.</span>
<?php } ?>
<?php if ($can_edit) { ?>
<a class="btn btn-sm btn-danger confirm-link" href="post.php?recurring_invoice_catch_up=<?= $recurring_invoice_id ?>&csrf_token=<?= $csrf ?>"
data-confirm-title="Skip the missed runs?" data-confirm-text="The next date moves on to <?= escapeHtml(recurringInvoiceCatchUp($recurring)) ?>. Nothing is billed for the runs it missed - use Invoice Now for those." data-confirm-button="Move on">
<i class="fas fa-forward me-1"></i>Move on to <?= escapeHtml(recurringInvoiceCatchUp($recurring)) ?>
</a>
<?php } ?>
</div>
<?php } ?>
<div data-billing-live="warning"><?= taxDocWarning('recurring', $recurring_invoice_id, $client_id, $items, true, $can_edit) ?></div>
<div class="row g-3">
<!-- The invoice it makes -->
<div class="col-xl-8 itflow-paper-col">
<?php require "../includes/billing_paper.php"; ?>
</div>
<div class="col-xl-4 d-print-none" data-billing-live="side">
<div class="card mb-3">
<div class="card-header">
<h3 class="card-title mb-0"><i class="fas fa-fw fa-calendar-alt text-secondary me-2"></i>Schedule</h3>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-end mb-3">
<div>
<div class="itflow-billing-stat-label">Each <?= $period_word ?></div>
<div class="itflow-billing-stat-value"><?= $money($totals['total']) ?></div>
</div>
<?php if ($frequency !== 'month') { ?>
<div class="text-end small text-body-secondary"><?= $money(recurringInvoiceMonthly($totals['total'], $frequency)) ?> a month</div>
<?php } ?>
</div>
<dl class="itflow-asset-dl mb-0">
<dt>Next</dt>
<dd>
<?php if ($active) { ?>
<?= escapeHtml($recurring['recurring_invoice_next_date']) ?>
<div class="small text-body-secondary">For <?= escapeHtml($next_period['{period}']) ?></div>
<?php if (count($upcoming) > 1 && !$late) { ?>
<div class="small text-body-secondary">then <?= escapeHtml(implode(', ', array_slice($upcoming, 1))) ?></div>
<?php } ?>
<?php } else { ?>
<span class="text-body-secondary"><?= $for_contract && $for_contract['contract_status'] === 'Draft' ? 'Paused - it starts when ' . escapeHtml($for_contract['contract_name']) . ' is active' : 'Paused - nothing until it is resumed' ?></span>
<?php } ?>
</dd>
<dt>Last</dt><dd><?= $recurring['recurring_invoice_last_sent'] ? escapeHtml($recurring['recurring_invoice_last_sent']) : '<span class="text-body-secondary">Not yet</span>' ?></dd>
<?php $margin_html = billingMarginHtml('recurring', $items, $currency, $period_word); if ($margin_html) { ?>
<dt>Margin</dt><dd><?= $margin_html ?></dd>
<?php } ?>
<?php $reprice_doc = $can_edit && empty($recurring['recurring_invoice_archived_at']) ? productRepriceCount(['kind' => 'recurring', 'doc_id' => $recurring_invoice_id]) : null; if ($reprice_doc && $reprice_doc['lines']) { ?>
<dt>Prices</dt>
<dd><a class="text-warning-emphasis" href="products_reprice.php?kind=recurring&amp;doc_id=<?= $recurring_invoice_id ?>"><i class="fas fa-tags me-1"></i><?= intval($reprice_doc['lines']) ?> line<?= $reprice_doc['lines'] == 1 ? '' : 's' ?> at an old price</a></dd>
<?php } ?>
<dt>Sending</dt>
<dd>
<?php if (!$auto_send) { ?>
Made as a draft for you to check and send
<?php } elseif (empty($config_recurring_auto_send_invoice)) { ?>
Made as a draft<div class="small text-body-secondary">Emailing the invoices recurring invoices make is off in Settings &gt; Notifications</div>
<?php } elseif ($email_notify && empty($config_smtp_provider)) { ?>
Made as a draft<div class="small text-body-secondary">Outgoing mail isn't set up</div>
<?php } elseif ($email_notify) { ?>
Emailed to the primary and billing contacts as it is made
<?php } else { ?>
Made as a draft<div class="small text-body-secondary">Emailing is off for this one</div>
<?php } ?>
</dd>
<dt>Since</dt><dd title="<?= escapeHtml($recurring['recurring_invoice_created_at']) ?>"><?= escapeHtml(date('Y-m-d', strtotime($recurring['recurring_invoice_created_at']))) ?></dd>
</dl>
</div>
</div>
<?php if ($seat_lines) { ?>
<!-- Per user, per device: what it bills and what the client has now -->
<div class="card mb-3">
<div class="card-header d-flex align-items-center">
<h3 class="card-title mb-0 me-auto"><i class="fas fa-fw fa-users text-secondary me-2"></i>Seats</h3>
<?php if ($can_edit && $seat_drift) { ?>
<a class="btn btn-sm btn-outline-warning" href="post.php?recurring_invoice_sync=<?= $recurring_invoice_id ?>&csrf_token=<?= $csrf ?>"><i class="fas fa-sync-alt me-1"></i>Update now</a>
<?php } ?>
</div>
<div class="list-group list-group-flush">
<?php foreach ($seat_lines as $seat) {
$source = $seat_sources[$seat['item_seat_source']] ?? ['icon' => 'fa-users', 'unit' => 'seat']; ?>
<div class="list-group-item d-flex align-items-center gap-2">
<span class="itflow-asset-icon itflow-asset-icon-sm itflow-billing-tile itflow-billing-tile-recurring" aria-hidden="true"><i class="fas fa-fw <?= $source['icon'] ?>"></i></span>
<div class="flex-grow-1 min-w-0">
<div class="text-truncate"><?= escapeHtml($seat['item_name']) ?></div>
<div class="small text-body-secondary text-truncate"><?= escapeHtml($seat['seat_label']) ?></div>
</div>
<div class="text-end text-nowrap">
<div class="fw-semibold"><?= billingQuantity($seat['item_quantity']) ?></div>
<?php if ($seat['seat_count'] === null) { ?>
<div class="small text-danger-emphasis">License gone</div>
<?php } elseif ($seat['seat_drift']) { ?>
<div class="small text-warning-emphasis">now <?= billingQuantity($seat['seat_quantity']) ?></div>
<?php } else { ?>
<div class="small text-body-secondary">up to date</div>
<?php } ?>
</div>
</div>
<?php } ?>
</div>
<div class="card-footer small text-body-secondary">Counted again each time an invoice is made<?= $seat_drift ? ' - the next one bills the new counts' : '' ?>.</div>
</div>
<?php } ?>
<?php if ($costs) { ?>
<!-- What it costs us: the recurring expenses it covers, and the margin left -->
<div class="card mb-3">
<div class="card-header d-flex align-items-center">
<h3 class="card-title mb-0 me-auto"><i class="fas fa-fw fa-shopping-cart text-secondary me-2"></i>Costs <span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis fw-normal ms-1"><?= count($costs['rows']) ?></span></h3>
<?php if ($can_add_costs) { ?>
<button type="button" class="btn btn-sm btn-outline-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/recurring_expense/recurring_expense_add.php?recurring_invoice_id=<?= $recurring_invoice_id ?>" title="A recurring expense this covers">
<i class="fas fa-plus me-1"></i>Add
</button>
<?php } ?>
</div>
<?php if ($costs['rows']) { ?>
<div class="list-group list-group-flush">
<?php foreach ($costs['rows'] as $cost) { ?>
<div class="list-group-item d-flex align-items-center gap-2">
<?= vendorTile($cost['vendor_name'] ?: 'Recurring', 'sm') ?>
<div class="flex-grow-1 min-w-0">
<a class="text-truncate d-block ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/recurring_expense/recurring_expense.php?id=<?= intval($cost['recurring_expense_id']) ?>"><?= escapeHtml($cost['vendor_name'] ?: 'No vendor') ?></a>
<div class="small text-body-secondary text-truncate"><?= escapeHtml(implode(' · ', array_filter([$cost['recurring_expense_description'], $cost['category_name']]))) ?: '&nbsp;' ?></div>
</div>
<div class="text-end text-nowrap small">
<div class="itflow-money <?= $cost['running'] ? '' : 'text-body-secondary text-decoration-line-through' ?>"><?= $money($cost['monthly']) ?></div>
<div class="text-body-secondary"><?= $cost['running'] ? 'a month' : ucfirst(recurringExpenseState($cost)) ?></div>
</div>
</div>
<?php } ?>
</div>
<div class="card-footer small">
<div class="d-flex justify-content-between gap-2">
<span class="text-body-secondary">Bills <?= $money($costs['revenue']) ?> a month before tax &middot; costs <?= $money($costs['cost']) ?></span>
<span class="fw-semibold text-nowrap <?= $costs['margin'] < 0 ? 'text-danger-emphasis' : 'text-success-emphasis' ?>"><?= $money($costs['margin']) ?><?= $costs['percent'] !== null ? ' &middot; ' . $costs['percent'] . '%' : '' ?></span>
</div>
</div>
<?php } else { ?>
<div class="card-body small text-body-secondary">
No costs against it yet. Add the recurring expenses it pays for - the licenses, hosting or backup it resells - to see the margin it leaves each month.
</div>
<?php } ?>
</div>
<?php } ?>
<div class="card mb-3">
<div class="card-header">
<h3 class="card-title mb-0"><i class="fas fa-fw fa-wallet text-secondary me-2"></i>Autopay</h3>
</div>
<div class="card-body">
<?php if ($saved_methods) { ?>
<form action="post.php" method="post">
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
<input type="hidden" name="set_recurring_payment" value="1">
<input type="hidden" name="recurring_invoice_id" value="<?= $recurring_invoice_id ?>">
<div class="input-group">
<span class="input-group-text"><i class="fas fa-fw fa-credit-card"></i></span>
<select class="form-select" name="saved_payment_id" onchange="this.form.submit()" <?= $can_edit ? '' : 'disabled' ?> aria-label="Card to charge">
<option value="0">Off - the client pays each invoice</option>
<?php foreach ($saved_methods as $saved_id => $method) { ?>
<option value="<?= $saved_id ?>" <?= $autopay_id === $saved_id ? 'selected' : '' ?>><?= escapeHtml($method['saved_payment_description']) ?><?= $method['payment_provider_name'] ? ' (' . escapeHtml($method['payment_provider_name']) . (intval($method['payment_provider_active']) ? '' : ', turned off') . ')' : '' ?></option>
<?php } ?>
</select>
</div>
</form>
<div class="form-text"><?= $autopay_id ? 'Charged on the due date of each invoice it makes.' : 'Charge a card on file on the due date of each invoice it makes.' ?><?= $autopay_fee ? ' With the ' . escapeHtml(billingCardFeeText($autopay_fee, $recurring['recurring_invoice_currency_code'] ?? '')) . ' card fee, as a line on the invoice.' : '' ?></div>
<?php } else { ?>
<div class="small text-body-secondary">No card on file for <?= escapeHtml($recurring['client_name']) ?>. Once they save one paying an invoice online, it can be charged here automatically.</div>
<?php } ?>
</div>
</div>
<div class="card mb-3">
<div class="card-header d-flex align-items-center">
<h3 class="card-title mb-0 me-auto"><i class="fas fa-fw fa-file-invoice-dollar text-secondary me-2"></i>Invoices Made <span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis fw-normal ms-1"><?= $generated_count ?></span></h3>
<?php if ($generated_count > count($generated)) { ?>
<a class="small" href="invoices.php?client_id=<?= $client_id ?>&recurring_invoice_id=<?= $recurring_invoice_id ?>">All</a>
<?php } ?>
</div>
<?php if ($generated) { ?>
<div class="list-group list-group-flush">
<?php foreach ($generated as $made) {
$made_state = invoiceState($made, $made['paid']); ?>
<a class="list-group-item list-group-item-action d-flex align-items-center gap-2" href="invoice.php?invoice_id=<?= intval($made['invoice_id']) ?>">
<div class="flex-grow-1 min-w-0">
<div class="fw-semibold"><?= escapeHtml($made['invoice_prefix'] . $made['invoice_number']) ?> <span class="fw-normal text-body-secondary small ms-1"><?= escapeHtml($made['invoice_date']) ?></span></div>
<div class="small"><?= invoiceStatusBadge($made['invoice_status'], $made_state['state'] === 'overdue') ?></div>
</div>
<span class="itflow-money small"><?= escapeHtml(billingMoney($made['invoice_amount'], $made['invoice_currency_code'])) ?></span>
</a>
<?php } ?>
</div>
<?php } else { ?>
<div class="card-body small text-body-secondary">None yet<?= $active ? ' - the first is made on ' . escapeHtml($recurring['recurring_invoice_next_date']) : '' ?>.</div>
<?php } ?>
</div>
<div class="card mb-3">
<div class="card-header">
<h3 class="card-title mb-0"><i class="fas fa-fw fa-history text-secondary me-2"></i>History</h3>
</div>
<?= billingHistoryHtml($history) ?>
</div>
</div>
</div>
<script src="<?= staticFileUrl('/js/inline_notes.js') ?>"></script>
<script src="<?= staticFileUrl('/libs/SortableJS/Sortable.min.js') ?>"></script>
<script src="<?= staticFileUrl('/js/billing_items.js') ?>"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
itflowBillingItems({
csrf: <?= json_encode($csrf) ?>,
currency: <?= json_encode($currency) ?>,
locale: <?= json_encode(str_replace('_', '-', (string) $session_company_locale)) ?>
});
});
</script>
<?php require_once "../includes/footer.php";