Files
itflow/agent/vendor.php
johnnyq 9e55d52432 Vendors overhaul for daily MSP work: new vendor page built around calling them, with Call support (dials the extension), Email, Support site, a star, and a New menu (case/ticket with them, license from them, asset bought from them, portal login, document, upload, or expense and recurring expense for our own vendors), Support card (phone, hours, SLA, email, site), Account card (account number, PIN shown as dots with show and copy, account manager name, direct line and email), blur-to-save notes, an Also used by list of the agent's other clients with the same vendor by name or template, and tabs for Cases (open first, with their ticket number), Licenses (soonest renewal first), Assets (serial, order ref, warranty status), Domains (registrar, web host, DNS, mail), Services, Logins, Documents, Files and History; our own vendors show spend this year, last year and in all, recent expenses and recurring expenses; new vendor quick look; rebuilt vendors list with starred first, initials tiles coloured by vendor name, support line, hours and SLA, account number and PIN, chips for open cases, licenses, assets, domains, services and logins, an Ours / Clients' switch with an all-clients view in All Client Docs, wider search (account manager, emails, tags), tag filter and bulk Tags/Archive/Restore/Delete; shared tabbed vendor form (Details, Support, Account, Notes) with star, tags and start from template that fills every field; Add a Template to Clients to roll one vendor out to many clients, skipping those that have it; Admin Vendor Templates shows how many clients use each template and has Add to clients; vendor tags (tag type 6) with vendor tags in the Tags starter pack; ticket vendor block shows their ticket number, support line, hours, SLA, account number and PIN with copy buttons; vendor tiles in every vendor picker and quick look links across tickets, licenses, assets, domains, services and expense lists; search opens the vendor page; CSV export adds tags, open cases, account manager and PIN; logins, documents and files can be linked to a vendor and ones made from a vendor's page are linked to it; demo vendors get tags, a star and an account manager; database update 2.8.2 adds vendor_tags and vendor_contact_phone_country_code, vendor_contact_phone, vendor_contact_email (db.sql updated) and clears references to vendors that no longer exist; ticket vendor now validated against the ticket's client and removing a vendor from a ticket is logged as such; deleting a vendor from a template no longer unlinks every other vendor from that template; deleting a vendor clears it from assets, licenses, tickets and domains, and vendors with expenses, recurring expenses or payment provider fees are archived rather than deleted; vendor websites without a scheme open correctly; domain form vendor names escaped; new vendor templates keep the phone country code and pushing a template update with nothing ticked no longer errors; vendors API accepts the account manager fields; new functions/vendor.php (VENDOR_USES, vendorCan/vendorEnforce with client vendors on module_client and ours on module_financial, tile, link, website, PIN, form, tags, counts, delete check, reference clearing, also used by, spend, history), credential|vendor and file|vendor link relations, tag module_ours for our own vendors, handlers add_vendor_template_to_clients, favorite_vendor, unfavorite_vendor and ajax vendor_set_notes, vendor_model.php removed
2026-10-14 22:49:05 -04:00

823 lines
59 KiB
PHP

