Files
itflow/agent/modals/client/client_statement.php
johnnyq 234afd25fb Contacts overhaul: rebuilt contacts list with role tabs and counts, avatars, flag pills, hover copy on phone/mobile/email, inline location picker, linked asset/credential/license/ticket chips and tag filter; contact page rebuilt to match the asset page with header actions, Reach/Details/Quick notes cards and tabbed Assets, Credentials, Tickets, Recurring, Licenses, Documents, Files, Services and Notes; add and edit contact share one tabbed form (Details, Roles, Access, Notes) with field icons, role switches, password generator, duplicate email check and photo removal; contact quick look redone to match the asset quick look; contact avatars now used everywhere a contact appears - contact pickers, ticket header, contact card, client replies, ticket list, kanban, assets, clients list, client header, overview key contacts, credentials, documents, services, invoice/quote/statement recipients, license users and global search - with each contact keeping the same colour throughout; ticket contact pickers show the name with title and role underneath; fixes: permission checks on the add contact modal and contact page/list actions, contact location limited to the contact's own client on add/edit/bulk assign, contacts list no longer selects user account columns, archived assets and credentials left off the contact page and quick look, saving a contact with the client portal disabled no longer clears their portal login method, replacing a photo no longer tries to delete a directory, sort column whitelisted, list counts and tags batched instead of per row, recurring ticket menu markup, undefined index notices, contacts list no longer scrolls sideways on phones, Edit Service shows avatars on unselected contacts and assets, license user labels toggle their checkbox; new functions/contact.php helpers (contactAvatar, contactLink, contactFlagPills, contactPhoneLink, contactAvatarData, contactOptionDescription), shared contact_form.php, contact_location quick pick, removed unused assetContactAvatar and clientOverviewPersonInitials
2026-10-14 18:33:18 -04:00

188 lines
7.3 KiB
PHP

