Files
itflow/agent/modals/software/software.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

319 lines
19 KiB
PHP

<?php
/*
* License quick look - the license page in a modal, opened from the asset
* and contact pages, the license list, a document's or file's links and the
* search. Header with renewal and seat state, the facts to copy (key, order
* reference, vendor account and phone), quick notes, then tabs for only what
* has something in it: devices, users, keys, credentials, documents, files.
*/
require_once '../../../includes/modal_header.php';
enforceUserPermission('module_support');
$software_id = intval($_GET['id'] ?? 0);
$license = softwareRow($software_id);
if (!$license) {
echo json_encode(['error' => 'That license no longer exists.']);
exit;
}
$client_id = intval($license['software_client_id']);
enforceClientAccess();
$software_name = escapeHtml($license['software_name']);
$software_version = escapeHtml($license['software_version']);
$software_description = escapeHtml($license['software_description']);
$software_purchase_reference = escapeHtml($license['software_purchase_reference']);
$software_notes = escapeHtml($license['software_notes']);
$software_archived = !empty($license['software_archived_at']);
$vendor_id = intval($license['vendor_id']);
$vendor_name = escapeHtml($license['vendor_name']);
$vendor_account = escapeHtml($license['vendor_account_number']);
$vendor_phone = contactPhoneLink($license['vendor_phone'], $license['vendor_phone_country_code'], $license['vendor_extension']);
$seats = softwareSeats($license['seats_used'], $license['software_seats']);
$renewal = softwareRenewal($license['software_expire']);
$can_edit = lookupUserPermission('module_support') >= 2 && !$software_archived;
$can_credentials = lookupUserPermission('module_credential') >= 1;
$holders = softwareSeatHolders($software_id);
$keys = softwareKeys($software_id);
$credentials = $can_credentials ? linkedRecords('software', $software_id, 'credential') : [];
$documents = linkedRecords('software', $software_id, 'document');
$files = linkedRecords('software', $software_id, 'file');
$quick_tabs = array_filter([
'devices' => ['label' => 'Devices', 'count' => count($holders['assets'])],
'users' => ['label' => 'Users', 'count' => count($holders['contacts'])],
'keys' => ['label' => 'Keys', 'count' => count($keys)],
'credentials' => ['label' => 'Credentials', 'count' => count($credentials)],
'documents' => ['label' => 'Documents', 'count' => count($documents)],
'files' => ['label' => 'Files', 'count' => count($files)],
], function ($tab) {
return $tab['count'] > 0;
});
$first_tab = array_key_first($quick_tabs);
$quick_id = "license-quick-$software_id";
ob_start();
?>
<div class="modal-header bg-dark" data-bs-theme="dark">
<div class="d-flex align-items-center gap-3 min-w-0">
<?= softwareTile($license['software_type']) ?>
<div class="min-w-0">
<h5 class="modal-title d-flex flex-wrap align-items-center column-gap-2 row-gap-1">
<span class="fw-semibold text-break"><?= $software_name ?></span>
<?php if ($software_version) { ?><span class="text-body-secondary fw-normal fs-6"><?= $software_version ?></span><?php } ?>
<?php if (intval($license['software_favorite'])) { ?><i class="fas fa-star text-warning fs-6" title="Favorite"></i><?php } ?>
<?php if ($software_archived) { ?><span class="badge rounded-pill text-bg-secondary fw-normal fs-6">Archived</span><?php } ?>
<span class="fs-6"><?= $renewal && $renewal['state'] !== 'active' ? softwareRenewalPill($renewal) : '' ?></span>
<?php if ($seats['state'] === 'over') { ?>
<span class="badge rounded-pill bg-danger-subtle text-danger-emphasis border border-danger-subtle fw-normal fs-6"><?= $seats['over'] ?> over seats</span>
<?php } ?>
</h5>
<div class="small text-body-secondary d-flex flex-wrap column-gap-3 mt-1">
<?php if ($license['software_type']) { ?><span><?= escapeHtml($license['software_type']) ?></span><?php } ?>
<?php if ($license['software_license_type']) { ?><span><i class="fas fa-fw <?= softwareLicenseTypeIcon($license['software_license_type']) ?> me-1"></i><?= escapeHtml($license['software_license_type']) ?></span><?php } ?>
<span><?= escapeHtml($license['client_name']) ?></span>
</div>
</div>
</div>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<?php if ($software_description) { ?>
<div class="text-body-secondary mb-3"><?= $software_description ?></div>
<?php } ?>
<!-- At a glance - the icons say what each line is -->
<div class="row g-2 itflow-asset-glance">
<div class="col-sm-6">
<div class="itflow-glance-item" title="Seats">
<i class="fas fa-fw fa-users"></i>
<div class="flex-grow-1 min-w-0 itflow-seats-glance"><?= softwareSeatBar($seats) ?></div>
</div>
<?php if ($license['software_key']) { ?>
<div class="itflow-glance-item" title="License key">
<i class="fas fa-fw fa-key"></i>
<span class="min-w-0"><?= softwareKeyHtml($license['software_key']) ?></span>
</div>
<?php } elseif ($keys) { ?>
<div class="itflow-glance-item" title="Keys">
<i class="fas fa-fw fa-key"></i>
<span><?= count($keys) ?> individual key<?= count($keys) === 1 ? '' : 's' ?></span>
</div>
<?php } ?>
<?php if ($software_purchase_reference) { ?>
<div class="itflow-glance-item" title="Order reference">
<i class="fas fa-fw fa-receipt"></i>
<span><?= $software_purchase_reference ?><button type="button" class="itflow-copy clipboardjs" title="Copy reference" data-clipboard-text="<?= $software_purchase_reference ?>"><i class="far fa-copy"></i></button></span>
</div>
<?php } ?>
</div>
<div class="col-sm-6">
<div class="itflow-glance-item" title="Renewal">
<i class="fas fa-fw fa-calendar-alt"></i>
<?php if ($renewal) { ?>
<span><?= escapeHtml($renewal['date']) ?> <span class="small text-<?= $renewal['tone'] === 'success' ? 'body-secondary' : $renewal['tone'] . '-emphasis' ?>">&middot; <?= escapeHtml($renewal['text']) ?></span></span>
<?php } else { ?>
<span class="text-body-secondary">No renewal date</span>
<?php } ?>
</div>
<?php if ($vendor_name) { ?>
<div class="itflow-glance-item" title="Vendor">
<i class="fas fa-fw fa-building"></i>
<span class="min-w-0">
<?= vendorLink($vendor_id, $license['vendor_name']) ?>
<?php if ($vendor_phone) { ?><span class="text-body-secondary">&middot;</span> <?= $vendor_phone ?><?php } ?>
</span>
</div>
<?php if ($vendor_account) { ?>
<div class="itflow-glance-item" title="Vendor account number">
<i class="fas fa-fw fa-id-card"></i>
<span><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>
</div>
<?php } ?>
<?php } ?>
</div>
</div>
<?php if ($can_edit || $software_notes) { ?>
<textarea class="form-control form-control-sm mt-3 itflow-license-notes" rows="2" placeholder="Notes - saves when you click away" aria-label="Notes"
data-software-id="<?= $software_id ?>" data-csrf="<?= escapeHtml($_SESSION['csrf_token'] ?? '') ?>" <?= $can_edit ? '' : 'readonly' ?>><?= $software_notes ?></textarea>
<?php } ?>
<?php if ($quick_tabs) { ?>
<div class="itflow-asset-related itflow-asset-quick-related mt-3" data-license-quick="<?= $software_id ?>">
<ul class="nav nav-pills itflow-asset-pills mb-2" role="tablist">
<?php foreach ($quick_tabs as $tab_key => $tab) { ?>
<li class="nav-item" role="presentation">
<button class="nav-link text-nowrap <?= $tab_key === $first_tab ? 'active' : '' ?>" data-bs-toggle="pill" data-bs-target="#<?= $quick_id ?>-<?= $tab_key ?>" data-license-quick-tab="<?= $tab_key ?>" type="button" role="tab">
<?= $tab['label'] ?> <span class="badge rounded-pill ms-1"><?= $tab['count'] ?></span>
</button>
</li>
<?php } ?>
</ul>
<div class="tab-content">
<?php if ($holders['assets']) { ?>
<div class="tab-pane fade <?= $first_tab === 'devices' ? 'show active' : '' ?>" id="<?= $quick_id ?>-devices" role="tabpanel">
<ul class="list-unstyled mb-0 itflow-links">
<?php foreach ($holders['assets'] as $holder) { ?>
<li class="itflow-link-row">
<span class="itflow-asset-icon itflow-asset-icon-sm itflow-asset-tone-<?= assetTypeTone($holder['asset_type']) ?>" aria-hidden="true"><i class="fas fa-fw fa-<?= getAssetIcon($holder['asset_type']) ?>"></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/asset/asset.php?id=<?= intval($holder['asset_id']) ?>"><?= escapeHtml($holder['asset_name']) ?></a></div>
<div class="small text-body-secondary text-truncate"><?= escapeHtml(implode(' · ', array_filter([trim($holder['asset_make'] . ' ' . $holder['asset_model']), $holder['asset_contact_name']]))) ?></div>
</div>
<?php if ($holder['held_keys']) { ?><span class="small text-nowrap"><?= softwareKeyHtml($holder['held_keys'][0]) ?></span><?php } ?>
</li>
<?php } ?>
</ul>
</div>
<?php } ?>
<?php if ($holders['contacts']) { ?>
<div class="tab-pane fade <?= $first_tab === 'users' ? 'show active' : '' ?>" id="<?= $quick_id ?>-users" role="tabpanel">
<ul class="list-unstyled mb-0 itflow-links">
<?php foreach ($holders['contacts'] as $holder) { ?>
<li class="itflow-link-row">
<?= contactAvatar($holder['contact_client_id'], $holder['contact_id'], $holder['contact_name'], $holder['contact_photo'], 'sm') ?>
<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/contact/contact.php?id=<?= intval($holder['contact_id']) ?>"><?= escapeHtml($holder['contact_name']) ?></a></div>
<div class="small text-body-secondary text-truncate"><?= escapeHtml(implode(' · ', array_filter([$holder['contact_title'], $holder['contact_email']]))) ?></div>
</div>
<?php if ($holder['held_keys']) { ?><span class="small text-nowrap"><?= softwareKeyHtml($holder['held_keys'][0]) ?></span><?php } ?>
</li>
<?php } ?>
</ul>
</div>
<?php } ?>
<?php if ($keys) { ?>
<div class="tab-pane fade <?= $first_tab === 'keys' ? 'show active' : '' ?>" id="<?= $quick_id ?>-keys" role="tabpanel">
<ul class="list-group list-group-flush">
<?php foreach ($keys as $key) {
$handed = array_merge(array_column($key['assets'], 'asset_name'), array_column($key['contacts'], 'contact_name')); ?>
<li class="list-group-item px-0 d-flex align-items-center justify-content-between gap-3">
<span class="min-w-0"><?= softwareKeyHtml($key['key']) ?></span>
<?php if ($handed) { ?>
<span class="small text-body-secondary text-truncate"><?= escapeHtml(implode(', ', $handed)) ?></span>
<?php } else { ?>
<span class="badge rounded-pill bg-success-subtle text-success-emphasis border border-success-subtle fw-normal">Free</span>
<?php } ?>
</li>
<?php } ?>
</ul>
</div>
<?php } ?>
<?php if ($credentials) { ?>
<div class="tab-pane fade <?= $first_tab === 'credentials' ? 'show active' : '' ?>" id="<?= $quick_id ?>-credentials" role="tabpanel">
<div class="table-responsive">
<table class="table table-sm table-hover align-middle mb-0">
<tbody>
<?php foreach ($credentials as $credential) {
$credential_id = intval($credential['id']);
$credential_username = escapeHtml(decryptCredentialEntry($credential['credential_username'])); ?>
<tr>
<td>
<a class="ajax-modal" href="#" data-modal-url="modals/credential/credential_edit.php?id=<?= $credential_id ?>"><?= escapeHtml($credential['name']) ?></a>
<?php if ($credential['credential_uri']) { ?><a class="ms-1 small" href="<?= escapeUrl($credential['credential_uri']) ?>" target="_blank" rel="noopener" title="<?= escapeHtml($credential['credential_uri']) ?>"><i class="fas fa-external-link-alt"></i></a><?php } ?>
</td>
<td class="text-nowrap"><?php if ($credential_username) { ?><?= $credential_username ?><button type="button" class="itflow-copy clipboardjs" title="Copy username" data-clipboard-text="<?= $credential_username ?>"><i class="far fa-copy"></i></button><?php } else { ?><span class="text-body-secondary">-</span><?php } ?></td>
<td class="text-nowrap">
<button class="btn btn-sm btn-light" aria-label="Show password" title="Show password" type="button" onclick="showPasswordViaCredentialID(this, <?= $credential_id ?>)"><i class="fas fa-ellipsis-h"></i><i class="fas fa-ellipsis-h"></i></button>
<button type="button" class="itflow-copy" title="Copy password" onclick="copyPasswordViaCredentialID(this, <?= $credential_id ?>)"><i class="far fa-copy"></i></button>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
<?php /* js/, not ../js/ - this file is injected by js/ajax_modal.js, so a
relative src resolves against the host page's URL (/agent/...) */ ?>
<script src="js/credential_show_password_via_id.js"></script>
<?php } ?>
<?php foreach (['documents' => ['document', $documents], 'files' => ['file', $files]] as $pane => [$to, $linked]) {
if (!$linked) {
continue;
} ?>
<div class="tab-pane fade <?= $first_tab === $pane ? 'show active' : '' ?>" id="<?= $quick_id ?>-<?= $pane ?>" role="tabpanel">
<ul class="list-unstyled mb-0 itflow-links">
<?php foreach ($linked as $linked_row) { ?>
<li class="itflow-link-row"><?= linkRowHtml($to, $linked_row, $client_id) ?></li>
<?php } ?>
</ul>
</div>
<?php } ?>
</div>
</div>
<?php } elseif ($can_edit) { ?>
<div class="text-body-secondary small mt-3">Nothing and nobody on this license yet - <a class="ajax-modal" href="#" data-modal-url="modals/software/software_seats.php?id=<?= $software_id ?>">add devices and users</a>.</div>
<?php } ?>
</div>
<div class="modal-footer">
<a href="license.php?client_id=<?= $client_id ?>&software_id=<?= $software_id ?>" class="btn btn-primary text-bold"><i class="fas fa-arrow-right me-2"></i>Open license</a>
<?php if ($can_edit) { ?>
<button type="button" class="btn btn-outline-secondary ajax-modal" data-modal-url="modals/software/software_seats.php?id=<?= $software_id ?>"><i class="fas fa-users me-2"></i>Seats</button>
<button type="button" class="btn btn-outline-secondary ajax-modal" data-modal-size="lg" data-modal-url="modals/software/software_edit.php?id=<?= $software_id ?>"><i class="fas fa-edit me-2"></i>Edit</button>
<?php } ?>
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Close</button>
</div>
<script>
(function () {
// Notes save when you click away - scoped to this modal's own textarea
document.querySelectorAll('.modal .itflow-license-notes:not([data-wired])').forEach(function (notes) {
notes.dataset.wired = '1';
var saved = notes.value;
notes.addEventListener('blur', function () {
if (notes.readOnly || notes.value === saved) {
return;
}
saved = notes.value;
itflowPost('ajax.php', {
software_set_notes: 'TRUE',
csrf_token: notes.dataset.csrf,
software_id: notes.dataset.softwareId,
notes: notes.value
});
});
});
// Open on the tab used last time, if this license has one
var key = 'itflow.licenseQuickTab';
document.querySelectorAll('[data-license-quick]:not([data-wired])').forEach(function (box) {
box.dataset.wired = '1';
var remembered = null;
try {
remembered = localStorage.getItem(key);
} catch (e) {}
var tab = remembered && box.querySelector('[data-license-quick-tab="' + remembered + '"]');
if (tab && !tab.classList.contains('active')) {
bootstrap.Tab.getOrCreateInstance(tab).show();
}
box.addEventListener('shown.bs.tab', function (e) {
try {
localStorage.setItem(key, e.target.dataset.licenseQuickTab);
} catch (err) {}
});
});
})();
</script>
<?php
require_once '../../../includes/modal_footer.php';