<?php
/*
* Vendor - one of a client's vendors, or one of ours.
*
* Built for the call to them: the support line, hours and SLA, the account
* number and PIN they will ask for (the PIN as dots until it is wanted), the
* account manager, and the notes - escalation path, who may call - always in
* view. Then, for a client's vendor, what is open with them and what came
* from them in tabs with counts: cases (tickets with their reference, open
* first), licenses, assets bought from them, domains they host, services,
* portal logins, documents, files and history. For one of ours: what we spend
* with them and the recurring expenses. "Also used by" lists the agent's
* other clients with the same vendor - who else an outage hits.
*
* The open tab follows the URL hash (#cases) and is remembered between visits.
*/
// 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";
} else {
require_once "includes/inc_all.php";
}
$vendor_id = intval($_GET['vendor_id'] ?? 0);
$vendor = vendorRow($vendor_id);
$vendor_client_id = $vendor ? intval($vendor['vendor_client_id']) : 0;
if (!$vendor || (isset($_GET['client_id']) && $vendor_client_id !== intval($client_id)) || !vendorCan($vendor_client_id, 1)) {
echo "<center><h1 class='text-secondary mt-5'>Nothing to see here</h1><a class='btn btn-lg btn-secondary mt-3' href='javascript:history.back()'><i class='fa fa-fw fa-arrow-left'></i> Go Back</a></center>";
} else {
$client_id = $vendor_client_id;
if ($client_id) {
enforceUserPermission('module_client');
enforceClientAccess();
}
$is_ours = !$client_id;
$vendor_name = escapeHtml($vendor['vendor_name']);
$vendor_description = escapeHtml($vendor['vendor_description']);
$vendor_archived_at = escapeHtml($vendor['vendor_archived_at']);
$vendor_created_at = escapeHtml($vendor['vendor_created_at']);
$vendor_favorite = intval($vendor['vendor_favorite']);
$vendor_phone = contactPhoneLink($vendor['vendor_phone'], $vendor['vendor_phone_country_code'], $vendor['vendor_extension']);
$vendor_phone_text = $vendor['vendor_phone'] ? formatPhoneNumber($vendor['vendor_phone'], $vendor['vendor_phone_country_code']) : '';
$vendor_dial = vendorDialNumber($vendor['vendor_phone'], $vendor['vendor_phone_country_code']);
$vendor_email = escapeHtml($vendor['vendor_email']);
$vendor_website_url = vendorWebsiteUrl($vendor['vendor_website']);
$vendor_website_label = escapeHtml(vendorWebsiteLabel($vendor['vendor_website']));
$vendor_hours = escapeHtml($vendor['vendor_hours']);
$vendor_sla = escapeHtml($vendor['vendor_sla']);
$vendor_account = escapeHtml($vendor['vendor_account_number']);
$vendor_pin = vendorSecretHtml($vendor['vendor_code']);
$vendor_notes = escapeHtml($vendor['vendor_notes']);
$manager_name = escapeHtml($vendor['vendor_contact_name']);
$manager_phone = contactPhoneLink($vendor['vendor_contact_phone'], $vendor['vendor_contact_phone_country_code']);
$manager_email = escapeHtml($vendor['vendor_contact_email']);
$can_edit = vendorCan($client_id, 2);
$can_delete = vendorCan($client_id, 3) && !empty($config_destructive_deletes_enable);
$can_support = lookupUserPermission('module_support') >= 1;
$can_support_edit = lookupUserPermission('module_support') >= 2;
$can_credentials = lookupUserPermission('module_credential') >= 1;
$can_credentials_edit = lookupUserPermission('module_credential') >= 2;
$can_financial = lookupUserPermission('module_financial') >= 1;
$can_financial_edit = lookupUserPermission('module_financial') >= 2;
// Override Tab Title // No Sanitizing needed as this var will only be used in the tab title
$page_title = $vendor['vendor_name'];
$list_url = $is_ours ? 'vendors.php' : "vendors.php?client_id=$client_id";
$vendor_tags_display = tagPills('vendor', $vendor_id, tagsForRecord('vendor', $vendor_id), ($is_ours ? 'vendors.php?' : "vendors.php?client_id=$client_id&"), $can_edit);
// Cases - every open one, then the most recent closed ones
$cases = [];
$case_count = $case_open_count = 0;
$licenses = $assets = $domains = $services = $logins = $documents = $files = [];
if ($can_support) {
$row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*), COALESCE(SUM(ticket_resolved_at IS NULL AND ticket_closed_at IS NULL), 0) FROM tickets WHERE ticket_client_id = $client_id AND ticket_vendor_id = $vendor_id"));
[$case_count, $case_open_count] = [intval($row[0]), intval($row[1])];
$sql = mysqli_query($mysqli, "SELECT ticket_id, ticket_prefix, ticket_number, ticket_subject, ticket_vendor_ticket_number, ticket_created_at, ticket_updated_at,
ticket_resolved_at, ticket_closed_at, ticket_assigned_to, user_name, user_avatar, ticket_status_name, ticket_status_color
FROM tickets
LEFT JOIN users ON ticket_assigned_to = user_id
LEFT JOIN ticket_statuses ON ticket_status_id = ticket_status
WHERE ticket_client_id = $client_id AND ticket_vendor_id = $vendor_id
ORDER BY (ticket_resolved_at IS NULL AND ticket_closed_at IS NULL) DESC, ticket_number DESC
LIMIT " . max(25, $case_open_count));
while ($row = mysqli_fetch_assoc($sql)) {
$cases[] = $row;
}
if (!$is_ours) {
$sql = mysqli_query($mysqli, "SELECT software_id AS id, software_name AS name, software_type, software_version, software_license_type,
software_seats, software_expire, software_purchase_reference, " . softwareSeatsUsedSql('software.software_id') . " AS seats_used
FROM software
WHERE software_client_id = $client_id AND software_vendor_id = $vendor_id AND software_archived_at IS NULL
ORDER BY software_expire IS NULL, software_expire ASC, software_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$licenses[] = $row;
}
$sql = mysqli_query($mysqli, "SELECT asset_id, asset_name, asset_type, asset_make, asset_model, asset_serial, asset_purchase_date, asset_purchase_reference, asset_warranty_expire
FROM assets
WHERE asset_client_id = $client_id AND asset_vendor_id = $vendor_id AND asset_archived_at IS NULL
ORDER BY asset_warranty_expire IS NULL, asset_warranty_expire ASC, asset_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$assets[] = $row;
}
$sql = mysqli_query($mysqli, "SELECT domain_id, domain_name, domain_description, domain_expire, domain_registrar, domain_webhost, domain_dnshost, domain_mailhost
FROM domains
WHERE domain_client_id = $client_id AND domain_archived_at IS NULL
AND $vendor_id IN (domain_registrar, domain_webhost, domain_dnshost, domain_mailhost)
ORDER BY domain_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$domains[] = $row;
}
$sql = mysqli_query($mysqli, "SELECT services.service_id, service_name, service_description, service_category, service_importance
FROM service_vendors JOIN services ON services.service_id = service_vendors.service_id
WHERE service_vendors.vendor_id = $vendor_id AND service_client_id = $client_id
ORDER BY service_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$services[] = $row;
}
$documents = linkedRecords('vendor', $vendor_id, 'document');
$files = linkedRecords('vendor', $vendor_id, 'file');
}
}
if ($can_credentials && !$is_ours) {
$logins = linkedRecords('vendor', $vendor_id, 'credential');
}
// What we spend with one of ours
$expenses = $recurring = [];
$spend = null;
if ($is_ours && $can_financial) {
$spend = vendorSpend($vendor_id);
$sql = mysqli_query($mysqli, "SELECT expense_id, expense_date, expense_amount, expense_currency_code, expense_description, expense_reference, category_name, account_name
FROM expenses
LEFT JOIN categories ON category_id = expense_category_id
LEFT JOIN accounts ON account_id = expense_account_id
WHERE expense_vendor_id = $vendor_id AND expense_archived_at IS NULL
ORDER BY expense_date DESC, expense_id DESC
LIMIT 25");
while ($row = mysqli_fetch_assoc($sql)) {
$expenses[] = $row;
}
$sql = mysqli_query($mysqli, "SELECT recurring_expense_id, recurring_expense_frequency, recurring_expense_next_date, recurring_expense_status, recurring_expense_amount,
recurring_expense_currency_code, recurring_expense_description, category_name
FROM recurring_expenses
LEFT JOIN categories ON category_id = recurring_expense_category_id
WHERE recurring_expense_vendor_id = $vendor_id AND recurring_expense_archived_at IS NULL
ORDER BY recurring_expense_status DESC, recurring_expense_next_date ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$recurring[] = $row;
}
}
$elsewhere = lookupUserPermission('module_client') >= 1 ? vendorElsewhere($vendor) : [];
$history = vendorHistory($vendor_id, 100);
if ($is_ours) {
$vendor_tabs = [
'expenses' => ['label' => 'Expenses', 'icon' => 'fa-receipt', 'count' => $spend ? $spend['count'] : 0],
'recurring' => ['label' => 'Recurring', 'icon' => 'fa-redo-alt', 'count' => count($recurring)],
'cases' => ['label' => 'Cases', 'icon' => 'fa-life-ring', 'count' => $case_count, 'badge' => $case_open_count],
'history' => ['label' => 'History', 'icon' => 'fa-history', 'count' => count($history)],
];
if (!$can_financial) {
unset($vendor_tabs['expenses'], $vendor_tabs['recurring']);
}
if (!$case_count) {
unset($vendor_tabs['cases']);
}
} else {
$vendor_tabs = [
'cases' => ['label' => 'Cases', 'icon' => 'fa-life-ring', 'count' => $case_count, 'badge' => $case_open_count],
'licenses' => ['label' => 'Licenses', 'icon' => 'fa-cube', 'count' => count($licenses)],
'assets' => ['label' => 'Assets', 'icon' => 'fa-desktop', 'count' => count($assets)],
'domains' => ['label' => 'Domains', 'icon' => 'fa-globe', 'count' => count($domains)],
'services' => ['label' => 'Services', 'icon' => 'fa-stream', 'count' => count($services)],
'logins' => ['label' => 'Logins', 'icon' => 'fa-key', 'count' => count($logins)],
'documents' => ['label' => 'Documents', 'icon' => 'fa-file-alt', 'count' => count($documents)],
'files' => ['label' => 'Files', 'icon' => 'fa-paperclip', 'count' => count($files)],
'history' => ['label' => 'History', 'icon' => 'fa-history', 'count' => count($history)],
];
if (!$can_support) {
unset($vendor_tabs['cases'], $vendor_tabs['licenses'], $vendor_tabs['assets'], $vendor_tabs['domains'], $vendor_tabs['services'], $vendor_tabs['documents'], $vendor_tabs['files']);
}
if (!$can_credentials) {
unset($vendor_tabs['logins']);
}
}
$link_url = "modals/link/link.php?from=vendor&id=$vendor_id&to=";
$unlink_url = function ($to, $other_id) use ($vendor_id) {
return "post.php?unlink_item&from=vendor&id=$vendor_id&to=$to&other=" . intval($other_id) . "&csrf_token=" . $_SESSION['csrf_token'];
};
$new_case_url = "modals/ticket/ticket_add.php?client_id=$client_id&vendor_id=$vendor_id";
$can_new = !$vendor_archived_at && (($is_ours && $can_financial_edit) || (!$is_ours && ($can_support_edit || $can_credentials_edit)));
?>
<ol class="breadcrumb d-print-none">
<?php if ($is_ours) { ?>
<li class="breadcrumb-item"><a href="vendors.php">Vendors</a></li>
<?php } else { ?>
<li class="breadcrumb-item"><a href="client_overview.php?client_id=<?= $client_id ?>"><?= escapeHtml($vendor['client_name']) ?></a></li>
<li class="breadcrumb-item"><a href="vendors.php?client_id=<?= $client_id ?>">Vendors</a></li>
<?php } ?>
<li class="breadcrumb-item active"><?= $vendor_name ?></li>
</ol>
<!-- Who they are and how to reach them -->
<div class="card mb-3">
<div class="card-body d-flex flex-wrap align-items-center gap-3">
<?= vendorTile($vendor['vendor_name'], '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><?= $vendor_name ?></span>
<?php if ($can_edit && !$vendor_archived_at) { ?>
<a class="text-decoration-none fs-6" href="post.php?<?= $vendor_favorite ? 'unfavorite_vendor' : 'favorite_vendor' ?>=<?= $vendor_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>" title="<?= $vendor_favorite ? 'Unstar' : 'Star - starred vendors come first' ?>">
<i class="<?= $vendor_favorite ? 'fas text-warning' : 'far text-body-secondary' ?> fa-star"></i>
</a>
<?php } elseif ($vendor_favorite) { ?>
<i class="fas fa-star text-warning fs-6" title="Starred"></i>
<?php } ?>
<?php if ($is_ours) { ?><span class="badge rounded-pill text-bg-dark fw-normal fs-6">Our vendor</span><?php } ?>
<?php if ($vendor_archived_at) { ?><span class="badge rounded-pill text-bg-secondary fw-normal fs-6">Archived</span><?php } ?>
</h1>
<div class="text-body-secondary small d-flex flex-wrap column-gap-3 row-gap-1">
<?php if ($vendor_description) { ?><span><?= $vendor_description ?></span><?php } ?>
<?php if ($vendor_hours) { ?><span><i class="fas fa-fw fa-clock me-1"></i><?= $vendor_hours ?></span><?php } ?>
<?php if ($vendor['vendor_template_name']) { ?><span title="Made from this template"><i class="fas fa-fw fa-puzzle-piece me-1"></i><?= escapeHtml($vendor['vendor_template_name']) ?></span><?php } ?>
</div>
<?php if ($vendor_tags_display) { ?>
<div class="mt-2"><?= $vendor_tags_display ?></div>
<?php } ?>
</div>
<div class="d-flex flex-wrap gap-2 d-print-none">
<?php if ($vendor_dial) { ?>
<a class="btn btn-outline-primary" href="tel:<?= escapeHtml($vendor_dial) ?><?= $vendor['vendor_extension'] ? ',' . escapeHtml($vendor['vendor_extension']) : '' ?>" title="<?= escapeHtml($vendor_phone_text) ?>"><i class="fas fa-phone me-2"></i>Call support</a>
<?php } ?>
<?php if ($vendor_email) { ?>
<a class="btn btn-outline-primary" href="mailto:<?= $vendor_email ?>"><i class="fas fa-envelope me-2"></i>Email</a>
<?php } ?>
<?php if ($vendor_website_url) { ?>
<a class="btn btn-outline-primary" href="<?= escapeHtml($vendor_website_url) ?>" target="_blank" rel="noopener"><i class="fas fa-external-link-alt me-2"></i>Support site</a>
<?php } ?>
<?php if ($can_new) { ?>
<div class="dropdown">
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown"><i class="fas fa-plus me-2"></i>New</button>
<div class="dropdown-menu dropdown-menu-end">
<?php if ($is_ours) { ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/expense/expense_add.php?vendor_id=<?= $vendor_id ?>">
<i class="fa fa-fw fa-receipt text-secondary me-2"></i>Expense
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/recurring_expense/recurring_expense_add.php?vendor_id=<?= $vendor_id ?>">
<i class="fa fa-fw fa-redo-alt text-secondary me-2"></i>Recurring expense
</a>
<?php } else { ?>
<?php if ($can_support_edit) { ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="<?= $new_case_url ?>">
<i class="fa fa-fw fa-life-ring text-secondary me-2"></i>Case with them
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/software/software_add.php?client_id=<?= $client_id ?>&vendor_id=<?= $vendor_id ?>">
<i class="fa fa-fw fa-cube text-secondary me-2"></i>License from them
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/asset/asset_add.php?client_id=<?= $client_id ?>&vendor_id=<?= $vendor_id ?>">
<i class="fa fa-fw fa-desktop text-secondary me-2"></i>Asset bought from them
</a>
<?php } ?>
<?php if ($can_credentials_edit) { ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/credential/credential_add.php?client_id=<?= $client_id ?>&vendor_id=<?= $vendor_id ?>">
<i class="fa fa-fw fa-key text-secondary me-2"></i>Portal login
</a>
<?php } ?>
<?php if ($can_support_edit) { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item ajax-modal" href="#" data-modal-size="xl" data-modal-url="modals/document/document_add.php?client_id=<?= $client_id ?>&vendor_id=<?= $vendor_id ?>">
<i class="fa fa-fw fa-file-alt text-secondary me-2"></i>Document
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/file/file_upload.php?client_id=<?= $client_id ?>&vendor_id=<?= $vendor_id ?>">
<i class="fa fa-fw fa-upload text-secondary me-2"></i>Upload File(s)
</a>
<?php } ?>
<?php } ?>
</div>
</div>
<?php } ?>
<?php if ($can_edit) { ?>
<button type="button" class="btn btn-outline-secondary ajax-modal" data-modal-size="lg" data-modal-url="modals/vendor/vendor_edit.php?id=<?= $vendor_id ?>">
<i class="fas fa-edit me-2"></i>Edit
</button>
<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">
<?php if ($vendor_archived_at) { ?>
<a class="dropdown-item" href="post.php?restore_vendor=<?= $vendor_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-redo text-secondary me-2"></i>Restore
</a>
<?php if ($can_delete) { ?>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_vendor=<?= $vendor_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"
data-confirm-text="<?= $vendor_name ?> goes for good. Its cases, licenses, assets and domains stay, from no vendor.">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
<?php } ?>
<?php } else { ?>
<a class="dropdown-item text-danger confirm-link" href="post.php?archive_vendor=<?= $vendor_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-archive me-2"></i>Archive
</a>
<?php } ?>
</div>
</div>
<?php } ?>
</div>
</div>
</div>
<div class="row g-3">
<!-- Calling them, what they will ask for, who looks after us -->
<div class="col-xl-4 col-lg-5">
<div class="card mb-3">
<div class="card-header">
<h3 class="card-title mb-0"><i class="fas fa-fw fa-headset text-secondary me-2"></i>Support</h3>
</div>
<div class="card-body">
<dl class="itflow-asset-dl mb-0">
<dt>Phone</dt>
<dd>
<?php if ($vendor_phone) { ?>
<?= $vendor_phone ?><button type="button" class="itflow-copy clipboardjs" title="Copy phone number" data-clipboard-text="<?= escapeHtml($vendor_phone_text) ?>"><i class="far fa-copy"></i></button>
<?php } else { ?>
<span class="text-body-secondary">Not recorded</span>
<?php } ?>
</dd>
<dt>Hours</dt>
<dd><?= $vendor_hours ?: '<span class="text-body-secondary">Not recorded</span>' ?></dd>
<?php if ($vendor_sla) { ?>
<dt>SLA</dt><dd><?= $vendor_sla ?></dd>
<?php } ?>
<?php if ($vendor_email) { ?>
<dt>Email</dt>
<dd class="text-break"><a href="mailto:<?= $vendor_email ?>"><?= $vendor_email ?></a><button type="button" class="itflow-copy clipboardjs" title="Copy email" data-clipboard-text="<?= $vendor_email ?>"><i class="far fa-copy"></i></button></dd>
<?php } ?>
<?php if ($vendor_website_url) { ?>
<dt>Site</dt>
<dd class="text-break"><a href="<?= escapeHtml($vendor_website_url) ?>" target="_blank" rel="noopener"><?= $vendor_website_label ?><i class="fas fa-external-link-alt ms-1 small"></i></a></dd>
<?php } ?>
</dl>
</div>
</div>
<div class="card mb-3">
<div class="card-header">
<h3 class="card-title mb-0"><i class="fas fa-fw fa-id-card text-secondary me-2"></i>Account</h3>
</div>
<div class="card-body">
<dl class="itflow-asset-dl mb-0">
<dt>Account</dt>
<dd><?= $vendor_account ? '<span class="font-monospace">' . $vendor_account . '</span><button type="button" class="itflow-copy clipboardjs" title="Copy account number" data-clipboard-text="' . $vendor_account . '"><i class="far fa-copy"></i></button>' : '<span class="text-body-secondary">Not recorded</span>' ?></dd>
<dt>PIN</dt>
<dd><?= $vendor_pin ?: '<span class="text-body-secondary">Not recorded</span>' ?></dd>
<dt>Manager</dt>
<dd>
<?php if ($manager_name || $manager_phone || $manager_email) { ?>
<?php if ($manager_name) { ?><div class="fw-semibold"><?= $manager_name ?></div><?php } ?>
<?php if ($manager_phone) { ?><div class="d-flex align-items-baseline"><i class="fas fa-fw fa-phone text-body-secondary me-1 flex-shrink-0"></i><span><?= $manager_phone ?></span></div><?php } ?>
<?php if ($manager_email) { ?><div class="d-flex align-items-baseline min-w-0"><i class="fas fa-fw fa-envelope text-body-secondary me-1 flex-shrink-0"></i><a class="text-truncate" href="mailto:<?= $manager_email ?>" title="<?= $manager_email ?>"><?= $manager_email ?></a><button type="button" class="itflow-copy clipboardjs flex-shrink-0" title="Copy email" data-clipboard-text="<?= $manager_email ?>"><i class="far fa-copy"></i></button></div><?php } ?>
<?php } else { ?>
<span class="text-body-secondary">Not recorded</span>
<?php } ?>
</dd>
<dt>Added</dt><dd title="<?= $vendor_created_at ?>"><?= timeAgo($vendor_created_at) ?></dd>
</dl>
</div>
</div>
<div class="card mb-3">
<div class="card-header">
<h3 class="card-title mb-0"><i class="fas fa-fw fa-sticky-note text-secondary me-2"></i>Notes</h3>
</div>
<textarea class="form-control border-0 rounded-0 rounded-bottom itflow-vendor-notes" rows="7" placeholder="Escalation path, who is authorised to call them, circuit IDs, contract renewal - saves when you click away"
data-vendor-id="<?= $vendor_id ?>" data-csrf="<?= escapeHtml($_SESSION['csrf_token']) ?>" <?= $can_edit ? '' : 'readonly' ?>><?= $vendor_notes ?></textarea>
</div>
<?php if ($elsewhere) { ?>
<div class="card mb-3">
<div class="card-header">
<h3 class="card-title mb-0"><i class="fas fa-fw fa-users text-secondary me-2"></i>Also used by <span class="badge rounded-pill text-bg-secondary ms-1"><?= count($elsewhere) ?></span></h3>
</div>
<ul class="list-group list-group-flush">
<?php foreach ($elsewhere as $other) {
$other_client_id = intval($other['vendor_client_id']); ?>
<li class="list-group-item d-flex align-items-center gap-2 py-2">
<a class="text-truncate flex-grow-1" href="vendor.php?client_id=<?= $other_client_id ?>&vendor_id=<?= intval($other['vendor_id']) ?>"><?= escapeHtml($other['client_name']) ?></a>
<?php if ($other['vendor_account_number']) { ?><span class="small text-body-secondary font-monospace text-nowrap" title="Their account number"><?= escapeHtml($other['vendor_account_number']) ?></span><?php } ?>
</li>
<?php } ?>
</ul>
<div class="card-footer small text-body-secondary">Clients with a vendor of the same name or template - who else an outage hits.</div>
</div>
<?php } ?>
</div>
<!-- What is open with them and what came from them -->
<div class="col-xl-8 col-lg-7">
<div class="card mb-3 itflow-asset-related">
<div class="card-header py-2">
<ul class="nav nav-pills itflow-asset-pills" role="tablist">
<?php foreach ($vendor_tabs as $tab_key => $tab) { ?>
<li class="nav-item" role="presentation">
<button class="nav-link text-nowrap <?= $tab['count'] ? '' : 'is-empty' ?>" id="vendor-tab-<?= $tab_key ?>" data-bs-toggle="pill" data-bs-target="#vendor-pane-<?= $tab_key ?>" data-vendor-tab="<?= $tab_key ?>" type="button" role="tab">
<i class="fas fa-fw <?= $tab['icon'] ?> me-1 d-none d-xxl-inline"></i><?= $tab['label'] ?>
<span class="badge rounded-pill ms-1"><?= $tab['count'] ?></span>
<?php if (!empty($tab['badge'])) { ?>
<span class="badge rounded-pill text-bg-danger" title="Open"><?= intval($tab['badge']) ?> open</span>
<?php } ?>
</button>
</li>
<?php } ?>
</ul>
</div>
<div class="tab-content">
<?php if (isset($vendor_tabs['cases'])) { ?>
<!-- Cases - tickets logged with this vendor, open first -->
<div class="tab-pane fade" id="vendor-pane-cases" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Tickets waiting on or worked with them, open first - with their reference.</span>
<?php if ($can_support_edit && !$vendor_archived_at) { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-size="lg" data-modal-url="<?= $new_case_url ?>"><i class="fas fa-plus me-2"></i>New Case</button>
<?php } ?>
</div>
<?php if ($cases) { ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="text-nowrap table-light"><tr><th>Ticket</th><th>Subject</th><th>Their ref</th><th>Status</th><th>Assigned</th><th>Updated</th></tr></thead>
<tbody>
<?php foreach ($cases as $case) {
$ticket_id = intval($case['ticket_id']);
$ticket_is_open = empty($case['ticket_resolved_at']) && empty($case['ticket_closed_at']);
$ticket_last = $case['ticket_updated_at'] ?: $case['ticket_created_at'];
$their_ref = escapeHtml($case['ticket_vendor_ticket_number']);
$ticket_url = "ticket.php?" . ($client_id ? "client_id=$client_id&" : '') . "ticket_id=$ticket_id"; ?>
<tr class="<?= $ticket_is_open ? '' : 'text-body-secondary' ?>">
<td class="text-nowrap"><a class="font-monospace" href="<?= $ticket_url ?>"><?= escapeHtml($case['ticket_prefix']) . intval($case['ticket_number']) ?></a></td>
<td><a class="<?= $ticket_is_open ? 'text-body fw-semibold' : 'text-body-secondary' ?>" href="<?= $ticket_url ?>"><?= escapeHtml($case['ticket_subject']) ?></a></td>
<td class="text-nowrap"><?php if ($their_ref) { ?><span class="font-monospace"><?= $their_ref ?></span><button type="button" class="itflow-copy clipboardjs" title="Copy their reference" data-clipboard-text="<?= $their_ref ?>"><i class="far fa-copy"></i></button><?php } else { ?><span class="text-body-secondary">-</span><?php } ?></td>
<td class="text-nowrap"><?= ticketStatusHtml($case['ticket_status_name'], $case['ticket_status_color'], 'detail') ?></td>
<td class="text-nowrap"><?= formatAssignee($case['ticket_assigned_to'], $case['user_name'], $case['user_avatar'], $ticket_is_open) ?></td>
<td class="text-nowrap small" title="<?= escapeHtml($ticket_last) ?>"><?= timeAgo($ticket_last) ?></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php if ($case_count > count($cases)) { ?>
<div class="small text-body-secondary px-3 py-2 border-top">Showing every open case and the <?= count($cases) - $case_open_count ?> most recent closed of <?= $case_count ?>.</div>
<?php } ?>
<?php } else { ?>
<div class="itflow-asset-empty">No cases with them. Set the vendor on a ticket, with their reference, when you log a call with them.</div>
<?php } ?>
</div>
<?php } ?>
<?php if (isset($vendor_tabs['licenses'])) { ?>
<!-- Licenses bought through them - soonest renewal first -->
<div class="tab-pane fade" id="vendor-pane-licenses" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Licenses and subscriptions bought through them, soonest renewal first.</span>
<?php if ($can_support_edit && !$vendor_archived_at) { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/software/software_add.php?client_id=<?= $client_id ?>&vendor_id=<?= $vendor_id ?>"><i class="fas fa-plus me-2"></i>New License</button>
<?php } ?>
</div>
<?php if ($licenses) { ?>
<ul class="list-unstyled mb-0 itflow-links itflow-license-links">
<?php foreach ($licenses as $license) {
$renewal = softwareRenewal($license['software_expire']); ?>
<li class="itflow-link-row">
<?= linkRowHtml('software', $license, $client_id) ?>
<?php if ($renewal) { ?><span class="text-nowrap"><?= softwareRenewalPill($renewal) ?></span><?php } ?>
</li>
<?php } ?>
</ul>
<?php } else { ?>
<div class="itflow-asset-empty">No licenses from them.</div>
<?php } ?>
</div>
<?php } ?>
<?php if (isset($vendor_tabs['assets'])) { ?>
<!-- Assets bought from them - for warranty claims -->
<div class="tab-pane fade" id="vendor-pane-assets" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Bought from them, warranty ending soonest first - what to have to hand for a claim.</span>
<?php if ($can_support_edit && !$vendor_archived_at) { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/asset/asset_add.php?client_id=<?= $client_id ?>&vendor_id=<?= $vendor_id ?>"><i class="fas fa-plus me-2"></i>New Asset</button>
<?php } ?>
</div>
<?php if ($assets) { ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="text-nowrap table-light"><tr><th>Asset</th><th>Serial</th><th>Order ref</th><th>Purchased</th><th>Warranty</th></tr></thead>
<tbody>
<?php foreach ($assets as $asset) {
$asset_id = intval($asset['asset_id']);
$serial = escapeHtml($asset['asset_serial']);
$order_ref = escapeHtml($asset['asset_purchase_reference']);
$warranty = softwareRenewal($asset['asset_warranty_expire'], 60); ?>
<tr>
<td>
<div class="d-flex align-items-center gap-2">
<span class="itflow-asset-icon itflow-asset-icon-sm itflow-asset-tone-<?= assetTypeTone($asset['asset_type']) ?>" aria-hidden="true"><i class="fas fa-fw fa-<?= getAssetIcon($asset['asset_type']) ?>"></i></span>
<div class="min-w-0">
<a class="fw-semibold ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/asset/asset.php?id=<?= $asset_id ?>"><?= escapeHtml($asset['asset_name']) ?></a>
<div class="small text-body-secondary"><?= escapeHtml(implode(' · ', array_filter([$asset['asset_type'], trim($asset['asset_make'] . ' ' . $asset['asset_model'])]))) ?></div>
</div>
</div>
</td>
<td class="text-nowrap"><?php if ($serial) { ?><span class="font-monospace small"><?= $serial ?></span><button type="button" class="itflow-copy clipboardjs" title="Copy serial" data-clipboard-text="<?= $serial ?>"><i class="far fa-copy"></i></button><?php } else { ?><span class="text-body-secondary">-</span><?php } ?></td>
<td class="text-nowrap small"><?= $order_ref ?: '<span class="text-body-secondary">-</span>' ?></td>
<td class="text-nowrap small"><?= escapeHtml($asset['asset_purchase_date']) ?: '<span class="text-body-secondary">-</span>' ?></td>
<td class="text-nowrap">
<?php if ($warranty) {
$tone = $warranty['tone']; ?>
<span class="badge rounded-pill bg-<?= $tone ?>-subtle text-<?= $tone ?>-emphasis border border-<?= $tone ?>-subtle fw-normal" title="<?= escapeHtml($warranty['date']) ?>">
<?= $warranty['state'] === 'expired' ? 'Expired' : ($warranty['state'] === 'soon' ? 'Ends in ' . intval($warranty['days']) . ' days' : 'To ' . escapeHtml($warranty['date'])) ?>
</span>
<?php } else { ?>
<span class="text-body-secondary">-</span>
<?php } ?>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php } else { ?>
<div class="itflow-asset-empty">No assets bought from them.</div>
<?php } ?>
</div>
<?php } ?>
<?php if (isset($vendor_tabs['domains'])) { ?>
<!-- Domains they register or host -->
<div class="tab-pane fade" id="vendor-pane-domains" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Domains they register or host, and for what.</span>
</div>
<?php if ($domains) { ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="text-nowrap table-light"><tr><th>Domain</th><th>They are</th><th>Expires</th></tr></thead>
<tbody>
<?php foreach ($domains as $domain) {
$roles = [];
foreach (VENDOR_DOMAIN_ROLES as $column => $role) {
if (intval($domain[$column]) === $vendor_id) {
$roles[] = $role;
}
}
$expiry = softwareRenewal($domain['domain_expire']); ?>
<tr>
<td>
<a class="fw-semibold ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/domain/domain_edit.php?client_id=<?= $client_id ?>&id=<?= intval($domain['domain_id']) ?>"><?= escapeHtml($domain['domain_name']) ?></a>
<?php if ($domain['domain_description']) { ?><div class="small text-body-secondary"><?= escapeHtml($domain['domain_description']) ?></div><?php } ?>
</td>
<td><?php foreach ($roles as $role) { ?><span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis fw-normal me-1"><?= $role ?></span><?php } ?></td>
<td class="text-nowrap"><?= $expiry ? softwareRenewalPill($expiry) : '<span class="text-body-secondary">-</span>' ?></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php } else { ?>
<div class="itflow-asset-empty">No domains with them.</div>
<?php } ?>
</div>
<?php } ?>
<?php if (isset($vendor_tabs['services'])) { ?>
<!-- Services they are part of -->
<div class="tab-pane fade" id="vendor-pane-services" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">The client's services that depend on them.</span>
</div>
<?php if ($services) { ?>
<ul class="list-unstyled mb-0 itflow-links itflow-license-links">
<?php foreach ($services as $service) { ?>
<li class="itflow-link-row">
<span class="itflow-link-icon" aria-hidden="true"><i class="fas fa-fw fa-stream"></i></span>
<div class="min-w-0 flex-grow-1">
<div class="d-flex"><a class="ajax-modal text-truncate" href="#" data-modal-size="lg" data-modal-url="modals/service/service.php?id=<?= intval($service['service_id']) ?>"><?= escapeHtml($service['service_name']) ?></a></div>
<div class="small text-body-secondary text-truncate"><?= escapeHtml(implode(' · ', array_filter([$service['service_category'], $service['service_description']]))) ?></div>
</div>
<?php if ($service['service_importance']) { ?><span class="badge rounded-pill bg-<?= $service['service_importance'] === 'High' ? 'danger' : ($service['service_importance'] === 'Medium' ? 'warning' : 'secondary') ?>-subtle text-body fw-normal"><?= escapeHtml($service['service_importance']) ?></span><?php } ?>
</li>
<?php } ?>
</ul>
<?php } else { ?>
<div class="itflow-asset-empty">No services depend on them.</div>
<?php } ?>
</div>
<?php } ?>
<!-- Portal logins, documents and files -->
<?php foreach (['logins' => ['credential', $logins, 'Their portal and support logins - linked from the client\'s credentials.', 'No logins linked.', $can_credentials_edit],
'documents' => ['document', $documents, 'The contract, the escalation sheet, how to raise a case with them.', 'No documents linked.', $can_support_edit],
'files' => ['file', $files, 'Quotes, invoices, the signed agreement.', 'No files linked.', $can_support_edit]] as $pane => [$to, $linked, $hint, $empty, $can_link]) {
if (!isset($vendor_tabs[$pane])) {
continue;
} ?>
<div class="tab-pane fade" id="vendor-pane-<?= $pane ?>" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small"><?= $hint ?></span>
<?php if ($can_link && !$vendor_archived_at) { ?>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary ajax-modal" data-modal-url="<?= $link_url . $to ?>">
<i class="fas fa-link me-2"></i>Link
</button>
<?php if ($to === 'credential') { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-url="modals/credential/credential_add.php?client_id=<?= $client_id ?>&vendor_id=<?= $vendor_id ?>">
<i class="fas fa-plus me-2"></i>New Login
</button>
<?php } elseif ($to === 'document') { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-size="xl" data-modal-url="modals/document/document_add.php?client_id=<?= $client_id ?>&vendor_id=<?= $vendor_id ?>">
<i class="fas fa-plus me-2"></i>New Document
</button>
<?php } else { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-url="modals/file/file_upload.php?client_id=<?= $client_id ?>&vendor_id=<?= $vendor_id ?>">
<i class="fas fa-upload me-2"></i>Upload
</button>
<?php } ?>
</div>
<?php } ?>
</div>
<?php if ($linked) { ?>
<ul class="list-unstyled mb-0 itflow-links itflow-license-links">
<?php foreach ($linked as $linked_row) { ?>
<li class="itflow-link-row">
<?= linkRowHtml($to, $linked_row, $client_id) ?>
<?php if ($can_link) { ?>
<a class="itflow-link-remove" href="<?= $unlink_url($to, $linked_row['id']) ?>" title="Unlink <?= $to ?>"><i class="fas fa-unlink"></i></a>
<?php } ?>
</li>
<?php } ?>
</ul>
<?php } else { ?>
<div class="itflow-asset-empty"><?= $empty ?></div>
<?php } ?>
</div>
<?php } ?>
<?php if (isset($vendor_tabs['expenses'])) { ?>
<!-- What we spend with them -->
<div class="tab-pane fade" id="vendor-pane-expenses" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small d-flex flex-wrap column-gap-3">
<span><strong class="text-body"><?= numfmt_format_currency($currency_format, $spend['year'], $session_company_currency) ?></strong> this year</span>
<span><?= numfmt_format_currency($currency_format, $spend['last_year'], $session_company_currency) ?> last year</span>
<span><?= numfmt_format_currency($currency_format, $spend['total'], $session_company_currency) ?> in all</span>
</span>
<div class="d-flex gap-2">
<a class="btn btn-sm btn-outline-secondary" href="expenses.php?vendor=<?= $vendor_id ?>"><i class="fas fa-list me-2"></i>All</a>
<?php if ($can_financial_edit && !$vendor_archived_at) { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-url="modals/expense/expense_add.php?vendor_id=<?= $vendor_id ?>"><i class="fas fa-plus me-2"></i>New Expense</button>
<?php } ?>
</div>
</div>
<?php if ($expenses) { ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="text-nowrap table-light"><tr><th>Date</th><th>What for</th><th>Account</th><th class="text-end">Amount</th></tr></thead>
<tbody>
<?php foreach ($expenses as $expense) { ?>
<tr>
<td class="text-nowrap"><a class="ajax-modal" href="#" data-modal-url="modals/expense/expense_edit.php?id=<?= intval($expense['expense_id']) ?>"><?= escapeHtml($expense['expense_date']) ?></a></td>
<td class="w-100">
<div><?= escapeHtml(truncate((string) $expense['expense_description'], 90)) ?: '<span class="text-body-secondary">-</span>' ?></div>
<div class="small text-body-secondary"><?= escapeHtml(implode(' · ', array_filter([$expense['category_name'], $expense['expense_reference']]))) ?></div>
</td>
<td class="text-nowrap small"><?= escapeHtml($expense['account_name']) ?></td>
<td class="text-end text-nowrap font-monospace"><?= numfmt_format_currency($currency_format, floatval($expense['expense_amount']), $expense['expense_currency_code']) ?></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php if ($spend['count'] > count($expenses)) { ?>
<div class="small text-body-secondary px-3 py-2 border-top">The <?= count($expenses) ?> most recent of <?= $spend['count'] ?> - <a href="expenses.php?vendor=<?= $vendor_id ?>">see them all</a>.</div>
<?php } ?>
<?php } else { ?>
<div class="itflow-asset-empty">Nothing spent with them yet.</div>
<?php } ?>
</div>
<!-- What is booked to them every month or year -->
<div class="tab-pane fade" id="vendor-pane-recurring" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Subscriptions and contracts booked automatically.</span>
<?php if ($can_financial_edit && !$vendor_archived_at) { ?>
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/recurring_expense/recurring_expense_add.php?vendor_id=<?= $vendor_id ?>"><i class="fas fa-plus me-2"></i>New Recurring</button>
<?php } ?>
</div>
<?php if ($recurring) { ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="text-nowrap table-light"><tr><th>Next</th><th>Every</th><th>What for</th><th class="text-end">Amount</th></tr></thead>
<tbody>
<?php foreach ($recurring as $recurring_expense) {
$active = intval($recurring_expense['recurring_expense_status']) === 1; ?>
<tr class="<?= $active ? '' : 'text-body-secondary' ?>">
<td class="text-nowrap"><a class="ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/recurring_expense/recurring_expense_edit.php?id=<?= intval($recurring_expense['recurring_expense_id']) ?>"><?= escapeHtml($recurring_expense['recurring_expense_next_date']) ?></a><?php if (!$active) { ?> <span class="badge rounded-pill text-bg-secondary fw-normal">Paused</span><?php } ?></td>
<td class="text-nowrap"><?= intval($recurring_expense['recurring_expense_frequency']) === 1 ? 'Month' : 'Year' ?></td>
<td class="w-100">
<div><?= escapeHtml(truncate((string) $recurring_expense['recurring_expense_description'], 90)) ?: '<span class="text-body-secondary">-</span>' ?></div>
<div class="small text-body-secondary"><?= escapeHtml($recurring_expense['category_name']) ?></div>
</td>
<td class="text-end text-nowrap font-monospace"><?= numfmt_format_currency($currency_format, floatval($recurring_expense['recurring_expense_amount']), $recurring_expense['recurring_expense_currency_code']) ?></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php } else { ?>
<div class="itflow-asset-empty">Nothing recurring with them.</div>
<?php } ?>
</div>
<?php } ?>
<!-- History, from the audit log -->
<div class="tab-pane fade" id="vendor-pane-history" role="tabpanel">
<div class="itflow-asset-pane-bar">
<span class="text-body-secondary small">Edits, links and notes - newest first.</span>
</div>
<?php if ($history) {
$history_icons = ['Create' => 'fa-plus', 'Edit' => 'fa-edit', 'Archive' => 'fa-archive', 'Restore' => 'fa-redo', 'Link' => 'fa-link', 'Unlink' => 'fa-unlink']; ?>
<ul class="list-group list-group-flush">
<?php foreach ($history as $entry) { ?>
<li class="list-group-item d-flex gap-3 py-2">
<span class="itflow-asset-icon itflow-asset-icon-sm itflow-asset-tone-other" title="<?= escapeHtml($entry['log_action']) ?>"><i class="fas fa-fw <?= $history_icons[$entry['log_action']] ?? 'fa-circle' ?>"></i></span>
<div class="flex-grow-1 min-w-0">
<div><?= escapeHtml($entry['log_description']) ?></div>
<div class="small text-body-secondary"><?= escapeHtml($entry['user_name'] ?: 'System') ?> &middot; <span title="<?= escapeHtml($entry['log_created_at']) ?>"><?= timeAgo($entry['log_created_at']) ?></span></div>
</div>
</li>
<?php } ?>
</ul>
<?php } else { ?>
<div class="itflow-asset-empty">Nothing logged yet.</div>
<?php } ?>
</div>
</div>
</div>
</div>
</div>
<script>
// Notes save when you click away
document.querySelectorAll('.itflow-vendor-notes').forEach(function (notes) {
var saved = notes.value;
notes.addEventListener('blur', function () {
if (notes.readOnly || notes.value === saved) {
return;
}
saved = notes.value;
itflowPost('ajax.php', {
vendor_set_notes: 'TRUE',
csrf_token: notes.dataset.csrf,
vendor_id: notes.dataset.vendorId,
notes: notes.value
});
});
});
/*
* Which tab is open: the URL hash wins (#licenses), then the tab last
* used on any vendor, then the first. Choosing a tab updates the hash
* without a history entry, so the redirect back from a save lands on
* the same tab. Bootstrap loads in the footer, so this waits for it.
*/
document.addEventListener('DOMContentLoaded', function () {
var key = 'itflow.vendorTab';
var tabs = document.querySelectorAll('[data-vendor-tab]');
if (!tabs.length) {
return;
}
function byName(name) {
return name ? document.querySelector('[data-vendor-tab="' + name + '"]') : null;
}
var remembered = '';
try { remembered = localStorage.getItem(key) || ''; } catch (e) {}
var start = byName(location.hash.replace('#', '')) || byName(remembered) || tabs[0];
bootstrap.Tab.getOrCreateInstance(start).show();
tabs.forEach(function (tab) {
tab.addEventListener('shown.bs.tab', function () {
var name = tab.getAttribute('data-vendor-tab');
try { localStorage.setItem(key, name); } catch (e) {}
history.replaceState(null, '', location.pathname + location.search + '#' + name);
});
});
});
</script>
<?php
}
require_once "../includes/footer.php";