<?php
require_once '../../../includes/modal_header.php';
enforceUserPermission('module_sales', 2);
$client_id = intval($_GET['client_id']);
enforceClientAccess();
$sql = mysqli_query($mysqli, "SELECT client_currency_code, client_name FROM clients WHERE client_id = $client_id LIMIT 1");
$row = mysqli_fetch_assoc($sql);
$client_name = escapeHtml($row['client_name']);
$client_currency_code = escapeHtml($row['client_currency_code']);
if (empty($client_currency_code)) {
$client_currency_code = $session_company_currency;
}
// What the client owes right now, shown as context so the agent knows roughly
// what is about to go out. Deliberately not filtered by the date range below -
// this is the standing balance, not a preview of the statement.
$row = mysqli_fetch_assoc(mysqli_query(
$mysqli,
"SELECT IFNULL(SUM(invoice_amount), 0) - IFNULL(SUM(amount_paid), 0) AS balance,
COUNT(invoice_id) AS invoice_count FROM invoices
LEFT JOIN (
SELECT payment_invoice_id, SUM(payment_amount) AS amount_paid FROM payments
WHERE payment_archived_at IS NULL
GROUP BY payment_invoice_id
) AS invoice_payments ON payment_invoice_id = invoice_id
WHERE invoice_client_id = $client_id
AND invoice_status NOT IN ('Draft', 'Cancelled', 'Non-Billable')"
));
$outstanding_balance = floatval($row['balance']);
// Statement recipients follow the invoice rules - a statement is a billing
// document, so the same people who would get the invoice get the statement.
$sql_contacts = mysqli_query(
$mysqli,
"SELECT contact_billing, contact_email, contact_id, contact_photo, contact_name, contact_primary, contact_title
FROM contacts
WHERE contact_client_id = $client_id
AND contact_archived_at IS NULL
AND contact_email IS NOT NULL
AND contact_email != ''
" . documentContactFilterSql('invoice') . "
ORDER BY contact_primary DESC, contact_billing DESC, contact_name ASC"
);
$contact_count = mysqli_num_rows($sql_contacts);
$default_contact_ids = [];
$sql_defaults = mysqli_query(
$mysqli,
"SELECT contact_id FROM contacts
WHERE contact_client_id = $client_id
AND contact_archived_at IS NULL
AND contact_email IS NOT NULL
AND contact_email != ''
" . documentDefaultContactFilterSql('invoice')
);
while ($row = mysqli_fetch_assoc($sql_defaults)) {
$default_contact_ids[] = intval($row['contact_id']);
}
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title text-white">
<i class="fa fa-fw fa-file-alt me-2"></i>Send Account Statement
<span class="text-muted ms-1"><?= $client_name ?></span>
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="client_id" value="<?= $client_id ?>">
<div class="modal-body">
<?php if ($contact_count == 0) { ?>
<p class="text-muted mb-0">
This client has no primary or billing contact with an email address, so there is
nobody to send a statement to. Flag a contact as billing first.
</p>
<?php } else { ?>
<div class="alert alert-secondary">
<i class="fa fa-fw fa-balance-scale me-2"></i>Current outstanding balance:
<strong><?= numfmt_format_currency($currency_format, $outstanding_balance, $client_currency_code) ?></strong>
</div>
<h6 class="text-bold">Statement Options</h6>
<div class="row g-2">
<div class="col-sm-6 mb-3">
<label>From</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
<input type="date" class="form-control" name="dtf" max="2999-12-31">
</div>
<small class="text-muted">Leave blank for all history</small>
</div>
<div class="col-sm-6 mb-3">
<label>To</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
<input type="date" class="form-control" name="dtt" max="2999-12-31" value="<?= date("Y-m-d") ?>">
</div>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="statementIncludePaid" name="include_paid" value="1">
<label class="form-check-label" for="statementIncludePaid">
Include paid invoices
<span class="text-muted">- off means outstanding items only</span>
</label>
</div>
</div>
<hr>
<h6 class="text-bold">Email Options</h6>
<label class="mb-2">Send to <strong class="text-danger">*</strong></label>
<div class="list-group mb-0">
<?php
while ($row = mysqli_fetch_assoc($sql_contacts)) {
$contact_id = intval($row['contact_id']);
$contact_name = escapeHtml($row['contact_name']);
$contact_email = escapeHtml($row['contact_email']);
$contact_title = escapeHtml($row['contact_title']);
$contact_checked = in_array($contact_id, $default_contact_ids, true);
?>
<label class="list-group-item d-flex align-items-center gap-2" for="statementContact<?= $contact_id ?>">
<input type="checkbox" class="form-check-input flex-shrink-0 m-0" name="contacts[]"
id="statementContact<?= $contact_id ?>" value="<?= $contact_id ?>"
<?php if ($contact_checked) { echo "checked"; } ?>>
<?= contactAvatar($client_id, $contact_id, $row['contact_name'], $row['contact_photo'], 'sm') ?>
<span class="min-w-0">
<span class="d-flex flex-wrap align-items-center column-gap-1">
<strong><?= $contact_name ?></strong>
<?= contactFlagPills($row, false) ?>
<?php if ($contact_title) { ?><span class="text-body-secondary small"><?= $contact_title ?></span><?php } ?>
</span>
<span class="d-block small text-body-secondary text-truncate"><?= $contact_email ?></span>
</span>
</label>
<?php
}
?>
</div>
<?php } ?>
</div>
<div class="modal-footer">
<?php if ($contact_count > 0) { ?>
<button type="submit" name="send_statement" class="btn btn-primary text-bold">
<i class="fa fa-fw fa-paper-plane me-2"></i>Send Statement
</button>
<?php } ?>
<button type="button" class="btn btn-light" data-bs-dismiss="modal">
<i class="fa fa-fw fa-times me-2"></i>Cancel
</button>
</div>
</form>
<?php
require_once '../../../includes/modal_footer.php';