mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 07:01:16 +00:00
Licenses: rebuilt the license list with Renewing soon, Expired, Over seats and No seats assigned tabs, a seat bar (used against bought, red when over) and colour-coded renewal on each row, license type and renewal filters, optional key, purchase and order reference columns, search by vendor, order reference and individual keys, and bulk Renew, Archive, Restore and Delete; new license page (license.php) with renewal and over-seats status in the header, License and Vendor cards (key, order ref, purchase date, vendor account number, phone, email, website, hours, SLA, all copyable), notes that save on blur, and tabs for Seats (each device and user with the key they were given), Keys, Credentials, Documents, Files and History from the audit log; new license quick look used everywhere a license is shown; New and Edit share one tabbed form (Details, License, Seats, Notes) with device and user pickers, a live seat counter, renewal shortcuts and a template picker that replaces the separate Create from Template modal; Renew modal for one license or a bulk selection that moves the renewal date by a term or to a set date, previews the new date for each license and records a new order reference; Manage Seats modal; individual keys pasted one per line and handed out to devices and users, which gives them a seat, with the key returned to the pool when they come off the license; Renewal Ticket opens New Ticket with the subject and details filled in; New Document and Upload from a license link the result to it; devices, users, credentials and files now link to licenses through the shared link modal, which shows the seats left on each license and offers every license type, not just Device; the asset and contact pages and quick looks list licenses with their seats, renewal and the key to install, and get New License; the client overview gets a Licenses over seats card under Needs attention, starred licenses under Favorites and renewals that open the license page; global search finds licenses by name, vendor, order reference or key; the vendor quick look lists that vendor's licenses; the dashboard card opens the Renewing tab; the export gains seats used, individual keys, vendor, order reference and client columns; admin license templates share the license type list and show type icons. Uses the existing software_keys, key assignment, software_credentials and software_files tables. Fixes editing a Subscription or Open Source license clearing its license type, archiving a license removing all of its seats (seats are now kept, matching the API), license forms and seat handlers accepting another client's assets, contacts or vendor, a 500 on duplicate seat links and on forms missing fields, and an extra query per row in the export; license deletes now respect the destructive deletes setting. No DB change
This commit is contained in:
@@ -2,15 +2,8 @@
|
||||
|
||||
require_once '../../includes/modal_header.php';
|
||||
|
||||
$license_types_array = array (
|
||||
'Device',
|
||||
'User',
|
||||
'Site',
|
||||
'Concurrent',
|
||||
'Trial',
|
||||
'Perpetual',
|
||||
'Usage-based'
|
||||
);
|
||||
// The license types the license form offers (functions/software.php)
|
||||
$license_types_array = array_keys(SOFTWARE_LICENSE_TYPES);
|
||||
|
||||
ob_start();
|
||||
|
||||
@@ -78,7 +71,7 @@ ob_start();
|
||||
<select class="form-select select2" name="license_type">
|
||||
<option value="">- Select a License Type -</option>
|
||||
<?php foreach($license_types_array as $license_type) { ?>
|
||||
<option><?= $license_type ?></option>
|
||||
<option<?= selectOptionData(['icon' => 'fas ' . softwareLicenseTypeIcon($license_type)]) ?>><?= escapeHtml($license_type) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -14,15 +14,11 @@ $software_type = escapeHtml($row['software_template_type']);
|
||||
$software_license_type = escapeHtml($row['software_template_license_type']);
|
||||
$software_notes = escapeHtml($row['software_template_notes']);
|
||||
|
||||
$license_types_array = array (
|
||||
'Device',
|
||||
'User',
|
||||
'Site',
|
||||
'Concurrent',
|
||||
'Trial',
|
||||
'Perpetual',
|
||||
'Usage-based'
|
||||
);
|
||||
// The license types the license form offers (functions/software.php), keeping one this template already has
|
||||
$license_types_array = array_keys(SOFTWARE_LICENSE_TYPES);
|
||||
if ($row['software_template_license_type'] && !in_array($row['software_template_license_type'], $license_types_array, true)) {
|
||||
$license_types_array[] = $row['software_template_license_type'];
|
||||
}
|
||||
|
||||
ob_start();
|
||||
|
||||
@@ -93,7 +89,7 @@ ob_start();
|
||||
<select class="form-select select2" name="license_type">
|
||||
<option value="">- Select a License Type -</option>
|
||||
<?php foreach($license_types_array as $license_type_select) { ?>
|
||||
<option <?php if($license_type_select == $software_license_type){ echo "selected"; } ?>><?= $license_type_select ?></option>
|
||||
<option <?php if (escapeHtml($license_type_select) === $software_license_type) { echo "selected"; } ?><?= selectOptionData(['icon' => 'fas ' . softwareLicenseTypeIcon($license_type_select)]) ?>><?= escapeHtml($license_type_select) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
</form>
|
||||
</div>
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-striped table-borderless table-hover mb-0">
|
||||
<table class="table table-striped table-borderless table-hover align-middle mb-0">
|
||||
<thead class="text-dark <?php if($num_rows[0] == 0){ echo "d-none"; } ?>">
|
||||
<tr>
|
||||
<th class="ps-3">
|
||||
@@ -80,18 +80,18 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
?>
|
||||
<tr>
|
||||
<td class="ps-3">
|
||||
<a class="text-dark ajax-modal" href="#" data-modal-url="modals/software_template/software_template_edit.php?id=<?= $software_template_id ?>">
|
||||
<div class="d-flex">
|
||||
<i class="fa fa-fw fa-2x fa-cube me-3"></i>
|
||||
<div class="flex-grow-1">
|
||||
<div><?= "$software_template_name <span>$software_template_version</span>" ?></div>
|
||||
<a class="text-reset ajax-modal" href="#" data-modal-url="modals/software_template/software_template_edit.php?id=<?= $software_template_id ?>">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<?= softwareTile($row['software_template_type']) ?>
|
||||
<div class="flex-grow-1 min-w-0">
|
||||
<div><span class="fw-semibold"><?= $software_template_name ?></span> <span class="text-body-secondary"><?= $software_template_version ?></span></div>
|
||||
<div><small class="text-secondary"><?= $software_template_description ?></small></div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</td>
|
||||
<td><?= $software_template_type ?></td>
|
||||
<td><?= $software_template_license_type ?></td>
|
||||
<td class="text-nowrap"><?php if ($software_template_license_type) { ?><i class="fas fa-fw <?= softwareLicenseTypeIcon($row['software_template_license_type']) ?> text-secondary me-1"></i><?= $software_template_license_type ?><?php } else { ?><span class="text-body-secondary">-</span><?php } ?></td>
|
||||
<td>
|
||||
<div class="dropdown dropstart text-center">
|
||||
<button class="btn btn-secondary btn-sm" title="More actions" data-bs-toggle="dropdown">
|
||||
|
||||
@@ -576,6 +576,31 @@ if (isset($_POST['asset_set_notes'])) {
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* License quick notes - the license page and quick look save on blur
|
||||
*/
|
||||
if (isset($_POST['software_set_notes'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$software_id = intval($_POST['software_id'] ?? 0);
|
||||
$row = softwareRow($software_id);
|
||||
if (!$row) {
|
||||
exit;
|
||||
}
|
||||
$client_id = intval($row['software_client_id']);
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
$notes = escapeSql($_POST['notes'] ?? '');
|
||||
mysqli_query($mysqli, "UPDATE software SET software_notes = '$notes' WHERE software_id = $software_id");
|
||||
|
||||
logAudit("Software", "Edit", "$session_name edited the notes on license " . escapeSql($row['software_name']), $client_id, $software_id);
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Ticketing Collision Detection/Avoidance
|
||||
* Called upon loading a ticket, and every 2 mins thereafter
|
||||
|
||||
@@ -224,20 +224,9 @@ if (mysqli_num_rows($sql) == 0) {
|
||||
$credential_count = mysqli_num_rows($sql_related_credentials);
|
||||
}
|
||||
|
||||
// Licenses, with seats in use counted in the same query instead of two queries per license
|
||||
$sql_related_software = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT software_expire, software_assets.software_id, software_license_type,
|
||||
software_name, software_seats, software_type, software_version,
|
||||
(SELECT COUNT(*) FROM software_assets AS sa WHERE sa.software_id = software.software_id)
|
||||
+ (SELECT COUNT(*) FROM software_contacts AS sc WHERE sc.software_id = software.software_id) AS seats_used
|
||||
FROM software_assets
|
||||
LEFT JOIN software ON software_assets.software_id = software.software_id
|
||||
WHERE software_assets.asset_id = $asset_id
|
||||
AND software_archived_at IS NULL
|
||||
ORDER BY software_name ASC"
|
||||
);
|
||||
$software_count = mysqli_num_rows($sql_related_software);
|
||||
// Licenses it holds a seat on, with each one's seats in use and the key it was given
|
||||
$held_software = softwareHeldBy('asset', $asset_id);
|
||||
$software_count = count($held_software);
|
||||
|
||||
// Linked services
|
||||
$sql_linked_services = mysqli_query($mysqli, "SELECT service_category, service_description, service_assets.service_id, service_importance,
|
||||
@@ -356,6 +345,9 @@ if (mysqli_num_rows($sql) == 0) {
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/asset/asset_interface_add.php?&asset_id=<?= $asset_id ?>">
|
||||
<i class="fa fa-fw fa-ethernet text-secondary me-2"></i>Interface
|
||||
</a>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/software/software_add.php?client_id=<?= $client_id ?>&asset_id=<?= $asset_id ?>">
|
||||
<i class="fa fa-fw fa-cube text-secondary me-2"></i>License
|
||||
</a>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/document/document_add.php?<?= $client_url ?>&asset_id=<?= $asset_id ?>" data-modal-size="xl">
|
||||
<i class="fa fa-fw fa-file-alt text-secondary me-2"></i>Document
|
||||
</a>
|
||||
@@ -370,7 +362,7 @@ if (mysqli_num_rows($sql) == 0) {
|
||||
<div class="dropdown">
|
||||
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown"><i class="fas fa-link me-2"></i>Link</button>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/asset/asset_link_software.php?id=<?= $asset_id ?>">
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/link/link.php?from=asset&id=<?= $asset_id ?>&to=software">
|
||||
<i class="fa fa-fw fa-cube text-secondary me-2"></i>License
|
||||
</a>
|
||||
<?php if ($can_credentials) { ?>
|
||||
@@ -969,62 +961,20 @@ if (mysqli_num_rows($sql) == 0) {
|
||||
<!-- Licenses -->
|
||||
<div class="tab-pane fade" id="asset-pane-licenses" role="tabpanel">
|
||||
<div class="itflow-asset-pane-bar">
|
||||
<span class="text-body-secondary small">Software licensed to this asset.</span>
|
||||
<span class="text-body-secondary small">Licenses this asset holds a seat on, and the key to install.</span>
|
||||
<?php if ($can_edit) { ?>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary ajax-modal" data-modal-url="modals/asset/asset_link_software.php?id=<?= $asset_id ?>">
|
||||
<i class="fas fa-link me-2"></i>Link License
|
||||
</button>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary ajax-modal" data-modal-url="modals/link/link.php?from=asset&id=<?= $asset_id ?>&to=software">
|
||||
<i class="fas fa-link me-2"></i>Link
|
||||
</button>
|
||||
<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 ?>&asset_id=<?= $asset_id ?>">
|
||||
<i class="fas fa-plus me-2"></i>New License
|
||||
</button>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php if ($software_count) { ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="text-nowrap table-light">
|
||||
<tr>
|
||||
<th>Software</th>
|
||||
<th>Type</th>
|
||||
<th>License</th>
|
||||
<th>Seats</th>
|
||||
<th>Expires</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php while ($row = mysqli_fetch_assoc($sql_related_software)) {
|
||||
$software_id = intval($row['software_id']);
|
||||
$software_name = escapeHtml($row['software_name']);
|
||||
$software_version = escapeHtml($row['software_version']);
|
||||
$software_type = escapeHtml($row['software_type']);
|
||||
$software_license_type = escapeHtml($row['software_license_type']);
|
||||
$software_seats = escapeHtml($row['software_seats']);
|
||||
$software_seats_used = intval($row['seats_used']);
|
||||
$software_expire = assetWarranty($row['software_expire'], 30);
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<a class="text-body fw-semibold ajax-modal" href="#" data-modal-url="modals/software/software_edit.php?id=<?= $software_id ?>"><?= $software_name ?></a>
|
||||
<?php if ($software_version) { ?><div class="small text-body-secondary"><?= $software_version ?></div><?php } ?>
|
||||
</td>
|
||||
<td><?= $software_type ?></td>
|
||||
<td><?= $software_license_type ?></td>
|
||||
<td class="text-nowrap <?= $software_seats !== '' && $software_seats_used > intval($software_seats) ? 'text-danger' : '' ?>"><?= $software_seats_used ?> / <?= $software_seats !== '' ? $software_seats : '∞' ?></td>
|
||||
<td class="text-nowrap">
|
||||
<?php if ($software_expire) { ?>
|
||||
<span class="text-<?= $software_expire['tone'] === 'success' ? 'body' : $software_expire['tone'] . '-emphasis' ?>" title="<?= escapeHtml($software_expire['text']) ?>"><?= escapeHtml($software_expire['date']) ?></span>
|
||||
<?php } else { ?>
|
||||
<span class="text-body-secondary">-</span>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<?php if ($can_edit) { ?>
|
||||
<a href="post.php?unlink_software_from_asset&asset_id=<?= $asset_id ?>&software_id=<?= $software_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>" class="btn btn-sm btn-light" title="Unlink"><i class="fas fa-fw fa-unlink"></i></a>
|
||||
<?php } ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?= softwareHeldTable($held_software, 'asset', $asset_id, $can_edit) ?>
|
||||
<?php } else { ?>
|
||||
<div class="itflow-asset-empty">No licenses linked.</div>
|
||||
<?php } ?>
|
||||
|
||||
@@ -205,7 +205,7 @@ $money_format = function ($amount) use ($currency_format, $client_currency_code,
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($favorites['assets'] || $favorites['credentials']) { ?>
|
||||
<?php if ($favorites['assets'] || $favorites['licenses'] || $favorites['credentials']) { ?>
|
||||
<div class="col-md-6 col-xl-4">
|
||||
<div class="card h-100 mb-0">
|
||||
<div class="card-header d-flex align-items-center">
|
||||
@@ -226,6 +226,19 @@ $money_format = function ($amount) use ($currency_format, $client_currency_code,
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<?php foreach ($favorites['licenses'] as $favorite_license) {
|
||||
$favorite_renewal = softwareRenewal($favorite_license['software_expire']); ?>
|
||||
<div class="list-group-item d-flex align-items-center gap-2 py-2">
|
||||
<i class="fas fa-fw <?= softwareTypeStyle($favorite_license['software_type'])['icon'] ?> text-body-secondary" title="License"></i>
|
||||
<span class="flex-grow-1 overflow-hidden">
|
||||
<a class="ajax-modal d-block text-truncate" href="#" data-modal-url="modals/software/software.php?id=<?= intval($favorite_license['software_id']) ?>" data-modal-size="lg"><?= escapeHtml($favorite_license['software_name']) ?></a>
|
||||
<span class="d-block small text-truncate text-<?= $favorite_renewal && $favorite_renewal['state'] !== 'active' ? $favorite_renewal['tone'] . '-emphasis' : 'body-secondary' ?>"><?= escapeHtml(implode(' · ', array_filter([$favorite_license['software_license_type'], $favorite_renewal ? $favorite_renewal['text'] : '']))) ?></span>
|
||||
</span>
|
||||
<?php if ($favorite_license['software_key']) { ?>
|
||||
<span class="small text-nowrap"><?= softwareKeyHtml($favorite_license['software_key']) ?></span>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<?php foreach ($favorites['credentials'] as $credential) {
|
||||
$credential_id = intval($credential['credential_id']);
|
||||
$credential_username = escapeHtml(decryptCredentialEntry($credential['credential_username']));
|
||||
|
||||
@@ -138,18 +138,9 @@ if ($can_support) {
|
||||
$recurring_ticket_count = mysqli_num_rows($sql_related_recurring_tickets);
|
||||
}
|
||||
|
||||
// Licenses, with seats in use counted in the same query instead of two queries per license
|
||||
$sql_linked_software = mysqli_query($mysqli, "SELECT software.software_id, software_name, software_version, software_type, software_license_type,
|
||||
software_seats, software_expire,
|
||||
(SELECT COUNT(*) FROM software_assets AS sa WHERE sa.software_id = software.software_id)
|
||||
+ (SELECT COUNT(*) FROM software_contacts AS sc WHERE sc.software_id = software.software_id) AS seats_used
|
||||
FROM software_contacts
|
||||
JOIN software ON software.software_id = software_contacts.software_id
|
||||
WHERE software_contacts.contact_id = $contact_id
|
||||
AND software_archived_at IS NULL
|
||||
ORDER BY software_name ASC"
|
||||
);
|
||||
$software_count = mysqli_num_rows($sql_linked_software);
|
||||
// Licenses they hold a seat on, with each one's seats in use and the key they were given
|
||||
$held_software = $can_support ? softwareHeldBy('contact', $contact_id) : [];
|
||||
$software_count = count($held_software);
|
||||
|
||||
// Documents
|
||||
$sql_linked_documents = mysqli_query($mysqli, "SELECT documents.document_id, document_name, document_description, document_created_at,
|
||||
@@ -215,7 +206,7 @@ if (!$can_credentials) {
|
||||
unset($contact_tabs['credentials']);
|
||||
}
|
||||
if (!$can_support) {
|
||||
unset($contact_tabs['tickets'], $contact_tabs['recurring']);
|
||||
unset($contact_tabs['tickets'], $contact_tabs['recurring'], $contact_tabs['licenses']);
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -297,6 +288,9 @@ if (!$can_support) {
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/file/file_upload.php?<?= $client_url ?>&contact_id=<?= $contact_id ?>">
|
||||
<i class="fa fa-fw fa-upload text-secondary me-2"></i>Upload File(s)
|
||||
</a>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/software/software_add.php?client_id=<?= $client_id ?>&contact_id=<?= $contact_id ?>">
|
||||
<i class="fa fa-fw fa-cube text-secondary me-2"></i>License
|
||||
</a>
|
||||
<?php } ?>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/contact/contact_note_add.php?id=<?= $contact_id ?>">
|
||||
<i class="fas fa-fw fa-sticky-note text-secondary me-2"></i>Note
|
||||
@@ -314,9 +308,11 @@ if (!$can_support) {
|
||||
<i class="fa fa-fw fa-key text-secondary me-2"></i>Credential
|
||||
</a>
|
||||
<?php } ?>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/contact/contact_link_software.php?id=<?= $contact_id ?>">
|
||||
<i class="fa fa-fw fa-cube text-secondary me-2"></i>License
|
||||
</a>
|
||||
<?php if ($can_support_edit) { ?>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/link/link.php?from=contact&id=<?= $contact_id ?>&to=software">
|
||||
<i class="fa fa-fw fa-cube text-secondary me-2"></i>License
|
||||
</a>
|
||||
<?php } ?>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/contact/contact_link_service.php?id=<?= $contact_id ?>">
|
||||
<i class="fa fa-fw fa-stream text-secondary me-2"></i>Service
|
||||
</a>
|
||||
@@ -710,52 +706,29 @@ if (!$can_support) {
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($can_support) { ?>
|
||||
<!-- Licenses -->
|
||||
<div class="tab-pane fade" id="contact-pane-licenses" role="tabpanel">
|
||||
<div class="itflow-asset-pane-bar">
|
||||
<span class="text-body-secondary small">Software licensed to them. Seats count every asset and contact on the license.</span>
|
||||
<?php if ($can_edit) { ?>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary ajax-modal" data-modal-url="modals/contact/contact_link_software.php?id=<?= $contact_id ?>">
|
||||
<i class="fas fa-link me-2"></i>Link License
|
||||
</button>
|
||||
<span class="text-body-secondary small">Licenses they hold a seat on, and the key they were given.</span>
|
||||
<?php if ($can_support_edit) { ?>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary ajax-modal" data-modal-url="modals/link/link.php?from=contact&id=<?= $contact_id ?>&to=software">
|
||||
<i class="fas fa-link me-2"></i>Link
|
||||
</button>
|
||||
<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 ?>&contact_id=<?= $contact_id ?>">
|
||||
<i class="fas fa-plus me-2"></i>New License
|
||||
</button>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php if ($software_count) { ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="text-nowrap table-light">
|
||||
<tr><th>License</th><th>Type</th><th>Seats</th><th>Expires</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php while ($row = mysqli_fetch_assoc($sql_linked_software)) {
|
||||
$software_id = intval($row['software_id']);
|
||||
$seats = intval($row['software_seats']);
|
||||
$seats_used = intval($row['seats_used']); ?>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="fw-semibold"><?= escapeHtml($row['software_name']) ?></span>
|
||||
<?php if ($row['software_version']) { ?><span class="text-body-secondary ms-1"><?= escapeHtml($row['software_version']) ?></span><?php } ?>
|
||||
</td>
|
||||
<td>
|
||||
<?= escapeHtml($row['software_type']) ?: '<span class="text-body-secondary">-</span>' ?>
|
||||
<?php if ($row['software_license_type']) { ?><div class="small text-body-secondary"><?= escapeHtml($row['software_license_type']) ?></div><?php } ?>
|
||||
</td>
|
||||
<td class="text-nowrap <?= $seats && $seats_used > $seats ? 'text-danger-emphasis' : '' ?>"><?= $seats_used ?><?= $seats ? " / $seats" : '' ?></td>
|
||||
<td class="text-nowrap"><?= escapeHtml($row['software_expire']) ?: '<span class="text-body-secondary">-</span>' ?></td>
|
||||
<td class="text-end">
|
||||
<?php if ($can_edit) { ?>
|
||||
<a class="btn btn-sm btn-light" title="Unlink" href="post.php?unlink_software_from_contact&contact_id=<?= $contact_id ?>&software_id=<?= $software_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"><i class="fas fa-fw fa-unlink"></i></a>
|
||||
<?php } ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?= softwareHeldTable($held_software, 'contact', $contact_id, $can_support_edit) ?>
|
||||
<?php } else { ?>
|
||||
<div class="itflow-asset-empty">No licenses.</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Documents -->
|
||||
<div class="tab-pane fade" id="contact-pane-documents" role="tabpanel">
|
||||
|
||||
692
agent/license.php
Normal file
692
agent/license.php
Normal file
@@ -0,0 +1,692 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* License - one license (a row in the software table).
|
||||
*
|
||||
* What an MSP needs from it in one place: when it renews and whether the
|
||||
* seats add up (header), the key and order details to copy, who to call at
|
||||
* the vendor and their account number, and then everything hung off it in
|
||||
* tabs with counts - the devices and users on it (each with the key they were
|
||||
* given), the individual keys and who has each, the portal logins, documents
|
||||
* and files, and its history from the audit log. The open tab follows the
|
||||
* URL hash (#keys) 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";
|
||||
$client_url = "client_id=$client_id&";
|
||||
} else {
|
||||
require_once "includes/inc_client_overview_all.php";
|
||||
$client_url = '';
|
||||
}
|
||||
|
||||
// Perms
|
||||
enforceUserPermission('module_support');
|
||||
|
||||
$software_id = intval($_GET['software_id'] ?? 0);
|
||||
$license = softwareRow($software_id);
|
||||
|
||||
if (!$license || ($client_url && intval($license['software_client_id']) !== intval($client_id))) {
|
||||
|
||||
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 = intval($license['software_client_id']);
|
||||
enforceClientAccess();
|
||||
$client_url = "client_id=$client_id&";
|
||||
|
||||
$client_name = escapeHtml($license['client_name']);
|
||||
$software_name = escapeHtml($license['software_name']);
|
||||
$software_version = escapeHtml($license['software_version']);
|
||||
$software_type = escapeHtml($license['software_type']);
|
||||
$software_description = escapeHtml($license['software_description']);
|
||||
$software_license_type = escapeHtml($license['software_license_type']);
|
||||
$software_key = $license['software_key'];
|
||||
$software_purchase_reference = escapeHtml($license['software_purchase_reference']);
|
||||
$software_purchase = escapeHtml($license['software_purchase']);
|
||||
$software_purchase_age = assetAge($license['software_purchase']);
|
||||
$software_notes = escapeHtml($license['software_notes']);
|
||||
$software_favorite = intval($license['software_favorite']);
|
||||
$software_created_at = escapeHtml($license['software_created_at']);
|
||||
$software_archived_at = escapeHtml($license['software_archived_at']);
|
||||
$vendor_id = intval($license['vendor_id']);
|
||||
$vendor_name = escapeHtml($license['vendor_name']);
|
||||
|
||||
$seats = softwareSeats($license['seats_used'], $license['software_seats']);
|
||||
$renewal = softwareRenewal($license['software_expire']);
|
||||
|
||||
$can_edit = lookupUserPermission('module_support') >= 2 && !$software_archived_at;
|
||||
$can_restore = lookupUserPermission('module_support') >= 2;
|
||||
$can_delete = lookupUserPermission('module_support') >= 3 && !empty($config_destructive_deletes_enable);
|
||||
$can_credentials = lookupUserPermission('module_credential') >= 1;
|
||||
$can_link_credentials = $can_edit && lookupUserPermission('module_credential') >= 2;
|
||||
|
||||
// Override Tab Title // No Sanitizing needed as this var will only be used in the tab title
|
||||
$page_title = $license['software_name'];
|
||||
|
||||
$holders = softwareSeatHolders($software_id);
|
||||
$keys = softwareKeys($software_id);
|
||||
$keys_free = count(array_filter($keys, function ($key) {
|
||||
return !$key['assets'] && !$key['contacts'];
|
||||
}));
|
||||
$credentials = $can_credentials ? linkedRecords('software', $software_id, 'credential') : [];
|
||||
$documents = linkedRecords('software', $software_id, 'document');
|
||||
$files = linkedRecords('software', $software_id, 'file');
|
||||
$history = softwareHistory($software_id, 100);
|
||||
|
||||
$license_tabs = [
|
||||
'seats' => ['label' => 'Seats', 'icon' => 'fa-users', 'count' => $seats['used']],
|
||||
'keys' => ['label' => 'Keys', 'icon' => 'fa-key', 'count' => count($keys) + ($software_key ? 1 : 0)],
|
||||
'credentials' => ['label' => 'Credentials', 'icon' => 'fa-lock', 'count' => count($credentials)],
|
||||
'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_credentials) {
|
||||
unset($license_tabs['credentials']);
|
||||
}
|
||||
|
||||
// A renewal ticket, filled in with what the person doing it needs
|
||||
$renewal_subject = 'Renew ' . $license['software_name'] . ($renewal ? ' - ' . strtolower($renewal['text']) : '');
|
||||
// Plain text, one fact a line - the ticket form escapes it
|
||||
$renewal_details = implode("\n", array_filter([
|
||||
'Renew ' . trim($license['software_name'] . ' ' . $license['software_version']) . ' for ' . $license['client_name'] . '.',
|
||||
$renewal ? 'Renewal date: ' . $renewal['date'] : '',
|
||||
$seats['state'] !== 'unlimited' ? 'Seats: ' . $seats['used'] . ' in use of ' . $seats['total'] : 'Seats in use: ' . $seats['used'],
|
||||
$license['vendor_name'] ? 'Vendor: ' . $license['vendor_name'] . ($license['vendor_account_number'] ? ' (account ' . $license['vendor_account_number'] . ')' : '') : '',
|
||||
$license['software_purchase_reference'] ? 'Last order: ' . $license['software_purchase_reference'] : '',
|
||||
]));
|
||||
$renewal_ticket_url = 'modals/ticket/ticket_add.php?' . http_build_query(['client_id' => $client_id, 'subject' => $renewal_subject, 'details' => $renewal_details]);
|
||||
|
||||
$link_url = "modals/link/link.php?from=software&id=$software_id&to=";
|
||||
$unlink_url = function ($to, $other_id) use ($software_id) {
|
||||
return "post.php?unlink_item&from=software&id=$software_id&to=$to&other=" . intval($other_id) . "&csrf_token=" . $_SESSION['csrf_token'];
|
||||
};
|
||||
|
||||
?>
|
||||
|
||||
<ol class="breadcrumb d-print-none">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="client_overview.php?client_id=<?= $client_id ?>"><?= $client_name ?></a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="software.php?client_id=<?= $client_id ?>">Licenses</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active"><?= $software_name ?></li>
|
||||
</ol>
|
||||
|
||||
<!-- What it is, when it renews, whether the seats add up -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body d-flex flex-wrap align-items-center gap-3">
|
||||
<?= softwareTile($license['software_type'], '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><?= $software_name ?></span>
|
||||
<?php if ($software_version) { ?><span class="text-body-secondary fw-normal"><?= $software_version ?></span><?php } ?>
|
||||
<?php if ($software_favorite) { ?><i class="fas fa-star text-warning fs-6" title="Favorite"></i><?php } ?>
|
||||
<?php if ($software_archived_at) { ?><span class="badge rounded-pill text-bg-secondary fw-normal fs-6">Archived</span><?php } ?>
|
||||
<span class="fs-6"><?= softwareRenewalPill($renewal, true) ?></span>
|
||||
<?php if ($seats['state'] === 'over') { ?>
|
||||
<span class="fs-6"><span class="badge rounded-pill bg-danger-subtle text-danger-emphasis border border-danger-subtle fw-normal"><i class="fas fa-exclamation-triangle me-1"></i><?= $seats['over'] ?> over seats</span></span>
|
||||
<?php } ?>
|
||||
</h1>
|
||||
<div class="text-body-secondary small d-flex flex-wrap column-gap-3 row-gap-1">
|
||||
<?php if ($software_type) { ?><span><i class="fas fa-fw <?= softwareTypeStyle($license['software_type'])['icon'] ?> me-1"></i><?= $software_type ?></span><?php } ?>
|
||||
<?php if ($software_license_type) { ?><span><i class="fas fa-fw <?= softwareLicenseTypeIcon($license['software_license_type']) ?> me-1"></i><?= $software_license_type ?></span><?php } ?>
|
||||
<?php if ($vendor_name) { ?><span><i class="fas fa-fw fa-building me-1"></i><?= $vendor_name ?></span><?php } ?>
|
||||
<?php if ($software_description) { ?><span><?= $software_description ?></span><?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 d-print-none">
|
||||
<?php if ($can_edit) { ?>
|
||||
<button type="button" class="btn btn-outline-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/software/software_renew.php?id=<?= $software_id ?>">
|
||||
<i class="fas fa-redo-alt me-2"></i>Renew
|
||||
</button>
|
||||
<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">
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="<?= escapeHtml($renewal_ticket_url) ?>">
|
||||
<i class="fa fa-fw fa-life-ring text-secondary me-2"></i>Renewal Ticket
|
||||
</a>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/software/software_key_add.php?software_id=<?= $software_id ?>">
|
||||
<i class="fa fa-fw fa-key text-secondary me-2"></i>Keys
|
||||
</a>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-size="xl" data-modal-url="modals/document/document_add.php?client_id=<?= $client_id ?>&software_id=<?= $software_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 ?>&software_id=<?= $software_id ?>">
|
||||
<i class="fa fa-fw fa-upload text-secondary me-2"></i>Upload File(s)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown"><i class="fas fa-link me-2"></i>Link</button>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="<?= $link_url ?>asset"><i class="fa fa-fw fa-desktop text-secondary me-2"></i>Device</a>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="<?= $link_url ?>contact"><i class="fa fa-fw fa-user text-secondary me-2"></i>User</a>
|
||||
<?php if ($can_link_credentials) { ?>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="<?= $link_url ?>credential"><i class="fa fa-fw fa-lock text-secondary me-2"></i>Credential</a>
|
||||
<?php } ?>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="<?= $link_url ?>document"><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="<?= $link_url ?>file"><i class="fa fa-fw fa-paperclip text-secondary me-2"></i>File</a>
|
||||
</div>
|
||||
</div>
|
||||
<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 } ?>
|
||||
<?php if ($can_restore) { ?>
|
||||
<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 ($software_archived_at) { ?>
|
||||
<a class="dropdown-item" href="post.php?restore_software=<?= $software_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_software=<?= $software_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"
|
||||
data-confirm-text="<?= $software_name ?> goes for good, with its seats and keys.">
|
||||
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
||||
</a>
|
||||
<?php } ?>
|
||||
<?php } else { ?>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/software/software_seats.php?id=<?= $software_id ?>">
|
||||
<i class="fas fa-fw fa-users text-secondary me-2"></i>Seats
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger confirm-link" href="post.php?archive_software=<?= $software_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"
|
||||
data-confirm-text="It drops off every device and user until it is restored.">
|
||||
<i class="fas fa-fw fa-archive me-2"></i>Archive
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- The facts: seats, key, order, dates - then the vendor -->
|
||||
<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-cube text-secondary me-2"></i>License</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="itflow-asset-dl mb-0">
|
||||
<dt>Seats</dt>
|
||||
<dd><?= softwareSeatBar($seats) ?></dd>
|
||||
<?php if ($software_license_type) { ?>
|
||||
<dt>Type</dt>
|
||||
<dd><i class="fas fa-fw <?= softwareLicenseTypeIcon($license['software_license_type']) ?> text-secondary me-1"></i><?= $software_license_type ?></dd>
|
||||
<?php } ?>
|
||||
<dt>Key</dt>
|
||||
<dd>
|
||||
<?php if ($software_key) { ?>
|
||||
<?= softwareKeyHtml($software_key) ?>
|
||||
<?php } elseif ($keys) { ?>
|
||||
<a href="#keys" data-license-tab-link="keys"><?= count($keys) ?> individual key<?= count($keys) === 1 ? '' : 's' ?></a>
|
||||
<?php } else { ?>
|
||||
<span class="text-body-secondary">None recorded</span>
|
||||
<?php } ?>
|
||||
</dd>
|
||||
<dt>Renews</dt>
|
||||
<dd>
|
||||
<?php if ($renewal) { ?>
|
||||
<?= escapeHtml($renewal['date']) ?>
|
||||
<div class="small text-<?= $renewal['tone'] === 'success' ? 'body-secondary' : $renewal['tone'] . '-emphasis' ?>"><?= escapeHtml($renewal['text']) ?></div>
|
||||
<?php } else { ?>
|
||||
<span class="text-body-secondary">No date</span>
|
||||
<?php } ?>
|
||||
</dd>
|
||||
<?php if ($software_purchase) { ?>
|
||||
<dt>Purchased</dt>
|
||||
<dd><?= $software_purchase ?><?php if ($software_purchase_age) { ?><div class="small text-body-secondary"><?= $software_purchase_age ?> ago</div><?php } ?></dd>
|
||||
<?php } ?>
|
||||
<?php if ($software_purchase_reference) { ?>
|
||||
<dt>Order ref</dt>
|
||||
<dd><?= $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></dd>
|
||||
<?php } ?>
|
||||
<dt>Added</dt><dd title="<?= $software_created_at ?>"><?= timeAgo($software_created_at) ?></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($vendor_name) {
|
||||
$vendor_phone = contactPhoneLink($license['vendor_phone'], $license['vendor_phone_country_code'], $license['vendor_extension']);
|
||||
$vendor_email = escapeHtml($license['vendor_email']);
|
||||
$vendor_website = escapeHtml($license['vendor_website']);
|
||||
$vendor_account = escapeHtml($license['vendor_account_number']); ?>
|
||||
<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-building text-secondary me-2"></i>Vendor</h3>
|
||||
<a class="small ajax-modal" href="#" data-modal-url="modals/vendor/vendor.php?id=<?= $vendor_id ?>"><?= $vendor_name ?></a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="itflow-asset-dl mb-0">
|
||||
<?php if ($vendor_account) { ?>
|
||||
<dt>Account</dt>
|
||||
<dd><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></dd>
|
||||
<?php } ?>
|
||||
<?php if ($vendor_phone) { ?>
|
||||
<dt>Phone</dt><dd><?= $vendor_phone ?></dd>
|
||||
<?php } ?>
|
||||
<?php if ($vendor_email) { ?>
|
||||
<dt>Email</dt>
|
||||
<dd><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) { ?>
|
||||
<dt>Website</dt><dd><a href="<?= escapeUrl($license['vendor_website']) ?>" target="_blank" rel="noopener"><?= $vendor_website ?><i class="fas fa-external-link-alt ms-1 small"></i></a></dd>
|
||||
<?php } ?>
|
||||
<?php if ($license['vendor_hours']) { ?>
|
||||
<dt>Hours</dt><dd><?= escapeHtml($license['vendor_hours']) ?></dd>
|
||||
<?php } ?>
|
||||
<?php if ($license['vendor_sla']) { ?>
|
||||
<dt>SLA</dt><dd><?= escapeHtml($license['vendor_sla']) ?></dd>
|
||||
<?php } ?>
|
||||
<?php if (!$vendor_account && !$vendor_phone && !$vendor_email && !$vendor_website && !$license['vendor_hours'] && !$license['vendor_sla']) { ?>
|
||||
<dt>Contact</dt><dd class="text-body-secondary">Nothing recorded on the vendor yet</dd>
|
||||
<?php } ?>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title mb-0"><i class="fas fa-fw fa-pen text-secondary me-2"></i>Notes</h3>
|
||||
</div>
|
||||
<textarea class="form-control border-0 rounded-0 rounded-bottom itflow-license-notes" rows="6" placeholder="Admin portal, how it is billed, who to call to renew - saves when you click away"
|
||||
data-software-id="<?= $software_id ?>" data-csrf="<?= escapeHtml($_SESSION['csrf_token']) ?>" <?= $can_edit ? '' : 'readonly' ?>><?= $software_notes ?></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Everything hung off it -->
|
||||
<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 ($license_tabs as $tab_key => $tab) { ?>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link text-nowrap <?= $tab['count'] ? '' : 'is-empty' ?>" id="license-tab-<?= $tab_key ?>" data-bs-toggle="pill" data-bs-target="#license-pane-<?= $tab_key ?>" data-license-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'] ?><?= $tab_key === 'seats' && $seats['total'] ? ' / ' . $seats['total'] : '' ?></span>
|
||||
</button>
|
||||
</li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
|
||||
<!-- Seats: the devices and users on it -->
|
||||
<div class="tab-pane fade" id="license-pane-seats" role="tabpanel">
|
||||
<div class="itflow-asset-pane-bar">
|
||||
<span class="text-body-secondary small">
|
||||
<?php if ($seats['state'] === 'unlimited') { ?>
|
||||
<?= $seats['used'] ?> in use - no seat limit.
|
||||
<?php } else { ?>
|
||||
<?= $seats['used'] ?> of <?= $seats['total'] ?> seats in use<?= $seats['over'] ? ' - <span class="text-danger-emphasis fw-semibold">' . $seats['over'] . ' over</span>' : ($seats['free'] ? ', ' . $seats['free'] . ' free' : '') ?>.
|
||||
<?php } ?>
|
||||
</span>
|
||||
<?php if ($can_edit) { ?>
|
||||
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-url="modals/software/software_seats.php?id=<?= $software_id ?>">
|
||||
<i class="fas fa-users me-2"></i>Manage Seats
|
||||
</button>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php if ($holders['assets']) { ?>
|
||||
<div class="itflow-license-section"><i class="fas fa-fw fa-desktop me-1"></i>Devices <span class="badge rounded-pill ms-1"><?= count($holders['assets']) ?></span></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<tbody>
|
||||
<?php foreach ($holders['assets'] as $holder) {
|
||||
$holder_id = intval($holder['asset_id']); ?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<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">
|
||||
<a class="fw-semibold ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/asset/asset.php?id=<?= $holder_id ?>"><?= escapeHtml($holder['asset_name']) ?></a>
|
||||
<div class="small text-body-secondary text-truncate"><?= escapeHtml(implode(' · ', array_filter([$holder['asset_type'], trim($holder['asset_make'] . ' ' . $holder['asset_model'])]))) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($holder['asset_contact_name']) { ?>
|
||||
<?= contactLink($client_id, $holder['asset_contact_id'], $holder['asset_contact_name'], $holder['asset_contact_photo']) ?>
|
||||
<?php } else { ?>
|
||||
<span class="text-body-secondary small">Nobody assigned</span>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td class="itflow-license-key-cell">
|
||||
<?php foreach ($holder['held_keys'] as $held_key) { ?>
|
||||
<div><?= softwareKeyHtml($held_key) ?></div>
|
||||
<?php } ?>
|
||||
<?php if (!$holder['held_keys'] && $software_key) { ?>
|
||||
<span class="small text-body-secondary" title="No key of its own - the license key">License key</span>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<?php if ($can_edit) { ?>
|
||||
<a href="<?= $unlink_url('asset', $holder_id) ?>" class="btn btn-sm btn-light confirm-link" title="Take off this license" data-confirm-text="Frees the seat, and any key it had."><i class="fas fa-fw fa-unlink"></i></a>
|
||||
<?php } ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<?php if ($holders['contacts']) { ?>
|
||||
<div class="itflow-license-section"><i class="fas fa-fw fa-user me-1"></i>Users <span class="badge rounded-pill ms-1"><?= count($holders['contacts']) ?></span></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<tbody>
|
||||
<?php foreach ($holders['contacts'] as $holder) {
|
||||
$holder_id = intval($holder['contact_id']);
|
||||
$holder_email = escapeHtml($holder['contact_email']); ?>
|
||||
<tr>
|
||||
<td>
|
||||
<?= contactLink($holder['contact_client_id'], $holder_id, $holder['contact_name'], $holder['contact_photo'], ['size' => 'sm', 'class' => 'fw-semibold']) ?>
|
||||
<?php if ($holder['contact_title'] || $holder_email) { ?>
|
||||
<div class="small text-body-secondary itflow-license-holder-sub text-truncate">
|
||||
<?= escapeHtml($holder['contact_title']) ?><?= $holder['contact_title'] && $holder_email ? ' · ' : '' ?><?php if ($holder_email) { ?><a class="text-reset" href="mailto:<?= $holder_email ?>"><?= $holder_email ?></a><button type="button" class="itflow-copy clipboardjs" title="Copy email" data-clipboard-text="<?= $holder_email ?>"><i class="far fa-copy"></i></button><?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td class="itflow-license-key-cell">
|
||||
<?php foreach ($holder['held_keys'] as $held_key) { ?>
|
||||
<div><?= softwareKeyHtml($held_key) ?></div>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<?php if ($can_edit) { ?>
|
||||
<a href="<?= $unlink_url('contact', $holder_id) ?>" class="btn btn-sm btn-light confirm-link" title="Take off this license" data-confirm-text="Frees the seat, and any key they had."><i class="fas fa-fw fa-unlink"></i></a>
|
||||
<?php } ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<?php if (!$holders['assets'] && !$holders['contacts']) { ?>
|
||||
<div class="itflow-asset-empty">
|
||||
<div>Nothing and nobody on this license yet.</div>
|
||||
<?php if ($can_edit) { ?>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary mt-3 ajax-modal" data-modal-url="modals/software/software_seats.php?id=<?= $software_id ?>"><i class="fas fa-users me-2"></i>Add devices and users</button>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<!-- Keys: the license key, and a key per device or user -->
|
||||
<div class="tab-pane fade" id="license-pane-keys" role="tabpanel">
|
||||
<div class="itflow-asset-pane-bar">
|
||||
<span class="text-body-secondary small">
|
||||
<?php if ($keys) { ?>
|
||||
<?= count($keys) ?> individual key<?= count($keys) === 1 ? '' : 's' ?><?= $keys_free ? ", $keys_free not handed out" : ', all handed out' ?>. A key shows on the page of whoever has it.
|
||||
<?php } else { ?>
|
||||
A key per device or user, for licenses sold that way. Each shows on the page of whoever has it.
|
||||
<?php } ?>
|
||||
</span>
|
||||
<?php if ($can_edit) { ?>
|
||||
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-url="modals/software/software_key_add.php?software_id=<?= $software_id ?>">
|
||||
<i class="fas fa-plus me-2"></i>Add Keys
|
||||
</button>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php if ($software_key) { ?>
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 px-3 py-2 border-bottom itflow-license-main-key">
|
||||
<span class="badge rounded-pill text-bg-primary fw-normal">License key</span>
|
||||
<?= softwareKeyHtml($software_key, 'itflow-copy-always') ?>
|
||||
<span class="small text-body-secondary ms-auto">Covers every seat - change it with Edit</span>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<?php if ($keys) { ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="text-nowrap table-light">
|
||||
<tr><th>Key</th><th>Handed to</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($keys as $key) { ?>
|
||||
<tr>
|
||||
<td class="itflow-license-key-cell"><?= softwareKeyHtml($key['key']) ?></td>
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<?php foreach ($key['assets'] as $key_asset) { ?>
|
||||
<a class="itflow-contact-chip ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/asset/asset.php?id=<?= intval($key_asset['asset_id']) ?>">
|
||||
<i class="fas fa-fw fa-<?= getAssetIcon($key_asset['asset_type']) ?> text-secondary"></i><?= escapeHtml($key_asset['asset_name']) ?>
|
||||
</a>
|
||||
<?php } ?>
|
||||
<?php foreach ($key['contacts'] as $key_contact) { ?>
|
||||
<?= contactLink($key_contact['contact_client_id'], $key_contact['contact_id'], $key_contact['contact_name'], $key_contact['contact_photo'], ['archived' => !empty($key_contact['contact_archived_at'])]) ?>
|
||||
<?php } ?>
|
||||
<?php if (!$key['assets'] && !$key['contacts']) { ?>
|
||||
<span class="badge rounded-pill bg-success-subtle text-success-emphasis border border-success-subtle fw-normal">Free</span>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end text-nowrap">
|
||||
<?php if ($can_edit) { ?>
|
||||
<button type="button" class="btn btn-sm btn-light ajax-modal" title="Hand out or change" data-modal-url="modals/software/software_key_edit.php?id=<?= $key['id'] ?>"><i class="fas fa-fw fa-user-tag"></i></button>
|
||||
<a href="post.php?delete_software_key=<?= $key['id'] ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>" class="btn btn-sm btn-light text-danger confirm-link" title="Delete key" data-confirm-text="The key is removed from the license and from whoever has it."><i class="fas fa-fw fa-trash"></i></a>
|
||||
<?php } ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php } elseif (!$software_key) { ?>
|
||||
<div class="itflow-asset-empty">No keys recorded.</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<?php if ($can_credentials) { ?>
|
||||
<!-- Credentials: the vendor portal, the admin console -->
|
||||
<div class="tab-pane fade" id="license-pane-credentials" role="tabpanel">
|
||||
<div class="itflow-asset-pane-bar">
|
||||
<span class="text-body-secondary small">Logins that go with it - the vendor portal, the admin console. Passwords are fetched only when you ask.</span>
|
||||
<?php if ($can_link_credentials) { ?>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary ajax-modal" data-modal-url="<?= $link_url ?>credential">
|
||||
<i class="fas fa-link me-2"></i>Link
|
||||
</button>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php if ($credentials) { ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="text-nowrap table-light">
|
||||
<tr><th>Name</th><th>Username</th><th>Password</th><th>OTP</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($credentials as $credential) {
|
||||
$credential_id = intval($credential['id']);
|
||||
$credential_uri = escapeHtml($credential['credential_uri']);
|
||||
$credential_username = escapeHtml(decryptCredentialEntry($credential['credential_username'])); ?>
|
||||
<tr>
|
||||
<td>
|
||||
<a class="text-body fw-semibold ajax-modal" href="#" data-modal-url="modals/credential/credential_edit.php?id=<?= $credential_id ?>"><?= escapeHtml($credential['name']) ?></a>
|
||||
<?php if ($credential_uri) { ?><a class="ms-1 small" href="<?= escapeUrl($credential['credential_uri']) ?>" target="_blank" rel="noopener" title="<?= $credential_uri ?>"><i class="fas fa-external-link-alt"></i></a><?php } ?>
|
||||
<?php if ($credential['credential_description']) { ?><div class="small text-body-secondary"><?= escapeHtml($credential['credential_description']) ?></div><?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>
|
||||
<td class="text-nowrap">
|
||||
<?php if (!empty($credential['credential_otp_secret'])) { ?>
|
||||
<span onmouseenter="showOTPViaCredentialID(<?= $credential_id ?>)"><i class="far fa-clock text-secondary me-1"></i><span id="otp_<?= $credential_id ?>"><i>Hover...</i></span></span>
|
||||
<?php } else { ?>
|
||||
<span class="text-body-secondary">-</span>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<?php if ($can_link_credentials) { ?>
|
||||
<a href="<?= $unlink_url('credential', $credential_id) ?>" class="btn btn-sm btn-light" title="Unlink"><i class="fas fa-fw fa-unlink"></i></a>
|
||||
<?php } ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php } else { ?>
|
||||
<div class="itflow-asset-empty">No credentials linked.</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Documents and files -->
|
||||
<?php foreach (['documents' => ['document', $documents, 'Setup guides, the renewal process, what is included.', 'No documents linked.'],
|
||||
'files' => ['file', $files, 'Invoices, license certificates, installers, exports.', 'No files linked.']] as $pane => [$to, $linked, $hint, $empty]) { ?>
|
||||
<div class="tab-pane fade" id="license-pane-<?= $pane ?>" role="tabpanel">
|
||||
<div class="itflow-asset-pane-bar">
|
||||
<span class="text-body-secondary small"><?= $hint ?></span>
|
||||
<?php if ($can_edit) { ?>
|
||||
<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 === '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 ?>&software_id=<?= $software_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 ?>&software_id=<?= $software_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_edit) { ?>
|
||||
<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 } ?>
|
||||
|
||||
<!-- History, from the audit log -->
|
||||
<div class="tab-pane fade" id="license-pane-history" role="tabpanel">
|
||||
<div class="itflow-asset-pane-bar">
|
||||
<span class="text-body-secondary small">Renewals, seat and key changes, edits - newest first.</span>
|
||||
</div>
|
||||
<?php if ($history) {
|
||||
$history_icons = ['Create' => 'fa-plus', 'Edit' => 'fa-edit', 'Renew' => 'fa-redo-alt', 'Seats' => 'fa-users', 'Keys' => 'fa-key',
|
||||
'Link' => 'fa-link', 'Unlink' => 'fa-unlink', 'Archive' => 'fa-archive', 'Restore' => 'fa-redo', 'Export' => 'fa-download']; ?>
|
||||
<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') ?> · <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-license-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', {
|
||||
software_set_notes: 'TRUE',
|
||||
csrf_token: notes.dataset.csrf,
|
||||
software_id: notes.dataset.softwareId,
|
||||
notes: notes.value
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* Which tab is open: the URL hash wins (#keys), then the tab last used
|
||||
* on any license, then Seats. 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.licenseTab';
|
||||
var tabs = document.querySelectorAll('[data-license-tab]');
|
||||
if (!tabs.length) {
|
||||
return;
|
||||
}
|
||||
function byName(name) {
|
||||
return document.querySelector('[data-license-tab="' + name + '"]');
|
||||
}
|
||||
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-license-tab');
|
||||
try { localStorage.setItem(key, name); } catch (e) {}
|
||||
history.replaceState(null, '', location.pathname + location.search + '#' + name);
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('[data-license-tab-link]').forEach(function (link) {
|
||||
link.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
bootstrap.Tab.getOrCreateInstance(byName(link.dataset.licenseTabLink)).show();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php if ($credentials) { ?>
|
||||
<!-- Fetch TOTP codes and passwords only when asked for -->
|
||||
<script src="js/credential_show_otp_via_id.js"></script>
|
||||
<script src="js/credential_show_password_via_id.js"></script>
|
||||
<?php } ?>
|
||||
|
||||
<?php
|
||||
}
|
||||
|
||||
require_once "../includes/footer.php";
|
||||
@@ -141,17 +141,9 @@ $sql_related_recurring_tickets = mysqli_query($mysqli, "SELECT recurring_tickets
|
||||
);
|
||||
$recurring_ticket_count = mysqli_num_rows($sql_related_recurring_tickets);
|
||||
|
||||
// Licenses, with seats in use counted in the same query
|
||||
$sql_related_software = mysqli_query($mysqli, "SELECT software_assets.software_id, software_name, software_version, software_seats,
|
||||
(SELECT COUNT(*) FROM software_assets AS sa WHERE sa.software_id = software.software_id)
|
||||
+ (SELECT COUNT(*) FROM software_contacts AS sc WHERE sc.software_id = software.software_id) AS seats_used
|
||||
FROM software_assets
|
||||
LEFT JOIN software ON software_assets.software_id = software.software_id
|
||||
WHERE software_assets.asset_id = $asset_id
|
||||
AND software_archived_at IS NULL
|
||||
ORDER BY software_name ASC"
|
||||
);
|
||||
$software_count = mysqli_num_rows($sql_related_software);
|
||||
// Licenses it holds a seat on, with the key it was given
|
||||
$held_software = softwareHeldBy('asset', $asset_id);
|
||||
$software_count = count($held_software);
|
||||
|
||||
// Documents
|
||||
$linked_documents = linkedRecords('asset', $asset_id, 'document');
|
||||
@@ -450,15 +442,8 @@ ob_start();
|
||||
|
||||
<?php if ($software_count) { ?>
|
||||
<div class="tab-pane fade <?= $first_tab === 'licenses' ? 'show active' : '' ?>" id="asset-quick-<?= $asset_id ?>-licenses" role="tabpanel">
|
||||
<ul class="list-group list-group-flush">
|
||||
<?php while ($row = mysqli_fetch_assoc($sql_related_software)) {
|
||||
$seats = intval($row['software_seats']); ?>
|
||||
<li class="list-group-item px-0 d-flex justify-content-between gap-3">
|
||||
<span><?= escapeHtml($row['software_name']) ?> <span class="text-body-secondary"><?= escapeHtml($row['software_version']) ?></span></span>
|
||||
<span class="small text-nowrap <?= $seats && intval($row['seats_used']) > $seats ? 'text-danger-emphasis' : 'text-body-secondary' ?>"><?= intval($row['seats_used']) ?><?= $seats ? " / $seats" : '' ?> seats</span>
|
||||
</li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
<!-- Each opens its quick look - seats, renewal, the key to install -->
|
||||
<?= softwareHeldList($held_software) ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
|
||||
@@ -1,72 +1,13 @@
|
||||
<?php
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
/*
|
||||
* Kept so existing links keep working. Every pair is linked in
|
||||
* agent/modals/link/link.php, here opened on its Licenses tab - every license
|
||||
* the client has, with how many seats each has left.
|
||||
*/
|
||||
|
||||
$asset_id = intval($_GET['id']);
|
||||
$_GET['from'] = 'asset';
|
||||
$_GET['id'] = intval($_GET['id'] ?? 0);
|
||||
$_GET['to'] = 'software';
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT asset_client_id, asset_name FROM assets
|
||||
WHERE asset_id = $asset_id
|
||||
LIMIT 1
|
||||
");
|
||||
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$asset_name = escapeHtml($row['asset_name']);
|
||||
$client_id = intval($row['asset_client_id']);
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fa fa-fw fa-cube me-2"></i>License Software to <strong><?= $asset_name ?></strong></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="asset_id" value="<?= $asset_id ?>">
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-cube"></i></span>
|
||||
<select class="form-select select2" name="software_id">
|
||||
<option value="">- Select a Device Software License -</option>
|
||||
<?php
|
||||
$sql_software_select = mysqli_query($mysqli, "
|
||||
SELECT software.software_id, software.software_name
|
||||
FROM software
|
||||
LEFT JOIN software_assets
|
||||
ON software.software_id = software_assets.software_id
|
||||
AND software_assets.asset_id = $asset_id
|
||||
WHERE software.software_client_id = $client_id
|
||||
AND software.software_archived_at IS NULL
|
||||
AND software.software_license_type = 'Device'
|
||||
AND software_assets.asset_id IS NULL
|
||||
ORDER BY software.software_name ASC
|
||||
");
|
||||
while ($row = mysqli_fetch_assoc($sql_software_select)) {
|
||||
$software_id = intval($row['software_id']);
|
||||
$software_name = escapeHtml($row['software_name']);
|
||||
|
||||
?>
|
||||
<option value="<?= $software_id ?>"><?= $software_name ?></option>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="link_software_to_asset" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Link</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
require __DIR__ . '/../link/link.php';
|
||||
|
||||
@@ -111,17 +111,9 @@ if ($can_support) {
|
||||
$recurring_ticket_count = mysqli_num_rows($sql_related_recurring_tickets);
|
||||
}
|
||||
|
||||
// Licenses, with seats in use counted in the same query
|
||||
$sql_linked_software = mysqli_query($mysqli, "SELECT software.software_id, software_name, software_version, software_seats,
|
||||
(SELECT COUNT(*) FROM software_assets AS sa WHERE sa.software_id = software.software_id)
|
||||
+ (SELECT COUNT(*) FROM software_contacts AS sc WHERE sc.software_id = software.software_id) AS seats_used
|
||||
FROM software_contacts
|
||||
JOIN software ON software.software_id = software_contacts.software_id
|
||||
WHERE software_contacts.contact_id = $contact_id
|
||||
AND software_archived_at IS NULL
|
||||
ORDER BY software_name ASC"
|
||||
);
|
||||
$software_count = mysqli_num_rows($sql_linked_software);
|
||||
// Licenses they hold a seat on, with the key they were given
|
||||
$held_software = $can_support ? softwareHeldBy('contact', $contact_id) : [];
|
||||
$software_count = count($held_software);
|
||||
|
||||
// Documents
|
||||
$linked_documents = linkedRecords('contact', $contact_id, 'document');
|
||||
@@ -373,15 +365,8 @@ ob_start();
|
||||
|
||||
<?php if ($software_count) { ?>
|
||||
<div class="tab-pane fade <?= $first_tab === 'licenses' ? 'show active' : '' ?>" id="contact-quick-<?= $contact_id ?>-licenses" role="tabpanel">
|
||||
<ul class="list-group list-group-flush">
|
||||
<?php while ($row = mysqli_fetch_assoc($sql_linked_software)) {
|
||||
$seats = intval($row['software_seats']); ?>
|
||||
<li class="list-group-item px-0 d-flex justify-content-between gap-3">
|
||||
<span><?= escapeHtml($row['software_name']) ?> <span class="text-body-secondary"><?= escapeHtml($row['software_version']) ?></span></span>
|
||||
<span class="small text-nowrap <?= $seats && intval($row['seats_used']) > $seats ? 'text-danger-emphasis' : 'text-body-secondary' ?>"><?= intval($row['seats_used']) ?><?= $seats ? " / $seats" : '' ?> seats</span>
|
||||
</li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
<!-- Each opens its quick look - seats, renewal, the key to install -->
|
||||
<?= softwareHeldList($held_software) ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
|
||||
@@ -1,69 +1,13 @@
|
||||
<?php
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
/*
|
||||
* Kept so existing links keep working. Every pair is linked in
|
||||
* agent/modals/link/link.php, here opened on its Licenses tab - every license
|
||||
* the client has, with how many seats each has left.
|
||||
*/
|
||||
|
||||
$contact_id = intval($_GET['id']);
|
||||
$_GET['from'] = 'contact';
|
||||
$_GET['id'] = intval($_GET['id'] ?? 0);
|
||||
$_GET['to'] = 'software';
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT contact_client_id, contact_name FROM contacts
|
||||
WHERE contact_id = $contact_id
|
||||
LIMIT 1
|
||||
");
|
||||
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$contact_name = escapeHtml($row['contact_name']);
|
||||
$client_id = intval($row['contact_client_id']);
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fa fa-fw fa-cube me-2"></i>License Software to <strong><?= $contact_name ?></strong></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="contact_id" value="<?= $contact_id ?>">
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-cube"></i></span>
|
||||
<select class="form-select select2" name="software_id">
|
||||
<option value="">- Select a User Software License -</option>
|
||||
<?php
|
||||
$sql_software_select = mysqli_query($mysqli, "
|
||||
SELECT software.software_id, software.software_name
|
||||
FROM software
|
||||
LEFT JOIN software_contacts
|
||||
ON software.software_id = software_contacts.software_id
|
||||
AND software_contacts.contact_id = $contact_id
|
||||
WHERE software.software_client_id = $client_id
|
||||
AND software.software_archived_at IS NULL
|
||||
AND software.software_license_type = 'User'
|
||||
AND software_contacts.contact_id IS NULL
|
||||
ORDER BY software.software_name ASC
|
||||
");
|
||||
while ($row = mysqli_fetch_assoc($sql_software_select)) {
|
||||
$software_id = intval($row['software_id']);
|
||||
$software_name = escapeHtml($row['software_name']);
|
||||
?>
|
||||
<option value="<?= $software_id ?>"><?= $software_name ?></option>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="link_software_to_contact" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Link</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
?>
|
||||
require __DIR__ . '/../link/link.php';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* New document - in the folder it was opened from, or linked to the asset or
|
||||
* contact it was opened from.
|
||||
* New document - in the folder it was opened from, or linked to the asset,
|
||||
* contact or license it was opened from.
|
||||
*/
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
@@ -15,7 +15,9 @@ enforceClientAccess();
|
||||
$contact_id = assetClientOwnedId('contact', $_GET['contact_id'] ?? 0, $client_id);
|
||||
$asset = linkRecord('asset', $_GET['asset_id'] ?? 0);
|
||||
$asset_id = $asset && intval($asset['client_id']) === $client_id ? intval($asset['id']) : 0;
|
||||
$link_name = $asset_id ? $asset['name'] : ($contact_id ? getFieldById('contacts', $contact_id, 'contact_name') : '');
|
||||
$software = linkRecord('software', $_GET['software_id'] ?? 0);
|
||||
$software_id = $software && intval($software['client_id']) === $client_id ? intval($software['id']) : 0;
|
||||
$link_name = $asset_id ? $asset['name'] : ($contact_id ? getFieldById('contacts', $contact_id, 'contact_name') : ($software_id ? $software['name'] : ''));
|
||||
|
||||
$document_form_mode = 'add';
|
||||
$document_name = $document_description = $document_content = '';
|
||||
@@ -34,6 +36,7 @@ ob_start();
|
||||
<input type="hidden" name="client_id" value="<?= $client_id ?>">
|
||||
<input type="hidden" name="contact" value="<?= $contact_id ?>">
|
||||
<input type="hidden" name="asset" value="<?= $asset_id ?>">
|
||||
<input type="hidden" name="software" value="<?= $software_id ?>">
|
||||
<div class="modal-body">
|
||||
<?php require 'document_form.php'; ?>
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
/*
|
||||
* Upload files into a folder - dropped onto the box, picked with the button,
|
||||
* or dropped straight onto the files list (agent/js/files.js opens this and
|
||||
* hands them over). Opened from an asset or a contact, the uploads are also
|
||||
* linked to it.
|
||||
* hands them over). Opened from an asset, a contact or a license, the uploads
|
||||
* are also linked to it.
|
||||
*/
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
@@ -18,7 +18,9 @@ $folder_id = folderOwnedId($_GET['folder_id'] ?? 0, $client_id);
|
||||
$contact_id = assetClientOwnedId('contact', $_GET['contact_id'] ?? 0, $client_id);
|
||||
$asset = linkRecord('asset', $_GET['asset_id'] ?? 0);
|
||||
$asset_id = $asset && intval($asset['client_id']) === $client_id ? intval($asset['id']) : 0;
|
||||
$link_name = $asset_id ? $asset['name'] : ($contact_id ? getFieldById('contacts', $contact_id, 'contact_name') : '');
|
||||
$software = linkRecord('software', $_GET['software_id'] ?? 0);
|
||||
$software_id = $software && intval($software['client_id']) === $client_id ? intval($software['id']) : 0;
|
||||
$link_name = $asset_id ? $asset['name'] : ($contact_id ? getFieldById('contacts', $contact_id, 'contact_name') : ($software_id ? $software['name'] : ''));
|
||||
|
||||
// The extensions agent/post/file.php accepts - offered to the picker so the wrong kind is never chosen
|
||||
$upload_extensions = ['jpg', 'jpeg', 'gif', 'png', 'webp', 'pdf', 'txt', 'md', 'doc', 'docx', 'odt', 'csv', 'xls', 'xlsx', 'ods',
|
||||
@@ -38,6 +40,7 @@ ob_start();
|
||||
<input type="hidden" name="client_id" value="<?= $client_id ?>">
|
||||
<input type="hidden" name="contact_id" value="<?= $contact_id ?>">
|
||||
<input type="hidden" name="asset_id" value="<?= $asset_id ?>">
|
||||
<input type="hidden" name="software_id" value="<?= $software_id ?>">
|
||||
<div class="modal-body">
|
||||
|
||||
<label class="itflow-upload-drop" data-upload-drop>
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
/*
|
||||
* Link things to a document or a file - or a document or file to an asset or
|
||||
* contact. One modal for every pair in LINK_RELATIONS (functions/links.php):
|
||||
* contact, devices, users and credentials to a license. One modal for every
|
||||
* pair in LINK_RELATIONS (functions/links.php):
|
||||
* ?from=document&id=81&to=asset opens on Assets; the pills along the top
|
||||
* switch to anything else this record can be linked to. Pick as many as you
|
||||
* like; only the client's own, unarchived, not-yet-linked records are offered.
|
||||
@@ -50,7 +51,14 @@ function linkOptionData($to, $row, $client_id) {
|
||||
case 'document':
|
||||
return selectOptionData(['icon' => 'fas fa-file-alt', 'description' => $row['document_description'] ?: 'Updated ' . timeAgo($row['document_updated_at'] ?: $row['document_created_at'])]);
|
||||
case 'software':
|
||||
return selectOptionData(['icon' => 'fas fa-cube', 'description' => trim($row['software_type'] . ' ' . $row['software_version'])]);
|
||||
// How many seats are left, so a full license is seen before it is picked
|
||||
$seats = softwareSeats($row['seats_used'], $row['software_seats']);
|
||||
$seat_text = $seats['state'] === 'unlimited' ? '' : ($seats['state'] === 'over' || $seats['state'] === 'full'
|
||||
? 'Full - ' . $seats['used'] . ' of ' . $seats['total'] . ' seats'
|
||||
: $seats['free'] . ' of ' . $seats['total'] . ' seats free');
|
||||
return selectOptionData(['icon' => 'fas ' . softwareTypeStyle($row['software_type'])['icon'], 'description' => implode(' · ', array_filter([$row['software_license_type'], $seat_text, $row['software_version']]))]);
|
||||
case 'credential':
|
||||
return selectOptionData(['icon' => 'fas fa-key', 'description' => $row['credential_description'] ?: $row['credential_uri']]);
|
||||
case 'vendor':
|
||||
return selectOptionData(['icon' => 'fas fa-building', 'description' => $row['vendor_description']]);
|
||||
}
|
||||
|
||||
318
agent/modals/software/software.php
Normal file
318
agent/modals/software/software.php
Normal file
@@ -0,0 +1,318 @@
|
||||
<?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' ?>">· <?= 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">
|
||||
<a class="ajax-modal" href="#" data-modal-url="modals/vendor/vendor.php?id=<?= $vendor_id ?>"><?= $vendor_name ?></a>
|
||||
<?php if ($vendor_phone) { ?><span class="text-body-secondary">·</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';
|
||||
@@ -1,297 +1,47 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* New license - for the client it was opened from, or pick the client here.
|
||||
* Opened from an asset or a contact (?asset_id= / ?contact_id=), that device
|
||||
* or user starts with a seat on it; ?template_id= starts it from a template.
|
||||
*/
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
|
||||
$client_id = intval($_GET['client_id'] ?? 0);
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$license_types_array = array (
|
||||
'Device',
|
||||
'User',
|
||||
'Site',
|
||||
'Concurrent',
|
||||
'Trial',
|
||||
'Perpetual',
|
||||
'Usage-based'
|
||||
);
|
||||
$client_id = intval($_GET['client_id'] ?? 0);
|
||||
if ($client_id) {
|
||||
enforceClientAccess();
|
||||
}
|
||||
|
||||
$software_form_mode = 'add';
|
||||
$software_id = 0;
|
||||
$software_name = $software_version = $software_type = $software_description = $software_license_type = '';
|
||||
$software_seats = $software_key = $software_purchase_reference = $software_purchase = $software_expire = $software_notes = '';
|
||||
$software_vendor_id = 0;
|
||||
$software_favorite = 0;
|
||||
|
||||
// Opened from a device or a user - they get the first seat
|
||||
$software_asset_ids = $client_id ? softwareClientIds('asset', [$_GET['asset_id'] ?? 0], $client_id) : [];
|
||||
$software_contact_ids = $client_id ? softwareClientIds('contact', [$_GET['contact_id'] ?? 0], $client_id) : [];
|
||||
$for_name = $software_asset_ids ? getFieldById('assets', $software_asset_ids[0], 'asset_name') : ($software_contact_ids ? getFieldById('contacts', $software_contact_ids[0], 'contact_name') : '');
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fa fa-fw fa-cube me-2"></i>New License</h5>
|
||||
<h5 class="modal-title"><i class="fa fa-fw fa-cube me-2"></i>New License<?php if ($for_name) { ?> <small class="opacity-75 ms-1">for <?= escapeHtml($for_name) ?></small><?php } ?></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'] ?>">
|
||||
<?php if ($client_id) { ?>
|
||||
<input type="hidden" name="client_id" value="<?= $client_id ?>">
|
||||
<?php } ?>
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<ul class="nav nav-pills nav-justified mb-3">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="pill" href="#pills-details">Details</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" href="#pills-licensing">Licensing</a>
|
||||
</li>
|
||||
<?php if ($client_id) { // Dont show these when in global mode ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" href="#pills-device-licenses">Devices</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" href="#pills-user-licenses">Users</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" href="#pills-notes">Notes</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="tab-content">
|
||||
|
||||
<div class="tab-pane fade show active" id="pills-details">
|
||||
|
||||
<?php if ($client_id) { ?>
|
||||
<input type="hidden" name="client_id" value="<?= $client_id ?>">
|
||||
<?php } else { ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Client <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
|
||||
<select class="form-select select2" name="client_id" required>
|
||||
<option value="">- Select Client -</option>
|
||||
<?php
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT client_id, client_name, client_abbreviation FROM clients WHERE client_archived_at IS NULL " . clientScopeSql('clients.client_id') . " ORDER BY client_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$client_id_select = intval($row['client_id']);
|
||||
$client_name = escapeHtml($row['client_name']); ?>
|
||||
<option <?php if ($client_id == $client_id_select) { echo "selected"; } ?> value="<?= $client_id_select ?>"<?= selectOptionClient($row['client_id'], $row['client_name'], $row['client_abbreviation'] ?? '') ?>><?= $client_name ?></option>
|
||||
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Type <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-tag"></i></span>
|
||||
<select class="form-select select2" name="type" required>
|
||||
<option value="">- Select Type -</option>
|
||||
<<?php
|
||||
$sql_software_types_select = mysqli_query($mysqli, "
|
||||
SELECT category_name, category_color, category_icon FROM categories
|
||||
WHERE category_type = 'software_type'
|
||||
AND category_archived_at IS NULL
|
||||
ORDER BY category_order ASC, category_name ASC
|
||||
");
|
||||
while ($row = mysqli_fetch_assoc($sql_software_types_select)) {
|
||||
$software_type_select = escapeHtml($row['category_name']);
|
||||
?>
|
||||
<option<?= selectOptionColor($row['category_color'] ?? '', $row['category_icon'] ?? '') ?>><?= $software_type_select ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Software Name <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-cube"></i></span>
|
||||
<input type="text" class="form-control" name="name" placeholder="Software name" maxlength="200" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Version</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-cube"></i></span>
|
||||
<input type="text" class="form-control" name="version" placeholder="Software version" maxlength="200">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($client_id) { ?>
|
||||
<div class="mb-3">
|
||||
<label>Vendor</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-building"></i></span>
|
||||
<select class="form-select select2" name="vendor">
|
||||
<option value="">- Select Vendor -</option>
|
||||
<?php
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT vendor_name, vendor_id FROM vendors WHERE vendor_archived_at IS NULL AND vendor_client_id = $client_id ORDER BY vendor_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$vendor_id = intval($row['vendor_id']);
|
||||
$vendor_name = escapeHtml($row['vendor_name']);
|
||||
?>
|
||||
<option value="<?= $vendor_id ?>"><?= $vendor_name ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Description</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-align-left"></i></span>
|
||||
<input type="text" class="form-control" name="description" placeholder="Short description">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="pills-licensing">
|
||||
|
||||
<div class="mb-3">
|
||||
<label>License Type</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-cube"></i></span>
|
||||
<select class="form-select select2" name="license_type">
|
||||
<option value="">- Select a License Type -</option>
|
||||
<?php foreach ($license_types_array as $license_type) { ?>
|
||||
<option><?= $license_type ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Seats</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-chair"></i></span>
|
||||
<input type="text" inputmode="numeric" pattern="[0-9]*" class="form-control" name="seats" placeholder="Number of seats">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>License Key</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
|
||||
<input type="text" class="form-control" name="key" placeholder="License key" maxlength="200">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Purchase Reference</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
|
||||
<input type="text" class="form-control" name="purchase_reference" placeholder="eg. Invoice, PO Number" maxlength="200">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Purchase Date</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-calendar-check"></i></span>
|
||||
<input type="date" class="form-control" name="purchase" max="2999-12-31">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Expire</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-calendar-times"></i></span>
|
||||
<input type="date" class="form-control" name="expire" max="2999-12-31">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php if ($client_id) { // Dont show these when in global mode ?>
|
||||
|
||||
<div class="tab-pane fade" id="pills-device-licenses">
|
||||
|
||||
<ul class="list-group">
|
||||
|
||||
<li class="list-group-item bg-dark">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" onclick="this.closest('.tab-pane').querySelectorAll('.asset-checkbox').forEach(checkbox => checkbox.checked = this.checked);">
|
||||
<label class="form-check-label ms-3"><strong>Licensed Devices</strong></label>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
|
||||
<?php
|
||||
$sql_assets_select = mysqli_query($mysqli, "SELECT asset_archived_at, asset_id, asset_name, asset_type, contact_name FROM assets LEFT JOIN contacts ON asset_contact_id = contact_id WHERE asset_archived_at IS NULL AND asset_client_id = $client_id ORDER BY asset_archived_at ASC, asset_name ASC");
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql_assets_select)) {
|
||||
$asset_id_select = intval($row['asset_id']);
|
||||
$asset_name_select = escapeHtml($row['asset_name']);
|
||||
$asset_type_select = escapeHtml($row['asset_type']);
|
||||
$asset_archived_at = escapeHtml($row['asset_archived_at']);
|
||||
if (empty($asset_archived_at)) {
|
||||
$asset_archived_display = "";
|
||||
} else {
|
||||
$asset_archived_display = "Archived - ";
|
||||
}
|
||||
$contact_name_select = escapeHtml($row['contact_name']);
|
||||
|
||||
?>
|
||||
<li class="list-group-item">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input asset-checkbox" name="assets[]" value="<?= $asset_id_select ?>">
|
||||
<label class="form-check-label ms-2"><?= "$asset_archived_display$asset_name_select - $contact_name_select" ?></label>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="pills-user-licenses">
|
||||
|
||||
<ul class="list-group">
|
||||
|
||||
<li class="list-group-item bg-dark">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" onclick="this.closest('.tab-pane').querySelectorAll('.user-checkbox').forEach(checkbox => checkbox.checked = this.checked);">
|
||||
<label class="form-check-label ms-3"><strong>Licensed Users</strong></label>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<?php
|
||||
$sql_contacts_select = mysqli_query($mysqli, "SELECT contact_email, contact_id, contact_name, contact_photo FROM contacts WHERE contact_archived_at IS NULL AND contact_client_id = $client_id ORDER BY contact_name ASC");
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql_contacts_select)) {
|
||||
$contact_id_select = intval($row['contact_id']);
|
||||
$contact_name_select = escapeHtml($row['contact_name']);
|
||||
$contact_email_select = escapeHtml($row['contact_email']);
|
||||
|
||||
?>
|
||||
<li class="list-group-item">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input user-checkbox" name="contacts[]" id="softwareAddContact<?= $contact_id_select ?>" value="<?= $contact_id_select ?>">
|
||||
<label class="form-check-label ms-2 d-inline-flex align-items-center gap-2" for="softwareAddContact<?= $contact_id_select ?>">
|
||||
<?= contactAvatar($client_id, $contact_id_select, $row['contact_name'], $row['contact_photo'], 'xs') ?>
|
||||
<span><?= $contact_name_select ?><?php if ($contact_email_select) { ?> <span class="small text-body-secondary"><?= $contact_email_select ?></span><?php } ?></span>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
<div class="tab-pane fade" id="pills-notes">
|
||||
|
||||
<textarea class="form-control" rows="12" placeholder="Enter some notes" name="notes"></textarea>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php require 'software_form.php'; ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="add_software" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Create</button>
|
||||
@@ -300,5 +50,4 @@ ob_start();
|
||||
</form>
|
||||
|
||||
<?php
|
||||
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
|
||||
@@ -1,74 +1,9 @@
|
||||
<?php
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
/*
|
||||
* Kept so existing links keep working. Templates are picked at the top of the
|
||||
* New License form now, which fills in the details and still lets you set the
|
||||
* seats, key and renewal date before it is saved.
|
||||
*/
|
||||
|
||||
$client_id = intval($_GET['client_id'] ?? 0);
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fa fa-fw fa-cube me-2"></i>New License from Template</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'] ?>">
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<?php if ($client_id) { ?>
|
||||
<input type="hidden" name="client_id" value="<?= $client_id ?>">
|
||||
<?php } else { ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Client <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
|
||||
<select class="form-select select2" name="client_id" required>
|
||||
<option value="">- Select Client -</option>
|
||||
<?php
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT client_id, client_name, client_abbreviation FROM clients WHERE client_archived_at IS NULL " . clientScopeSql('clients.client_id') . " ORDER BY client_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$client_id_select = intval($row['client_id']);
|
||||
$client_name_select = escapeHtml($row['client_name']); ?>
|
||||
<option value="<?= $client_id_select ?>"<?= selectOptionClient($row['client_id'], $row['client_name'], $row['client_abbreviation'] ?? '') ?>><?= $client_name_select ?></option>
|
||||
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
<label>Template</label>
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-puzzle-piece"></i></span>
|
||||
<select class="form-select" name="software_template_id" required>
|
||||
<option value="">- Select Template -</option>
|
||||
<?php
|
||||
$sql_software_templates = mysqli_query($mysqli, "SELECT software_template_id, software_template_name FROM software_templates WHERE software_template_archived_at IS NULL ORDER BY software_template_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql_software_templates)) {
|
||||
$software_template_id = intval($row['software_template_id']);
|
||||
$software_template_name = escapeHtml($row['software_template_name']);
|
||||
|
||||
?>
|
||||
<option value="<?= $software_template_id ?>"><?= $software_template_name ?></option>
|
||||
<?php } ?>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="add_software_from_template" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Create</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
require __DIR__ . '/software_add.php';
|
||||
|
||||
@@ -1,320 +1,56 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Edit a license - the same form as New License (software_form.php), with
|
||||
* its devices and users already picked.
|
||||
*/
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$software_id = intval($_GET['id']);
|
||||
$software_id = intval($_GET['id'] ?? 0);
|
||||
$row = softwareRow($software_id);
|
||||
if (!$row) {
|
||||
echo json_encode(['error' => 'That license no longer exists.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT software_client_id, software_created_at, software_description, software_expire,
|
||||
software_key, software_license_type, software_name, software_notes, software_purchase,
|
||||
software_purchase_reference, software_seats, software_type, software_vendor_id,
|
||||
software_version FROM software WHERE software_id = $software_id LIMIT 1");
|
||||
$client_id = intval($row['software_client_id']);
|
||||
enforceClientAccess();
|
||||
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$software_form_mode = 'edit';
|
||||
$software_name = escapeHtml($row['software_name']);
|
||||
$software_description = escapeHtml($row['software_description']);
|
||||
$software_version = escapeHtml($row['software_version']);
|
||||
$software_type = escapeHtml($row['software_type']);
|
||||
$software_description = escapeHtml($row['software_description']);
|
||||
$software_license_type = escapeHtml($row['software_license_type']);
|
||||
$software_seats = $row['software_seats'] !== null && intval($row['software_seats']) > 0 ? intval($row['software_seats']) : '';
|
||||
$software_key = escapeHtml($row['software_key']);
|
||||
$software_seats = escapeHtml($row['software_seats']);
|
||||
$software_purchase_reference = escapeHtml($row['software_purchase_reference']);
|
||||
$software_purchase = escapeHtml($row['software_purchase']);
|
||||
$software_expire = escapeHtml($row['software_expire']);
|
||||
$software_notes = escapeHtml($row['software_notes']);
|
||||
$software_created_at = escapeHtml($row['software_created_at']);
|
||||
$software_vendor_id = intval($row['software_vendor_id']);
|
||||
$client_id = intval($row['software_client_id']);
|
||||
$seat_count = 0;
|
||||
$software_favorite = intval($row['software_favorite']);
|
||||
|
||||
// Device Licenses
|
||||
$asset_licenses_sql = mysqli_query($mysqli, "SELECT asset_id FROM software_assets WHERE software_id = $software_id");
|
||||
$asset_licenses_array = array();
|
||||
while ($row = mysqli_fetch_assoc($asset_licenses_sql)) {
|
||||
$asset_licenses_array[] = intval($row['asset_id']);
|
||||
$seat_count = $seat_count + 1;
|
||||
}
|
||||
$asset_licenses = implode(',', $asset_licenses_array);
|
||||
|
||||
// User Licenses
|
||||
$contact_licenses_sql = mysqli_query($mysqli, "SELECT contact_id FROM software_contacts WHERE software_id = $software_id");
|
||||
$contact_licenses_array = array();
|
||||
while ($row = mysqli_fetch_assoc($contact_licenses_sql)) {
|
||||
$contact_licenses_array[] = intval($row['contact_id']);
|
||||
$seat_count = $seat_count + 1;
|
||||
}
|
||||
$contact_licenses = implode(',', $contact_licenses_array);
|
||||
|
||||
$license_types_array = array (
|
||||
'Device',
|
||||
'User',
|
||||
'Site',
|
||||
'Concurrent',
|
||||
'Trial',
|
||||
'Perpetual',
|
||||
'Usage-based'
|
||||
);
|
||||
|
||||
enforceClientAccess();
|
||||
$holders = softwareSeatHolders($software_id);
|
||||
$software_asset_ids = array_map('intval', array_column($holders['assets'], 'asset_id'));
|
||||
$software_contact_ids = array_map('intval', array_column($holders['contacts'], 'contact_id'));
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fa fa-fw fa-cube me-2"></i>Editing license: <strong><?= $software_name ?></strong></h5>
|
||||
<h5 class="modal-title text-truncate"><i class="fa fa-fw fa-cube me-2"></i>Editing license: <strong><?= $software_name ?></strong></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="software_id" value="<?= $software_id ?>">
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<ul class="nav nav-pills nav-justified mb-3">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="pill" href="#pills-details<?= $software_id ?>">Details</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" href="#pills-license<?= $software_id ?>">License</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" href="#pills-device-licenses<?= $software_id ?>">Devices</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" href="#pills-user-licenses<?= $software_id ?>">Users</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" href="#pills-notes<?= $software_id ?>">Notes</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="tab-content" <?php if (lookupUserPermission('module_support') <= 1) { echo 'inert'; } ?>>
|
||||
|
||||
<div class="tab-pane fade show active" id="pills-details<?= $software_id ?>">
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Type <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-tag"></i></span>
|
||||
<select class="form-select select2" name="type" required>
|
||||
<option value="">- Select Type -</option>
|
||||
<<?php
|
||||
$sql_software_types_select = mysqli_query($mysqli, "
|
||||
SELECT category_name, category_color, category_icon FROM categories
|
||||
WHERE category_type = 'software_type'
|
||||
AND category_archived_at IS NULL
|
||||
ORDER BY category_order ASC, category_name ASC
|
||||
");
|
||||
while ($row = mysqli_fetch_assoc($sql_software_types_select)) {
|
||||
$software_type_select = escapeHtml($row['category_name']);
|
||||
?>
|
||||
<option <?php if ($software_type == $software_type_select) { echo "selected"; } ?><?= selectOptionColor($row['category_color'] ?? '', $row['category_icon'] ?? '') ?>>
|
||||
<?= $software_type_select ?>
|
||||
</option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Software Name <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-cube"></i></span>
|
||||
<input type="text" class="form-control" name="name" placeholder="Software name" maxlength="200" value="<?= $software_name ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Version</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-cube"></i></span>
|
||||
<input type="text" class="form-control" name="version" placeholder="Software version" maxlength="200" value="<?= $software_version ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Vendor</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-building"></i></span>
|
||||
<select class="form-select select2" name="vendor">
|
||||
<option value="">- Select Vendor -</option>
|
||||
<?php
|
||||
$vendor_sql = mysqli_query($mysqli, "SELECT vendor_id, vendor_name FROM vendors WHERE vendor_client_id = $client_id AND vendor_archived_at IS NULL ORDER BY vendor_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($vendor_sql)) {
|
||||
$vendor_id = $row['vendor_id'];
|
||||
$vendor_name = $row['vendor_name'];
|
||||
?>
|
||||
<option <?php if ($software_vendor_id == $vendor_id) { echo "selected"; } ?> value="<?= $vendor_id ?>"><?= $vendor_name ?></option>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Description</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-align-left"></i></span>
|
||||
<input type="text" class="form-control" name="description" placeholder="Short description" value="<?= $software_description ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="pills-license<?= $software_id ?>">
|
||||
|
||||
<div class="mb-3">
|
||||
<label>License Type</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-cube"></i></span>
|
||||
<select class="form-select select2" name="license_type">
|
||||
<option value="">- Select a License Type -</option>
|
||||
<?php foreach($license_types_array as $license_type_select) { ?>
|
||||
<option <?php if ($license_type_select == $software_license_type) { echo "selected"; } ?>><?= $license_type_select ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Seats</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-cube"></i></span>
|
||||
<input type="text" class="form-control" inputmode="numeric" pattern="[0-9]*" name="seats" placeholder="Number of seats" value="<?= $software_seats ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>License Key</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
|
||||
<input type="text" class="form-control" name="key" placeholder="License key" maxlength="200" value="<?= $software_key ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Purchase Reference</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-shopping-cart"></i></span>
|
||||
<input type="text" class="form-control" name="purchase_reference" placeholder="eg. Invoice, PO Number" maxlength="200" value="<?= $software_purchase_reference ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Purchase Date</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-calendar-check"></i></span>
|
||||
<input type="date" class="form-control" name="purchase" max="2999-12-31" value="<?= $software_purchase ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Expire</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-calendar-times"></i></span>
|
||||
<input type="date" class="form-control" name="expire" max="2999-12-31" value="<?= $software_expire ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="pills-device-licenses<?= $software_id ?>">
|
||||
|
||||
<ul class="list-group">
|
||||
|
||||
<li class="list-group-item bg-dark">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input"
|
||||
onclick="this.closest('.tab-pane').querySelectorAll('.asset-checkbox').forEach(checkbox => checkbox.checked = this.checked);"
|
||||
>
|
||||
<label class="form-check-label ms-3"><strong>Licensed Devices</strong></label>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
|
||||
<?php
|
||||
$sql_assets_select = mysqli_query($mysqli, "SELECT asset_archived_at, asset_id, asset_name, asset_type, contact_name FROM assets LEFT JOIN contacts ON asset_contact_id = contact_id WHERE (asset_archived_at > '$software_created_at' OR asset_archived_at IS NULL) AND asset_client_id = $client_id ORDER BY asset_archived_at ASC, asset_name ASC");
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql_assets_select)) {
|
||||
$asset_id_select = intval($row['asset_id']);
|
||||
$asset_name_select = escapeHtml($row['asset_name']);
|
||||
$asset_type_select = escapeHtml($row['asset_type']);
|
||||
$asset_archived_at = escapeHtml($row['asset_archived_at']);
|
||||
if (empty($asset_archived_at)) {
|
||||
$asset_archived_display = "";
|
||||
} else {
|
||||
$asset_archived_display = "Archived - ";
|
||||
}
|
||||
$contact_name_select = escapeHtml($row['contact_name']);
|
||||
|
||||
?>
|
||||
<li class="list-group-item">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input asset-checkbox" name="assets[]" value="<?= $asset_id_select ?>" <?php if (in_array($asset_id_select, $asset_licenses_array)) { echo "checked"; } ?>>
|
||||
<label class="form-check-label ms-2"><?= "$asset_archived_display$asset_name_select - $contact_name_select" ?></label>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="pills-user-licenses<?= $software_id ?>">
|
||||
|
||||
<ul class="list-group">
|
||||
|
||||
<li class="list-group-item bg-dark">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" onclick="this.closest('.tab-pane').querySelectorAll('.user-checkbox').forEach(checkbox => checkbox.checked = this.checked);">
|
||||
<label class="form-check-label ms-3"><strong>Licensed Users</strong></label>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<?php
|
||||
$sql_contacts_select = mysqli_query($mysqli, "SELECT contact_archived_at, contact_email, contact_id, contact_name, contact_photo FROM contacts WHERE (contact_archived_at > '$software_created_at' OR contact_archived_at IS NULL) AND contact_client_id = $client_id ORDER BY contact_archived_at ASC, contact_name ASC");
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql_contacts_select)) {
|
||||
$contact_id_select = intval($row['contact_id']);
|
||||
$contact_name_select = escapeHtml($row['contact_name']);
|
||||
$contact_email_select = escapeHtml($row['contact_email']);
|
||||
$contact_archived_at = escapeHtml($row['contact_archived_at']);
|
||||
if (empty($contact_archived_at)) {
|
||||
$contact_archived_display = "";
|
||||
} else {
|
||||
$contact_archived_display = "Archived - ";
|
||||
}
|
||||
|
||||
?>
|
||||
<li class="list-group-item">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input user-checkbox" name="contacts[]" id="softwareEditContact<?= $contact_id_select ?>" value="<?= $contact_id_select ?>" <?php if (in_array("$contact_id_select", $contact_licenses_array)) { echo "checked"; } ?>>
|
||||
<label class="form-check-label ms-2 d-inline-flex align-items-center gap-2" for="softwareEditContact<?= $contact_id_select ?>">
|
||||
<?= contactAvatar($client_id, $contact_id_select, $row['contact_name'], $row['contact_photo'], 'xs') ?>
|
||||
<span><?= "$contact_archived_display$contact_name_select" ?><?php if ($contact_email_select) { ?> <span class="small text-body-secondary"><?= $contact_email_select ?></span><?php } ?></span>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="pills-notes<?= $software_id ?>">
|
||||
|
||||
<textarea class="form-control" rows="12" placeholder="Enter some notes" name="notes"><?= $software_notes ?></textarea>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php require 'software_form.php'; ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="edit_software" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Save</button>
|
||||
|
||||
@@ -23,12 +23,17 @@ $expire_filter = $_GET['expire_days'] ?? '';
|
||||
// Archived Filter
|
||||
$archived_filter = (isset($_GET['archived']) && $_GET['archived'] == 1) ? 1 : 0;
|
||||
|
||||
// Attention tab and license type
|
||||
$status_filters = softwareStatusFilters();
|
||||
$status_filter = isset($status_filters[$_GET['status'] ?? '']) ? $_GET['status'] : '';
|
||||
$license_type_filter = (string) ($_GET['license_type'] ?? '');
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fas fa-fw fa-download me-2"></i>Export Software</h5>
|
||||
<h5 class="modal-title"><i class="fas fa-fw fa-download me-2"></i>Export Licenses</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
@@ -46,7 +51,7 @@ ob_start();
|
||||
<label>Search</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-search"></i></span>
|
||||
<input type="text" class="form-control" name="q" value="<?= stripslashes(escapeHtml($q_filter)) ?>" placeholder="Name, type, key">
|
||||
<input type="text" class="form-control" name="q" value="<?= stripslashes(escapeHtml($q_filter)) ?>" placeholder="Name, type, key, vendor">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -86,6 +91,32 @@ ob_start();
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Showing</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-filter"></i></span>
|
||||
<select class="form-select select2" name="status">
|
||||
<option value="">- All -</option>
|
||||
<?php foreach ($status_filters as $status_key => $status_option) { ?>
|
||||
<option <?php if ($status_filter === $status_key) { echo "selected"; } ?> value="<?= $status_key ?>"<?= selectOptionData(['icon' => 'fas ' . $status_option['icon']]) ?>><?= $status_option['label'] ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>License Type</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-certificate"></i></span>
|
||||
<select class="form-select select2" name="license_type">
|
||||
<option value="">- Any -</option>
|
||||
<?php foreach (array_unique(array_merge(array_keys(SOFTWARE_LICENSE_TYPES), $license_type_filter !== '' ? [$license_type_filter] : [])) as $license_type_option) { ?>
|
||||
<option <?php if ($license_type_filter === $license_type_option) { echo "selected"; } ?><?= selectOptionData(['icon' => 'fas ' . softwareLicenseTypeIcon($license_type_option)]) ?>><?= escapeHtml($license_type_option) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Archived</label>
|
||||
<div class="input-group">
|
||||
|
||||
417
agent/modals/software/software_form.php
Normal file
417
agent/modals/software/software_form.php
Normal file
@@ -0,0 +1,417 @@
|
||||
<?php
|
||||
/*
|
||||
* License form fields - shared by software_add.php and software_edit.php,
|
||||
* which set $software_form_mode ('add' | 'edit'), $client_id (0 = pick the
|
||||
* client here), $software_id and the $software_* values (escaped, empty for a
|
||||
* new license), plus $software_asset_ids / $software_contact_ids (ints) for
|
||||
* the seats, before including this. Posts what softwareFormSet()
|
||||
* (functions/software.php) reads.
|
||||
*
|
||||
* Tabs: Details, License (type, seats, key, dates), Seats (the client's
|
||||
* devices and users, with a running count against the seats bought) and
|
||||
* Notes. A new license can start from a template, which fills in the details,
|
||||
* and take a batch of individual keys, one per line.
|
||||
*
|
||||
* Should not be accessed directly.
|
||||
*/
|
||||
|
||||
isset($mysqli) || die("Direct file access is not allowed");
|
||||
|
||||
// Software types - from Admin > Categories, keeping a retired value that is still set
|
||||
$type_options = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT category_name FROM categories WHERE category_type = 'software_type' AND category_archived_at IS NULL ORDER BY category_order ASC, category_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$type_options[] = $row['category_name'];
|
||||
}
|
||||
$software_type_raw = html_entity_decode($software_type, ENT_QUOTES, 'UTF-8');
|
||||
if ($software_type_raw !== '' && !in_array($software_type_raw, $type_options, true)) {
|
||||
$type_options[] = $software_type_raw;
|
||||
}
|
||||
|
||||
// License types - the usual ones, keeping whatever this one already has
|
||||
$license_type_options = array_keys(SOFTWARE_LICENSE_TYPES);
|
||||
$software_license_type_raw = html_entity_decode($software_license_type, ENT_QUOTES, 'UTF-8');
|
||||
if ($software_license_type_raw !== '' && !in_array($software_license_type_raw, $license_type_options, true)) {
|
||||
$license_type_options[] = $software_license_type_raw;
|
||||
}
|
||||
|
||||
// The client's own lists - only once the client is known
|
||||
$vendor_options = $asset_options = $contact_options = [];
|
||||
if ($client_id) {
|
||||
$sql = mysqli_query($mysqli, "SELECT vendor_archived_at, vendor_id, vendor_name, vendor_description FROM vendors
|
||||
WHERE vendor_client_id = $client_id AND (vendor_archived_at IS NULL OR vendor_id = $software_vendor_id)
|
||||
ORDER BY vendor_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$vendor_options[] = $row;
|
||||
}
|
||||
$sql = mysqli_query($mysqli, "SELECT asset_id, asset_name, asset_type, asset_make, asset_model, contact_name FROM assets
|
||||
LEFT JOIN contacts ON contact_id = asset_contact_id
|
||||
WHERE asset_client_id = $client_id AND asset_archived_at IS NULL
|
||||
ORDER BY asset_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$asset_options[] = $row;
|
||||
}
|
||||
$sql = mysqli_query($mysqli, "SELECT contact_id, contact_name, contact_title, contact_photo, contact_client_id FROM contacts
|
||||
WHERE contact_client_id = $client_id AND contact_archived_at IS NULL
|
||||
ORDER BY contact_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$contact_options[] = $row;
|
||||
}
|
||||
} else {
|
||||
$client_options = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT client_id, client_name, client_abbreviation FROM clients WHERE client_archived_at IS NULL " . clientScopeSql('clients.client_id') . " ORDER BY client_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$client_options[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
// Templates (Admin > License Templates) to start a new license from
|
||||
$template_options = [];
|
||||
if ($software_form_mode === 'add') {
|
||||
$sql = mysqli_query($mysqli, "SELECT software_template_id, software_template_name, software_template_version, software_template_description,
|
||||
software_template_type, software_template_license_type, software_template_notes
|
||||
FROM software_templates WHERE software_template_archived_at IS NULL ORDER BY software_template_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$template_options[intval($row['software_template_id'])] = [
|
||||
'name' => $row['software_template_name'],
|
||||
'version' => (string) $row['software_template_version'],
|
||||
'description' => (string) $row['software_template_description'],
|
||||
'type' => $row['software_template_type'],
|
||||
'license_type' => (string) $row['software_template_license_type'],
|
||||
'notes' => (string) $row['software_template_notes'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$software_type_style = softwareTypeStyle($software_type);
|
||||
$form_id = 'softwareForm' . ($software_form_mode === 'edit' ? $software_id : 'New');
|
||||
|
||||
?>
|
||||
|
||||
<ul class="nav nav-pills nav-fill flex-nowrap overflow-auto mb-3 itflow-asset-form-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="pill" href="#<?= $form_id ?>Details"><i class="fas fa-fw fa-cube me-2 d-none d-lg-inline"></i>Details</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" href="#<?= $form_id ?>License"><i class="fas fa-fw fa-key me-2 d-none d-lg-inline"></i>License</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" href="#<?= $form_id ?>Seats"><i class="fas fa-fw fa-users me-2 d-none d-lg-inline"></i>Seats <span class="badge rounded-pill text-bg-secondary ms-1" data-seat-badge></span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" href="#<?= $form_id ?>Notes"><i class="fas fa-fw fa-sticky-note me-2 d-none d-lg-inline"></i>Notes</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="<?= $form_id ?>">
|
||||
|
||||
<!-- What it is -->
|
||||
<div class="tab-pane fade show active" id="<?= $form_id ?>Details">
|
||||
<div class="row g-3">
|
||||
<?php if (!$client_id) { ?>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Client <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
|
||||
<select class="form-select select2" name="client_id" required>
|
||||
<option value="">- Select Client -</option>
|
||||
<?php foreach ($client_options as $client_option) { ?>
|
||||
<option value="<?= intval($client_option['client_id']) ?>"<?= selectOptionClient($client_option['client_id'], $client_option['client_name'], $client_option['client_abbreviation'] ?? '') ?>><?= escapeHtml($client_option['client_name']) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-text">Pick the vendor and who is on it once it's saved.</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($template_options) { ?>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Start from a template</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-fw fa-puzzle-piece"></i></span>
|
||||
<select class="form-select select2" data-software-template data-placeholder="- None - fill it in yourself -">
|
||||
<option value="">- None -</option>
|
||||
<?php foreach ($template_options as $template_id => $template) { ?>
|
||||
<option value="<?= $template_id ?>" <?= intval($_GET['template_id'] ?? 0) === $template_id ? 'selected' : '' ?><?= selectOptionData(['icon' => 'fas ' . softwareTypeStyle($template['type'])['icon'], 'description' => implode(' · ', array_filter([$template['type'], $template['license_type']]))]) ?>><?= escapeHtml($template['name']) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-text">Fills in the name, type, license type, description and notes - change anything after.</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Name <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-fw <?= $software_type_style['icon'] ?>" data-software-type-icon></i></span>
|
||||
<input type="text" class="form-control" name="name" placeholder="Microsoft 365 Business Premium" maxlength="200" value="<?= $software_name ?>" required <?= $software_form_mode === 'edit' ? '' : 'autofocus' ?>>
|
||||
<div class="input-group-text">
|
||||
<label class="star-toggle mb-0" title="Favorite - pinned to the client overview">
|
||||
<input class="form-check-input" type="checkbox" name="favorite" value="1" <?= $software_favorite ? 'checked' : '' ?>><i class="far fa-star"></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Version</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-fw fa-code-branch"></i></span>
|
||||
<input type="text" class="form-control" name="version" placeholder="2024, 16.0, LTSC" maxlength="200" value="<?= $software_version ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Type <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-fw fa-layer-group"></i></span>
|
||||
<select class="form-select select2" name="type" required>
|
||||
<option value="">- Select Type -</option>
|
||||
<?php foreach ($type_options as $type_option) { ?>
|
||||
<option <?= $type_option === $software_type_raw ? 'selected' : '' ?><?= selectOptionData(['icon' => 'fas ' . softwareTypeStyle($type_option)['icon']]) ?>><?= escapeHtml($type_option) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($client_id) { ?>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Vendor</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-building"></i></span>
|
||||
<select class="form-select select2" name="vendor">
|
||||
<option value="">- Select Vendor -</option>
|
||||
<?php foreach ($vendor_options as $vendor_option) {
|
||||
$vendor_option_id = intval($vendor_option['vendor_id']); ?>
|
||||
<option value="<?= $vendor_option_id ?>" <?= $software_vendor_id === $vendor_option_id ? 'selected' : '' ?><?= selectOptionData(['icon' => 'fas fa-building', 'description' => $vendor_option['vendor_description']]) ?>><?= escapeHtml($vendor_option['vendor_name']) ?><?= $vendor_option['vendor_archived_at'] ? ' (archived)' : '' ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Description</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-align-left"></i></span>
|
||||
<input type="text" class="form-control" name="description" placeholder="Email, Office apps and Intune for the whole office" value="<?= $software_description ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($software_form_mode === 'edit') { ?>
|
||||
<p class="text-body-secondary small text-end mt-3 mb-0">License ID <span class="font-monospace"><?= $software_id ?></span></p>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<!-- The license: what kind, how many, the key, when it renews -->
|
||||
<div class="tab-pane fade" id="<?= $form_id ?>License">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">License type</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-fw fa-certificate"></i></span>
|
||||
<select class="form-select select2" name="license_type">
|
||||
<option value="">- Select License Type -</option>
|
||||
<?php foreach ($license_type_options as $license_type_option) { ?>
|
||||
<option <?= $license_type_option === $software_license_type_raw ? 'selected' : '' ?><?= selectOptionData(['icon' => 'fas ' . softwareLicenseTypeIcon($license_type_option)]) ?>><?= escapeHtml($license_type_option) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Seats bought</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-fw fa-users"></i></span>
|
||||
<input type="number" class="form-control" name="seats" min="0" step="1" placeholder="No limit" value="<?= $software_seats ?>" data-seat-total>
|
||||
</div>
|
||||
<div class="form-text">Leave blank for a site or unlimited license.</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">License key</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-fw fa-key"></i></span>
|
||||
<input type="text" class="form-control font-monospace" name="key" placeholder="XXXXX-XXXXX-XXXXX-XXXXX-XXXXX" maxlength="200" value="<?= $software_key ?>">
|
||||
</div>
|
||||
<div class="form-text">The one key for the whole license, if it has one.<?= $software_form_mode === 'edit' ? ' A key per device or user goes on the license\'s Keys tab.' : '' ?></div>
|
||||
</div>
|
||||
<?php if ($software_form_mode === 'add') { ?>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Individual keys</label>
|
||||
<textarea class="form-control font-monospace" name="keys" rows="3" placeholder="One key per line - for a license with a key for each device or user"></textarea>
|
||||
<div class="form-text">Hand each one to a device or user from the license's Keys tab.</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Order / invoice reference</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-receipt"></i></span>
|
||||
<input type="text" class="form-control" name="purchase_reference" placeholder="INV-1045, PO-7782, contract number" maxlength="200" value="<?= $software_purchase_reference ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Purchased</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-fw fa-shopping-cart"></i></span>
|
||||
<input type="date" class="form-control" name="purchase" max="2999-12-31" value="<?= $software_purchase ?>" data-software-purchase>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Renews / expires</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-fw fa-calendar-alt"></i></span>
|
||||
<input type="date" class="form-control" name="expire" max="2999-12-31" value="<?= $software_expire ?>" data-software-expire>
|
||||
<button type="button" class="btn btn-outline-secondary" data-renew-term="1m" title="A month after it was bought (or today)">1 mo</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-renew-term="1y" title="A year after it was bought (or today)">1 yr</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-renew-term="3y" title="Three years after it was bought (or today)">3 yr</button>
|
||||
</div>
|
||||
<div class="form-text">Shows on the client overview's renewals and the dashboard as it gets close.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Who and what is on it -->
|
||||
<div class="tab-pane fade" id="<?= $form_id ?>Seats">
|
||||
<?php if ($client_id) { ?>
|
||||
<input type="hidden" name="seats_form" value="1">
|
||||
<div class="d-flex align-items-center justify-content-between gap-2 mb-3">
|
||||
<span class="text-body-secondary small">Each device and user here takes a seat.</span>
|
||||
<span class="badge rounded-pill fs-6 fw-normal" data-seat-count></span>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Devices</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-fw fa-desktop"></i></span>
|
||||
<select class="form-select select2" name="assets[]" multiple data-placeholder="<?= $asset_options ? 'Pick the devices it is installed on' : 'This client has no assets yet' ?>" data-seat-pick>
|
||||
<?php foreach ($asset_options as $asset_option) {
|
||||
$asset_option_id = intval($asset_option['asset_id']); ?>
|
||||
<option value="<?= $asset_option_id ?>" <?= in_array($asset_option_id, $software_asset_ids, true) ? 'selected' : '' ?><?= selectOptionAsset($asset_option['asset_type'], implode(' · ', array_filter([trim($asset_option['asset_make'] . ' ' . $asset_option['asset_model']), $asset_option['contact_name']]))) ?>><?= escapeHtml($asset_option['asset_name']) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Users</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-fw fa-user"></i></span>
|
||||
<select class="form-select select2" name="contacts[]" multiple data-placeholder="<?= $contact_options ? 'Pick the people licensed to use it' : 'This client has no contacts yet' ?>" data-seat-pick>
|
||||
<?php foreach ($contact_options as $contact_option) {
|
||||
$contact_option_id = intval($contact_option['contact_id']); ?>
|
||||
<option value="<?= $contact_option_id ?>" <?= in_array($contact_option_id, $software_contact_ids, true) ? 'selected' : '' ?><?= selectOptionContact($contact_option['contact_client_id'], $contact_option['contact_name'], $contact_option['contact_photo'], $contact_option['contact_title'], $contact_option_id) ?>><?= escapeHtml($contact_option['contact_name']) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php } else { ?>
|
||||
<p class="text-body-secondary text-center my-4">Pick the devices and users once it's saved - they come from the client.</p>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="<?= $form_id ?>Notes">
|
||||
<textarea class="form-control" rows="12" placeholder="Admin portal, how it is billed, who to call to renew..." name="notes"><?= $software_notes ?></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
itflowReady(function () {
|
||||
var box = document.getElementById(<?= json_encode($form_id) ?>);
|
||||
var form = box.closest('form');
|
||||
|
||||
// A required field on a tab that isn't showing: show that tab and point at it
|
||||
var reported = false;
|
||||
form.addEventListener('invalid', function (e) {
|
||||
var pane = e.target.closest('.tab-pane');
|
||||
var tab = pane && form.querySelector('[data-bs-toggle="pill"][href="#' + pane.id + '"]');
|
||||
if (!tab || pane.classList.contains('active') || reported) {
|
||||
return;
|
||||
}
|
||||
reported = true;
|
||||
tab.addEventListener('shown.bs.tab', function () {
|
||||
reported = false;
|
||||
e.target.reportValidity();
|
||||
}, { once: true });
|
||||
bootstrap.Tab.getOrCreateInstance(tab).show();
|
||||
}, true);
|
||||
|
||||
// Seats picked against seats bought, on the tab and above the pickers
|
||||
var total = form.querySelector('[data-seat-total]');
|
||||
var picks = form.querySelectorAll('[data-seat-pick]');
|
||||
var count = form.querySelector('[data-seat-count]');
|
||||
var badge = form.querySelector('[data-seat-badge]');
|
||||
function seats() {
|
||||
var used = 0;
|
||||
picks.forEach(function (select) {
|
||||
used += select.selectedOptions.length;
|
||||
});
|
||||
var bought = parseInt(total.value, 10) || 0;
|
||||
var over = bought && used > bought;
|
||||
var text = bought ? used + ' of ' + bought + ' seats' : used + ' in use';
|
||||
if (count) {
|
||||
count.textContent = text + (over ? ' - ' + (used - bought) + ' over' : '');
|
||||
count.className = 'badge rounded-pill fs-6 fw-normal ' + (over ? 'text-bg-danger' : (bought && used === bought ? 'text-bg-warning' : 'text-bg-secondary'));
|
||||
}
|
||||
badge.textContent = picks.length ? (bought ? used + '/' + bought : used) : '';
|
||||
badge.className = 'badge rounded-pill ms-1 ' + (over ? 'text-bg-danger' : 'text-bg-secondary');
|
||||
}
|
||||
picks.forEach(function (select) {
|
||||
select.addEventListener('change', seats);
|
||||
select.addEventListener('input', seats);
|
||||
});
|
||||
total.addEventListener('input', seats);
|
||||
seats();
|
||||
|
||||
// The name's icon follows the type
|
||||
var typeIcons = <?= json_encode(array_combine($type_options, array_map(function ($type) { return softwareTypeStyle($type)['icon']; }, $type_options)) ?: new stdClass(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
||||
var type = form.querySelector('[name="type"]');
|
||||
var typeIcon = form.querySelector('[data-software-type-icon]');
|
||||
type.addEventListener('change', function () {
|
||||
typeIcon.className = 'fas fa-fw ' + (typeIcons[type.value] || 'fa-cube');
|
||||
});
|
||||
|
||||
// Renewal a term after it was bought - or after today
|
||||
var purchase = form.querySelector('[data-software-purchase]');
|
||||
var expire = form.querySelector('[data-software-expire]');
|
||||
form.querySelectorAll('[data-renew-term]').forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
var from = purchase.value ? new Date(purchase.value + 'T00:00:00') : new Date();
|
||||
var term = button.dataset.renewTerm;
|
||||
if (term.slice(-1) === 'y') {
|
||||
from.setFullYear(from.getFullYear() + parseInt(term, 10));
|
||||
} else {
|
||||
from.setMonth(from.getMonth() + parseInt(term, 10));
|
||||
}
|
||||
var pad = function (n) { return (n < 10 ? '0' : '') + n; };
|
||||
expire.value = from.getFullYear() + '-' + pad(from.getMonth() + 1) + '-' + pad(from.getDate());
|
||||
expire.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
});
|
||||
});
|
||||
|
||||
// Starting from a template fills in what it knows
|
||||
var templates = <?= json_encode($template_options ?: new stdClass(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
||||
var picker = form.querySelector('[data-software-template]');
|
||||
function applyTemplate() {
|
||||
var t = templates[picker.value];
|
||||
if (!t) {
|
||||
return;
|
||||
}
|
||||
form.querySelector('[name="name"]').value = t.name;
|
||||
form.querySelector('[name="version"]').value = t.version;
|
||||
form.querySelector('[name="description"]').value = t.description;
|
||||
form.querySelector('[name="notes"]').value = t.notes;
|
||||
[['type', t.type], ['license_type', t.license_type]].forEach(function (pair) {
|
||||
var select = form.querySelector('[name="' + pair[0] + '"]');
|
||||
if (!pair[1]) {
|
||||
return;
|
||||
}
|
||||
if (!Array.prototype.some.call(select.options, function (o) { return o.value === pair[1]; })) {
|
||||
select.add(new Option(pair[1], pair[1]));
|
||||
}
|
||||
select.value = pair[1];
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
});
|
||||
}
|
||||
if (picker) {
|
||||
picker.addEventListener('change', applyTemplate);
|
||||
applyTemplate();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
44
agent/modals/software/software_key_add.php
Normal file
44
agent/modals/software/software_key_add.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Add individual keys to a license - paste them one per line. Blank lines and
|
||||
* keys the license already has are skipped. Each is then handed to a device
|
||||
* or user from the Keys tab.
|
||||
*/
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$software_id = intval($_GET['software_id'] ?? 0);
|
||||
$row = softwareRow($software_id);
|
||||
if (!$row) {
|
||||
echo json_encode(['error' => 'That license no longer exists.']);
|
||||
exit;
|
||||
}
|
||||
$client_id = intval($row['software_client_id']);
|
||||
enforceClientAccess();
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title text-truncate"><i class="fas fa-fw fa-key me-2"></i>Add keys to <strong><?= escapeHtml($row['software_name']) ?></strong></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="software_id" value="<?= $software_id ?>">
|
||||
<div class="modal-body">
|
||||
<label class="form-label">Keys</label>
|
||||
<textarea class="form-control font-monospace" name="keys" rows="8" placeholder="One key per line" required autofocus></textarea>
|
||||
<div class="form-text">Paste straight from the vendor's portal or the order email - one per line. Keys it already has are skipped.</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="add_software_keys" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Add</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
110
agent/modals/software/software_key_edit.php
Normal file
110
agent/modals/software/software_key_edit.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* One key on a license: change it, and hand it to the devices and users using
|
||||
* it. Handing it to someone not yet on the license gives them a seat.
|
||||
*/
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$software_key_id = intval($_GET['id'] ?? 0);
|
||||
$key_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT software_key, software_key_software_id FROM software_keys WHERE software_key_id = $software_key_id"));
|
||||
$row = $key_row ? softwareRow($key_row['software_key_software_id']) : null;
|
||||
if (!$row) {
|
||||
echo json_encode(['error' => 'That key no longer exists.']);
|
||||
exit;
|
||||
}
|
||||
$client_id = intval($row['software_client_id']);
|
||||
enforceClientAccess();
|
||||
|
||||
$software_id = intval($row['software_id']);
|
||||
$assigned_assets = $assigned_contacts = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT asset_id FROM software_key_asset_assignments WHERE software_key_id = $software_key_id");
|
||||
while ($assigned = mysqli_fetch_row($sql)) {
|
||||
$assigned_assets[] = intval($assigned[0]);
|
||||
}
|
||||
$sql = mysqli_query($mysqli, "SELECT contact_id FROM software_key_contact_assignments WHERE software_key_id = $software_key_id");
|
||||
while ($assigned = mysqli_fetch_row($sql)) {
|
||||
$assigned_contacts[] = intval($assigned[0]);
|
||||
}
|
||||
|
||||
// Who is on the license already - listed first, so the usual pick is at the top
|
||||
$holders = softwareSeatHolders($software_id);
|
||||
$seat_assets = array_map('intval', array_column($holders['assets'], 'asset_id'));
|
||||
$seat_contacts = array_map('intval', array_column($holders['contacts'], 'contact_id'));
|
||||
|
||||
$asset_options = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT asset_id, asset_name, asset_type, asset_make, asset_model FROM assets
|
||||
WHERE asset_client_id = $client_id AND asset_archived_at IS NULL ORDER BY asset_name ASC");
|
||||
while ($option = mysqli_fetch_assoc($sql)) {
|
||||
$option['on_license'] = in_array(intval($option['asset_id']), $seat_assets, true);
|
||||
$asset_options[] = $option;
|
||||
}
|
||||
$contact_options = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT contact_id, contact_name, contact_title, contact_photo, contact_client_id FROM contacts
|
||||
WHERE contact_client_id = $client_id AND contact_archived_at IS NULL ORDER BY contact_name ASC");
|
||||
while ($option = mysqli_fetch_assoc($sql)) {
|
||||
$option['on_license'] = in_array(intval($option['contact_id']), $seat_contacts, true);
|
||||
$contact_options[] = $option;
|
||||
}
|
||||
$on_license_first = function ($a, $b) {
|
||||
return intval($b['on_license']) - intval($a['on_license']);
|
||||
};
|
||||
usort($asset_options, $on_license_first);
|
||||
usort($contact_options, $on_license_first);
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title text-truncate"><i class="fas fa-fw fa-key me-2"></i>Key on <strong><?= escapeHtml($row['software_name']) ?></strong></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="software_key_id" value="<?= $software_key_id ?>">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Key <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-fw fa-key"></i></span>
|
||||
<input type="text" class="form-control font-monospace" name="key" maxlength="400" value="<?= escapeHtml($key_row['software_key']) ?>" required>
|
||||
<button type="button" class="btn btn-outline-secondary clipboardjs" title="Copy key" data-clipboard-text="<?= escapeHtml($key_row['software_key']) ?>"><i class="far fa-copy"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Installed on</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-fw fa-desktop"></i></span>
|
||||
<select class="form-select select2" name="assets[]" multiple data-placeholder="- Nothing yet -">
|
||||
<?php foreach ($asset_options as $option) {
|
||||
$option_id = intval($option['asset_id']); ?>
|
||||
<option value="<?= $option_id ?>" <?= in_array($option_id, $assigned_assets, true) ? 'selected' : '' ?><?= selectOptionAsset($option['asset_type'], $option['on_license'] ? 'On this license' : 'Not on this license yet - gets a seat') ?>><?= escapeHtml($option['asset_name']) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Used by</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-fw fa-user"></i></span>
|
||||
<select class="form-select select2" name="contacts[]" multiple data-placeholder="- Nobody yet -">
|
||||
<?php foreach ($contact_options as $option) {
|
||||
$option_id = intval($option['contact_id']); ?>
|
||||
<option value="<?= $option_id ?>" <?= in_array($option_id, $assigned_contacts, true) ? 'selected' : '' ?><?= selectOptionContact($option['contact_client_id'], $option['contact_name'], $option['contact_photo'], $option['on_license'] ? 'On this license' : 'Not on this license yet - gets a seat', $option_id) ?>><?= escapeHtml($option['contact_name']) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-text">Leave both empty to put the key back in the pool.</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="edit_software_key" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Save</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
145
agent/modals/software/software_renew.php
Normal file
145
agent/modals/software/software_renew.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Renew one license (?id=) or the ones ticked on the list (?software_ids[]=).
|
||||
* Move each renewal date on by a term - from its current date, or from today
|
||||
* if there is none or it has passed - or set one date for all of them. The
|
||||
* new date for each shows before you save. Optionally record the new order
|
||||
* or invoice reference.
|
||||
*/
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$ids = isset($_GET['id']) ? [intval($_GET['id'])] : array_map('intval', (array) ($_GET['software_ids'] ?? []));
|
||||
|
||||
$licenses = [];
|
||||
foreach (array_unique($ids) as $software_id) {
|
||||
$row = softwareRow($software_id);
|
||||
if (!$row) {
|
||||
continue;
|
||||
}
|
||||
$client_id = intval($row['software_client_id']);
|
||||
enforceClientAccess();
|
||||
$licenses[] = $row;
|
||||
}
|
||||
if (!$licenses) {
|
||||
echo json_encode(['error' => 'Tick the licenses to renew first.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$single = count($licenses) === 1;
|
||||
$default_term = '1y';
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title text-truncate"><i class="fas fa-fw fa-redo-alt me-2"></i>Renew <?= $single ? '<strong>' . escapeHtml($licenses[0]['software_name']) . '</strong>' : '<strong>' . count($licenses) . '</strong> licenses' ?></h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form action="post.php" method="post" autocomplete="off" data-renew-form>
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<?php foreach ($licenses as $row) { ?><input type="hidden" name="software_ids[]" value="<?= intval($row['software_id']) ?>"><?php } ?>
|
||||
<input type="hidden" name="renew_mode" value="extend" data-renew-mode>
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<label class="form-label">Renew for</label>
|
||||
<div class="d-flex flex-wrap gap-2 mb-2" role="group" aria-label="Term">
|
||||
<?php foreach (SOFTWARE_RENEWAL_TERMS as $term => $term_label) { ?>
|
||||
<input type="radio" class="btn-check" name="renew_term" id="renewTerm<?= $term ?>" value="<?= $term ?>" <?= $term === $default_term ? 'checked' : '' ?>>
|
||||
<label class="btn btn-outline-primary btn-sm" for="renewTerm<?= $term ?>"><?= $term_label ?></label>
|
||||
<?php } ?>
|
||||
<input type="radio" class="btn-check" name="renew_term" id="renewTermDate" value="date">
|
||||
<label class="btn btn-outline-secondary btn-sm" for="renewTermDate"><i class="fas fa-calendar-alt me-1"></i>A date</label>
|
||||
</div>
|
||||
<div class="mb-3" data-renew-date-box hidden>
|
||||
<input type="date" class="form-control" name="renew_date" max="2999-12-31" data-renew-date>
|
||||
</div>
|
||||
<div class="form-text mb-3">Each date moves on from its current renewal date, or from today if it has already passed.</div>
|
||||
|
||||
<div class="table-responsive border rounded mb-3">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead class="table-light text-nowrap"><tr><th class="ps-2">License</th><th>Renews now</th><th class="pe-2">New date</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($licenses as $row) {
|
||||
$renewal = softwareRenewal($row['software_expire']);
|
||||
$next = [];
|
||||
foreach (array_keys(SOFTWARE_RENEWAL_TERMS) as $term) {
|
||||
$next[$term] = softwareRenewalExtended($row['software_expire'], $term);
|
||||
} ?>
|
||||
<tr>
|
||||
<td class="ps-2">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<?= softwareTile($row['software_type'], 'sm') ?>
|
||||
<div class="min-w-0">
|
||||
<div class="fw-semibold text-truncate"><?= escapeHtml($row['software_name']) ?></div>
|
||||
<?php if (!$single) { ?><div class="small text-body-secondary text-truncate"><?= escapeHtml($row['client_name']) ?></div><?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
<?php if ($renewal) { ?>
|
||||
<?= escapeHtml($renewal['date']) ?>
|
||||
<div class="small text-<?= $renewal['tone'] === 'success' ? 'body-secondary' : $renewal['tone'] . '-emphasis' ?>"><?= escapeHtml($renewal['text']) ?></div>
|
||||
<?php } else { ?>
|
||||
<span class="text-body-secondary">No date</span>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td class="text-nowrap fw-semibold text-success-emphasis pe-2" data-renew-next='<?= escapeHtml(json_encode($next)) ?>'><?= $next[$default_term] ?></td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<label class="form-label">New order / invoice reference</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-fw fa-receipt"></i></span>
|
||||
<input type="text" class="form-control" name="purchase_reference" maxlength="200" placeholder="Leave blank to keep <?= $single && $licenses[0]['software_purchase_reference'] ? escapeHtml($licenses[0]['software_purchase_reference']) : 'what each has' ?>">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="renew_software" class="btn btn-primary text-bold"><i class="fas fa-redo-alt me-2"></i>Renew</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var form = document.querySelector('form[data-renew-form]:not([data-wired])');
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
form.dataset.wired = '1';
|
||||
var mode = form.querySelector('[data-renew-mode]');
|
||||
var dateBox = form.querySelector('[data-renew-date-box]');
|
||||
var date = form.querySelector('[data-renew-date]');
|
||||
function update() {
|
||||
var term = form.querySelector('[name="renew_term"]:checked').value;
|
||||
var byDate = term === 'date';
|
||||
mode.value = byDate ? 'date' : 'extend';
|
||||
dateBox.hidden = !byDate;
|
||||
date.required = byDate;
|
||||
form.querySelectorAll('[data-renew-next]').forEach(function (cell) {
|
||||
cell.textContent = byDate ? (date.value || '-') : JSON.parse(cell.dataset.renewNext)[term];
|
||||
});
|
||||
}
|
||||
form.querySelectorAll('[name="renew_term"]').forEach(function (radio) {
|
||||
radio.addEventListener('change', function () {
|
||||
update();
|
||||
if (radio.value === 'date') {
|
||||
date.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
date.addEventListener('input', update);
|
||||
update();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
124
agent/modals/software/software_seats.php
Normal file
124
agent/modals/software/software_seats.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Who and what is on a license - the client's devices and users in two
|
||||
* pickers, with the seats they take against the seats bought as you pick.
|
||||
* Taking someone off gives back any key they had.
|
||||
*/
|
||||
|
||||
require_once '../../../includes/modal_header.php';
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$software_id = intval($_GET['id'] ?? 0);
|
||||
$row = softwareRow($software_id);
|
||||
if (!$row) {
|
||||
echo json_encode(['error' => 'That license no longer exists.']);
|
||||
exit;
|
||||
}
|
||||
$client_id = intval($row['software_client_id']);
|
||||
enforceClientAccess();
|
||||
|
||||
$seats_total = intval($row['software_seats']);
|
||||
$holders = softwareSeatHolders($software_id);
|
||||
$asset_ids = array_map('intval', array_column($holders['assets'], 'asset_id'));
|
||||
$contact_ids = array_map('intval', array_column($holders['contacts'], 'contact_id'));
|
||||
|
||||
$asset_options = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT asset_id, asset_name, asset_type, asset_make, asset_model, contact_name FROM assets
|
||||
LEFT JOIN contacts ON contact_id = asset_contact_id
|
||||
WHERE asset_client_id = $client_id AND asset_archived_at IS NULL ORDER BY asset_name ASC");
|
||||
while ($option = mysqli_fetch_assoc($sql)) {
|
||||
$asset_options[] = $option;
|
||||
}
|
||||
$contact_options = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT contact_id, contact_name, contact_title, contact_photo, contact_client_id FROM contacts
|
||||
WHERE contact_client_id = $client_id AND contact_archived_at IS NULL ORDER BY contact_name ASC");
|
||||
while ($option = mysqli_fetch_assoc($sql)) {
|
||||
$contact_options[] = $option;
|
||||
}
|
||||
|
||||
// Start on users for a per-user license, devices otherwise
|
||||
$users_first = in_array($row['software_license_type'], ['User', 'Subscription', 'Concurrent'], true);
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title text-truncate"><i class="fas fa-fw fa-users me-2"></i>Seats on <strong><?= escapeHtml($row['software_name']) ?></strong></h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form action="post.php" method="post" autocomplete="off" data-seats-form>
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<input type="hidden" name="software_id" value="<?= $software_id ?>">
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="d-flex align-items-center justify-content-between gap-2 mb-3">
|
||||
<span class="text-body-secondary small">Each device and user here takes a seat.<?= $seats_total ? '' : ' This license has no seat limit.' ?></span>
|
||||
<span class="badge rounded-pill fs-6 fw-normal" data-seat-count></span>
|
||||
</div>
|
||||
<?php
|
||||
$pickers = [
|
||||
'contacts' => ['label' => 'Users', 'icon' => 'fa-user', 'placeholder' => $contact_options ? 'Pick the people licensed to use it' : 'This client has no contacts yet'],
|
||||
'assets' => ['label' => 'Devices', 'icon' => 'fa-desktop', 'placeholder' => $asset_options ? 'Pick the devices it is installed on' : 'This client has no assets yet'],
|
||||
];
|
||||
if (!$users_first) {
|
||||
$pickers = array_reverse($pickers, true);
|
||||
}
|
||||
foreach ($pickers as $field => $picker) { ?>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= $picker['label'] ?></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-fw <?= $picker['icon'] ?>"></i></span>
|
||||
<select class="form-select select2" name="<?= $field ?>[]" multiple data-placeholder="<?= $picker['placeholder'] ?>" data-seat-pick>
|
||||
<?php if ($field === 'assets') {
|
||||
foreach ($asset_options as $option) {
|
||||
$option_id = intval($option['asset_id']); ?>
|
||||
<option value="<?= $option_id ?>" <?= in_array($option_id, $asset_ids, true) ? 'selected' : '' ?><?= selectOptionAsset($option['asset_type'], implode(' · ', array_filter([trim($option['asset_make'] . ' ' . $option['asset_model']), $option['contact_name']]))) ?>><?= escapeHtml($option['asset_name']) ?></option>
|
||||
<?php }
|
||||
} else {
|
||||
foreach ($contact_options as $option) {
|
||||
$option_id = intval($option['contact_id']); ?>
|
||||
<option value="<?= $option_id ?>" <?= in_array($option_id, $contact_ids, true) ? 'selected' : '' ?><?= selectOptionContact($option['contact_client_id'], $option['contact_name'], $option['contact_photo'], $option['contact_title'], $option_id) ?>><?= escapeHtml($option['contact_name']) ?></option>
|
||||
<?php }
|
||||
} ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="set_software_seats" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Save</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var form = document.querySelector('form[data-seats-form]:not([data-wired])');
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
form.dataset.wired = '1';
|
||||
var bought = <?= $seats_total ?>;
|
||||
var picks = form.querySelectorAll('[data-seat-pick]');
|
||||
var count = form.querySelector('[data-seat-count]');
|
||||
function seats() {
|
||||
var used = 0;
|
||||
picks.forEach(function (select) {
|
||||
used += select.selectedOptions.length;
|
||||
});
|
||||
var over = bought && used > bought;
|
||||
count.textContent = (bought ? used + ' of ' + bought + ' seats' : used + ' in use') + (over ? ' - ' + (used - bought) + ' over' : '');
|
||||
count.className = 'badge rounded-pill fs-6 fw-normal ' + (over ? 'text-bg-danger' : (bought && used === bought ? 'text-bg-warning' : 'text-bg-secondary'));
|
||||
}
|
||||
picks.forEach(function (select) {
|
||||
select.addEventListener('change', seats);
|
||||
select.addEventListener('input', seats);
|
||||
});
|
||||
seats();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
@@ -7,6 +7,12 @@ $contact_id = intval($_GET['contact_id'] ?? 0);
|
||||
$project_id = intval($_GET['project_id'] ?? 0);
|
||||
$asset_id = intval($_GET['asset_id'] ?? 0);
|
||||
|
||||
// A ticket started from somewhere that knows what it is about (a license's
|
||||
// Renewal Ticket) - plain text in, escaped here
|
||||
$prefill_subject = mb_substr(trim((string) ($_GET['subject'] ?? '')), 0, 500);
|
||||
$prefill_details = trim((string) ($_GET['details'] ?? ''));
|
||||
$prefill_details_html = $prefill_details !== '' ? '<p>' . nl2br(escapeHtml($prefill_details)) . '</p>' : '';
|
||||
|
||||
if ($client_id) {
|
||||
enforceClientAccess();
|
||||
}
|
||||
@@ -91,12 +97,12 @@ ob_start();
|
||||
<label>Subject <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-tag"></i></span>
|
||||
<input type="text" class="form-control" id="subjectInput" name="subject" placeholder="Subject" maxlength="500" required>
|
||||
<input type="text" class="form-control" id="subjectInput" name="subject" placeholder="Subject" maxlength="500" value="<?= escapeHtml($prefill_subject) ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control tinymceTicket" id="detailsInput" name="details"></textarea>
|
||||
<textarea class="form-control tinymceTicket" id="detailsInput" name="details"><?= escapeHtml($prefill_details_html) ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
31
agent/modals/vendor/vendor.php
vendored
31
agent/modals/vendor/vendor.php
vendored
@@ -15,8 +15,8 @@ $name = escapeHtml($row['vendor_name']);
|
||||
$description = escapeHtml($row['vendor_description']);
|
||||
$account_number = escapeHtml($row['vendor_account_number']);
|
||||
$contact_name = escapeHtml($row['vendor_contact_name']);
|
||||
$phone = preg_replace("/[^0-9]/", '',$row['vendor_phone']);
|
||||
$extension = preg_replace("/[^0-9]/", '',$row['vendor_extension']);
|
||||
$phone = preg_replace("/[^0-9]/", '', (string) $row['vendor_phone']);
|
||||
$extension = preg_replace("/[^0-9]/", '', (string) $row['vendor_extension']);
|
||||
$email = escapeHtml($row['vendor_email']);
|
||||
$website = escapeHtml($row['vendor_website']);
|
||||
$hours = escapeHtml($row['vendor_hours']);
|
||||
@@ -29,6 +29,19 @@ if ($client_id) {
|
||||
enforceClientAccess();
|
||||
}
|
||||
|
||||
// Licenses bought through this vendor - soonest renewal first
|
||||
$vendor_licenses = [];
|
||||
if (lookupUserPermission('module_support') >= 1) {
|
||||
$sql_vendor_licenses = mysqli_query($mysqli, "SELECT software_id AS id, software_name AS name, software_type, software_version, software_license_type,
|
||||
software_seats, software_expire, " . softwareSeatsUsedSql('software.software_id') . " AS seats_used
|
||||
FROM software
|
||||
WHERE software_vendor_id = $vendor_id AND software_archived_at IS NULL " . clientScopeSql('software_client_id') . "
|
||||
ORDER BY software_expire IS NULL, software_expire ASC, software_name ASC");
|
||||
while ($vendor_license = mysqli_fetch_assoc($sql_vendor_licenses)) {
|
||||
$vendor_licenses[] = $vendor_license;
|
||||
}
|
||||
}
|
||||
|
||||
ob_start();
|
||||
|
||||
?>
|
||||
@@ -80,6 +93,20 @@ ob_start();
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($vendor_licenses) { ?>
|
||||
<!-- Licenses from this vendor -->
|
||||
<div class="card mb-3 shadow-sm rounded">
|
||||
<div class="card-body">
|
||||
<h6 class="text-secondary"><i class="fas fa-cube me-2"></i>Licenses <span class="badge rounded-pill text-bg-secondary ms-1"><?= count($vendor_licenses) ?></span></h6>
|
||||
<ul class="list-unstyled mb-0 itflow-links">
|
||||
<?php foreach ($vendor_licenses as $vendor_license) { ?>
|
||||
<li class="itflow-link-row"><?= linkRowHtml('software', $vendor_license, $client_id) ?></li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Notes Card -->
|
||||
<div class="card mb-3 shadow-sm rounded">
|
||||
<div class="card-body">
|
||||
|
||||
@@ -856,65 +856,7 @@ if (isset($_POST['bulk_delete_assets'])) {
|
||||
|
||||
// BEGIN LINKING
|
||||
|
||||
if (isset($_POST['link_software_to_asset'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$software_id = intval($_POST['software_id']);
|
||||
$asset_id = intval($_POST['asset_id']);
|
||||
|
||||
// Get software Name and Client ID for logging
|
||||
$sql_software = mysqli_query($mysqli,"SELECT software_name, software_client_id FROM software WHERE software_id = $software_id");
|
||||
$row = mysqli_fetch_assoc($sql_software);
|
||||
$software_name = escapeSql($row['software_name']);
|
||||
$client_id = intval($row['software_client_id']);
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
// Get Asset Name for logging
|
||||
$asset_name = escapeSql(getFieldById('assets', $asset_id, 'asset_name'));
|
||||
|
||||
mysqli_query($mysqli,"INSERT INTO software_assets SET asset_id = $asset_id, software_id = $software_id");
|
||||
|
||||
logAudit("Software", "Link", "$session_name added software license $software_name to asset $asset_name", $client_id, $software_id);
|
||||
|
||||
flashAlert("Software <strong>$software_name</strong> licensed for asset <strong>$asset_name</strong>");
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_GET['unlink_software_from_asset'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$asset_id = intval($_GET['asset_id']);
|
||||
$software_id = intval($_GET['software_id']);
|
||||
|
||||
// Get software Name and Client ID for logging
|
||||
$sql_software = mysqli_query($mysqli,"SELECT software_name, software_client_id FROM software WHERE software_id = $software_id");
|
||||
$row = mysqli_fetch_assoc($sql_software);
|
||||
$software_name = escapeSql($row['software_name']);
|
||||
$client_id = intval($row['software_client_id']);
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
// Get Asset Name for logging
|
||||
$asset_name = escapeSql(getFieldById('assets', $asset_id, 'asset_name'));
|
||||
|
||||
mysqli_query($mysqli,"DELETE FROM software_assets WHERE asset_id = $asset_id AND software_id = $software_id");
|
||||
|
||||
logAudit("software", "Unlink", "$session_name removed software license $software_name from asset $asset_name", $client_id, $software_id);
|
||||
|
||||
flashAlert("Removed Software License <strong>$software_name</strong> for Asset <strong>$asset_name</strong>", 'error');
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
// Licenses are linked in post/link.php (link_software_to_asset)
|
||||
|
||||
// Right now 1 login and have many assets but not many to many
|
||||
if (isset($_POST['link_asset_to_credential'])) {
|
||||
|
||||
@@ -990,65 +990,7 @@ if (isset($_GET['unlink_asset_from_contact'])) {
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['link_software_to_contact'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_client', 2);
|
||||
|
||||
$software_id = intval($_POST['software_id']);
|
||||
$contact_id = intval($_POST['contact_id']);
|
||||
|
||||
// Get software Name and Client ID for logging
|
||||
$sql_software = mysqli_query($mysqli,"SELECT software_name, software_client_id FROM software WHERE software_id = $software_id");
|
||||
$row = mysqli_fetch_assoc($sql_software);
|
||||
$software_name = escapeSql($row['software_name']);
|
||||
$client_id = intval($row['software_client_id']);
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
// Get Contact Name for logging
|
||||
$contact_name = escapeSql(getFieldById('contacts', $contact_id, 'contact_name'));
|
||||
|
||||
mysqli_query($mysqli,"INSERT INTO software_contacts SET contact_id = $contact_id, software_id = $software_id");
|
||||
|
||||
logAudit("Software", "Link", "$session_name added software license $software_name to contact $contact_name", $client_id, $software_id);
|
||||
|
||||
flashAlert("Software <strong>$software_name</strong> licensed for contact <strong>$contact_name</strong>");
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_GET['unlink_software_from_contact'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_client', 2);
|
||||
|
||||
$contact_id = intval($_GET['contact_id']);
|
||||
$software_id = intval($_GET['software_id']);
|
||||
|
||||
// Get software Name and Client ID for logging
|
||||
$sql_software = mysqli_query($mysqli,"SELECT software_name, software_client_id FROM software WHERE software_id = $software_id");
|
||||
$row = mysqli_fetch_assoc($sql_software);
|
||||
$software_name = escapeSql($row['software_name']);
|
||||
$client_id = intval($row['software_client_id']);
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
// Get Contact Name for logging
|
||||
$contact_name = escapeSql(getFieldById('contacts', $contact_id, 'contact_name'));
|
||||
|
||||
mysqli_query($mysqli,"DELETE FROM software_contacts WHERE contact_id = $contact_id AND software_id = $software_id");
|
||||
|
||||
logAudit("software", "Unlink", "$session_name removed software license $software_name from contact $contact_name", $client_id, $software_id);
|
||||
|
||||
flashAlert("Removed Software License <strong>$software_name</strong> for Contact <strong>$contact_name</strong>", 'error');
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
// Licenses are linked in post/link.php (link_software_to_contact)
|
||||
|
||||
if (isset($_POST['link_contact_to_credential'])) {
|
||||
|
||||
|
||||
@@ -52,11 +52,13 @@ if (isset($_POST['add_document'])) {
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
// Only the client's own folder, contact and asset
|
||||
// Only the client's own folder, contact, asset and license
|
||||
$folder = folderOwnedId($folder, $client_id);
|
||||
$contact_id = assetClientOwnedId('contact', $_POST['contact'] ?? 0, $client_id);
|
||||
$asset = linkRecord('asset', $_POST['asset'] ?? 0);
|
||||
$asset_id = $asset && intval($asset['client_id']) === $client_id ? intval($asset['id']) : 0;
|
||||
$software = linkRecord('software', $_POST['software'] ?? 0);
|
||||
$software_id = $software && intval($software['client_id']) === $client_id ? intval($software['id']) : 0;
|
||||
$client_visible = intval($_POST['client_visible'] ?? 0) ? 1 : 0;
|
||||
|
||||
if ($name === '') {
|
||||
@@ -84,13 +86,16 @@ if (isset($_POST['add_document'])) {
|
||||
if ($asset_id) {
|
||||
mysqli_query($mysqli, "INSERT IGNORE INTO asset_documents SET asset_id = $asset_id, document_id = $document_id");
|
||||
}
|
||||
if ($software_id) {
|
||||
mysqli_query($mysqli, "INSERT IGNORE INTO software_documents SET software_id = $software_id, document_id = $document_id");
|
||||
}
|
||||
|
||||
logAudit("Document", "Create", "$session_name created document $name", $client_id, $document_id);
|
||||
|
||||
flashAlert("Document <strong>" . trim($_POST['name']) . "</strong> created");
|
||||
|
||||
// From an asset or a contact, back to it; otherwise to the new document
|
||||
redirect(($contact_id || $asset_id) ? null : "document.php?client_id=$client_id&document_id=$document_id");
|
||||
// From an asset, a contact or a license, back to it; otherwise to the new document
|
||||
redirect(($contact_id || $asset_id || $software_id) ? null : "document.php?client_id=$client_id&document_id=$document_id");
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -28,11 +28,13 @@ if (isset($_POST['upload_files'])) {
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
// Only the client's own folder, contact and asset
|
||||
// Only the client's own folder, contact, asset and license
|
||||
$folder_id = folderOwnedId($_POST['folder_id'] ?? 0, $client_id);
|
||||
$contact_id = assetClientOwnedId('contact', $_POST['contact_id'] ?? 0, $client_id);
|
||||
$asset = linkRecord('asset', $_POST['asset_id'] ?? 0);
|
||||
$asset_id = $asset && intval($asset['client_id']) === $client_id ? intval($asset['id']) : 0;
|
||||
$software = linkRecord('software', $_POST['software_id'] ?? 0);
|
||||
$software_id = $software && intval($software['client_id']) === $client_id ? intval($software['id']) : 0;
|
||||
|
||||
if (!is_dir($client_dir)) {
|
||||
mkdir($client_dir, 0755, true);
|
||||
@@ -97,6 +99,9 @@ if (isset($_POST['upload_files'])) {
|
||||
if ($asset_id) {
|
||||
mysqli_query($mysqli, "INSERT IGNORE INTO asset_files SET asset_id = $asset_id, file_id = $file_id");
|
||||
}
|
||||
if ($software_id) {
|
||||
mysqli_query($mysqli, "INSERT IGNORE INTO software_files SET software_id = $software_id, file_id = $file_id");
|
||||
}
|
||||
|
||||
logAudit("File", "Upload", "$session_name uploaded file $file_name", $client_id, $file_id);
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - links between documents, files and what they are about.
|
||||
* ITFlow - links between documents, files and what they are about, and the
|
||||
* devices, users, credentials and files on a license.
|
||||
*
|
||||
* One handler for every pair LINK_RELATIONS knows (functions/links.php):
|
||||
* link_items links any number of records at once, unlink_item drops one.
|
||||
@@ -35,12 +36,21 @@ function linkHandle($action, $from, $id, $to, $ids) {
|
||||
|
||||
if ($action === 'link') {
|
||||
$added = linkAdd($from, $source['id'], $to, $ids);
|
||||
|
||||
// A license that has just gone over its seats says so
|
||||
$seat_warning = '';
|
||||
if ($added && in_array($relation['table'], ['software_assets', 'software_contacts'], true)) {
|
||||
foreach ($from === 'software' ? [$source['id']] : (array) $ids as $software_id) {
|
||||
$seat_warning .= softwareSeatWarning($software_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$added) {
|
||||
flashAlert("Nothing new to link to <strong>{$source['name']}</strong>.", 'warning');
|
||||
} elseif (count($added) === 1) {
|
||||
flashAlert(ucfirst($to_entity['label']) . " <strong>{$added[0]}</strong> linked to <strong>{$source['name']}</strong>");
|
||||
flashAlert(ucfirst($to_entity['label']) . " <strong>{$added[0]}</strong> linked to <strong>{$source['name']}</strong>$seat_warning", $seat_warning ? 'warning' : 'success');
|
||||
} else {
|
||||
flashAlert("Linked <strong>" . count($added) . "</strong> " . strtolower($to_entity['plural']) . " to <strong>{$source['name']}</strong>");
|
||||
flashAlert("Linked <strong>" . count($added) . "</strong> " . strtolower($to_entity['plural']) . " to <strong>{$source['name']}</strong>$seat_warning", $seat_warning ? 'warning' : 'success');
|
||||
}
|
||||
} else {
|
||||
$removed = linkRemove($from, $source['id'], $to, intval($ids[0] ?? 0));
|
||||
@@ -70,6 +80,8 @@ $legacy_links = [
|
||||
'vendor_to_document' => ['document', 'document_id', 'vendor', 'vendor_id'],
|
||||
'asset_to_file' => ['file', 'file_id', 'asset', 'asset_id'],
|
||||
'contact_to_file' => ['file', 'file_id', 'contact', 'contact_id'],
|
||||
'software_to_asset' => ['asset', 'asset_id', 'software', 'software_id'],
|
||||
'software_to_contact' => ['contact', 'contact_id', 'software', 'software_id'],
|
||||
];
|
||||
foreach ($legacy_links as $legacy => [$from, $from_field, $to, $to_field]) {
|
||||
if (isset($_POST["link_$legacy"])) {
|
||||
|
||||
@@ -1,46 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - GET/POST request handler for client software & licenses
|
||||
* ITFlow - GET/POST request handler for client licenses (the software table)
|
||||
*
|
||||
* The license itself, its seats (the devices and users on it), its individual
|
||||
* keys and who each is handed to, renewals one at a time or in bulk, and the
|
||||
* export. Every handler checks the license's own client, and every device,
|
||||
* user and vendor a form names must be that client's - a form posting another
|
||||
* client's asset id gets nothing. Links to documents, files and credentials,
|
||||
* and single seats from the asset and contact pages, go through post/link.php.
|
||||
*/
|
||||
|
||||
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
|
||||
|
||||
if (isset($_POST['add_software_from_template'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
// GET POST Data
|
||||
$client_id = intval($_POST['client_id']);
|
||||
$software_template_id = intval($_POST['software_template_id']);
|
||||
|
||||
/**
|
||||
* The license a request names, after the client access check - or back to
|
||||
* the page with an error if it is gone.
|
||||
*/
|
||||
function softwareForRequest($software_id) {
|
||||
global $client_id;
|
||||
$row = softwareRow($software_id);
|
||||
if (!$row) {
|
||||
flashAlert('That license no longer exists.', 'error');
|
||||
redirect();
|
||||
}
|
||||
$client_id = intval($row['software_client_id']);
|
||||
enforceClientAccess();
|
||||
return $row;
|
||||
}
|
||||
|
||||
// GET Software Template Info
|
||||
$sql_software_templates = mysqli_query($mysqli,"SELECT software_template_description, software_template_license_type, software_template_name,
|
||||
software_template_notes, software_template_type, software_template_version FROM software_templates WHERE software_template_id = $software_template_id");
|
||||
$row = mysqli_fetch_assoc($sql_software_templates);
|
||||
$name = escapeSql($row['software_template_name']);
|
||||
$version = escapeSql($row['software_template_version']);
|
||||
$description = escapeSql($row['software_template_description']);
|
||||
$type = escapeSql($row['software_template_type']);
|
||||
$license_type = escapeSql($row['software_template_license_type']);
|
||||
$notes = escapeSql($row['software_template_notes']);
|
||||
$vendor = intval($_POST['vendor'] ?? 0);
|
||||
|
||||
// Software add query
|
||||
mysqli_query($mysqli,"INSERT INTO software SET software_name = '$name', software_version = '$version', software_description = '$description', software_type = '$type', software_license_type = '$license_type', software_notes = '$notes', software_vendor_id = $vendor, software_client_id = $client_id");
|
||||
|
||||
$software_id = mysqli_insert_id($mysqli);
|
||||
|
||||
logAudit("Software", "Create", "$session_name created software $name using template", $client_id, $software_id);
|
||||
|
||||
flashAlert("Software <strong>$name</strong> created from template");
|
||||
|
||||
redirect();
|
||||
|
||||
/** The license ids a bulk form posted, each checked for client access. Returns [id => row]. */
|
||||
function softwareBulkRows($ids) {
|
||||
global $client_id;
|
||||
$rows = [];
|
||||
foreach (array_unique(array_map('intval', (array) $ids)) as $software_id) {
|
||||
$row = softwareRow($software_id);
|
||||
if (!$row) {
|
||||
continue;
|
||||
}
|
||||
$client_id = intval($row['software_client_id']);
|
||||
enforceClientAccess();
|
||||
$rows[$software_id] = $row;
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
if (isset($_POST['add_software'])) {
|
||||
@@ -49,58 +51,66 @@ if (isset($_POST['add_software'])) {
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$client_id = intval($_POST['client_id']);
|
||||
$name = escapeSql($_POST['name']);
|
||||
$version = escapeSql($_POST['version']);
|
||||
$description = escapeSql($_POST['description']);
|
||||
$type = escapeSql($_POST['type']);
|
||||
$license_type = escapeSql($_POST['license_type']);
|
||||
$notes = escapeSql($_POST['notes']);
|
||||
$key = escapeSql($_POST['key']);
|
||||
$seats = intval($_POST['seats']);
|
||||
$purchase_reference = escapeSql($_POST['purchase_reference']);
|
||||
$purchase = escapeSql($_POST['purchase']);
|
||||
if (empty($purchase)) {
|
||||
$purchase = "NULL";
|
||||
} else {
|
||||
$purchase = "'" . $purchase . "'";
|
||||
}
|
||||
$expire = escapeSql($_POST['expire']);
|
||||
if (empty($expire)) {
|
||||
$expire = "NULL";
|
||||
} else {
|
||||
$expire = "'" . $expire . "'";
|
||||
}
|
||||
$notes = escapeSql($_POST['notes']);
|
||||
$vendor = intval($_POST['vendor'] ?? 0);
|
||||
$client_id = intval($_POST['client_id'] ?? 0);
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
mysqli_query($mysqli,"INSERT INTO software SET software_name = '$name', software_version = '$version', software_description = '$description', software_type = '$type', software_key = '$key', software_license_type = '$license_type', software_seats = $seats, software_purchase_reference = '$purchase_reference', software_purchase = $purchase, software_expire = $expire, software_notes = '$notes', software_vendor_id = $vendor, software_client_id = $client_id");
|
||||
$set = softwareFormSet($_POST, $client_id);
|
||||
if ($set === null) {
|
||||
flashAlert('A license needs a name.', 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
mysqli_query($mysqli, "INSERT INTO software SET $set, software_client_id = $client_id");
|
||||
$software_id = mysqli_insert_id($mysqli);
|
||||
|
||||
$changes = softwareSyncSeats($software_id, $client_id, $_POST['assets'] ?? [], $_POST['contacts'] ?? []);
|
||||
$keys_added = softwareAddKeys($software_id, $_POST['keys'] ?? '');
|
||||
|
||||
$name = trim($_POST['name']);
|
||||
logAudit("Software", "Create", "$session_name created license " . escapeSql($name . softwareSeatChangeText($changes)), $client_id, $software_id);
|
||||
|
||||
$warning = softwareSeatWarning($software_id);
|
||||
flashAlert("License <strong>$name</strong> created" . ($keys_added ? " with $keys_added key" . ($keys_added === 1 ? '' : 's') : '') . $warning, $warning ? 'warning' : 'success');
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['add_software_from_template'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$client_id = intval($_POST['client_id'] ?? 0);
|
||||
$software_template_id = intval($_POST['software_template_id'] ?? 0);
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT software_template_description, software_template_license_type, software_template_name,
|
||||
software_template_notes, software_template_type, software_template_version FROM software_templates
|
||||
WHERE software_template_id = $software_template_id AND software_template_archived_at IS NULL"));
|
||||
if (!$row) {
|
||||
flashAlert('That template no longer exists.', 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
$name = escapeSql($row['software_template_name']);
|
||||
$version = escapeSql($row['software_template_version']);
|
||||
$description = escapeSql($row['software_template_description']);
|
||||
$type = escapeSql($row['software_template_type']);
|
||||
$license_type = escapeSql($row['software_template_license_type']);
|
||||
$notes = escapeSql($row['software_template_notes']);
|
||||
$vendor = assetClientOwnedId('vendor', $_POST['vendor'] ?? 0, $client_id);
|
||||
|
||||
mysqli_query($mysqli, "INSERT INTO software SET software_name = '$name', software_version = '$version', software_description = '$description', software_type = '$type', software_license_type = '$license_type', software_notes = '$notes', software_vendor_id = $vendor, software_client_id = $client_id");
|
||||
|
||||
$software_id = mysqli_insert_id($mysqli);
|
||||
|
||||
$alert_extended = "";
|
||||
logAudit("Software", "Create", "$session_name created license $name from a template", $client_id, $software_id);
|
||||
|
||||
// Add Asset Licenses
|
||||
if (isset($_POST['assets'])) {
|
||||
foreach($_POST['assets'] as $asset) {
|
||||
$asset_id = intval($asset);
|
||||
mysqli_query($mysqli,"INSERT INTO software_assets SET software_id = $software_id, asset_id = $asset_id");
|
||||
}
|
||||
}
|
||||
|
||||
// Add Contact Licenses
|
||||
if (isset($_POST['contacts'])) {
|
||||
foreach($_POST['contacts'] as $contact) {
|
||||
$contact = intval($contact);
|
||||
mysqli_query($mysqli,"INSERT INTO software_contacts SET software_id = $software_id, contact_id = $contact");
|
||||
}
|
||||
}
|
||||
|
||||
logAudit("Software", "Create", "$session_name created software $name", $client_id, $software_id);
|
||||
|
||||
flashAlert("Software <strong>$name</strong> created $alert_extended");
|
||||
flashAlert("License <strong>{$row['software_template_name']}</strong> created from template");
|
||||
|
||||
redirect();
|
||||
|
||||
@@ -112,59 +122,191 @@ if (isset($_POST['edit_software'])) {
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$software_id = intval($_POST['software_id']);
|
||||
$name = escapeSql($_POST['name']);
|
||||
$version = escapeSql($_POST['version']);
|
||||
$description = escapeSql($_POST['description']);
|
||||
$type = escapeSql($_POST['type']);
|
||||
$license_type = escapeSql($_POST['license_type']);
|
||||
$notes = escapeSql($_POST['notes']);
|
||||
$key = escapeSql($_POST['key']);
|
||||
$seats = intval($_POST['seats']);
|
||||
$purchase_reference = escapeSql($_POST['purchase_reference']);
|
||||
$purchase = escapeSql($_POST['purchase']);
|
||||
if (empty($purchase)) {
|
||||
$purchase = "NULL";
|
||||
$row = softwareForRequest($_POST['software_id'] ?? 0);
|
||||
$software_id = intval($row['software_id']);
|
||||
|
||||
$set = softwareFormSet($_POST, $client_id);
|
||||
if ($set === null) {
|
||||
flashAlert('A license needs a name.', 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
mysqli_query($mysqli, "UPDATE software SET $set WHERE software_id = $software_id");
|
||||
|
||||
// Only a form that showed the seats changes them
|
||||
$changes = ['added' => [], 'removed' => []];
|
||||
if (isset($_POST['seats_form'])) {
|
||||
$changes = softwareSyncSeats($software_id, $client_id, $_POST['assets'] ?? [], $_POST['contacts'] ?? []);
|
||||
}
|
||||
|
||||
$name = trim($_POST['name']);
|
||||
logAudit("Software", "Edit", "$session_name edited license " . escapeSql($name . softwareSeatChangeText($changes)), $client_id, $software_id);
|
||||
|
||||
$warning = softwareSeatWarning($software_id);
|
||||
flashAlert("License <strong>$name</strong> updated$warning", $warning ? 'warning' : 'success');
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['set_software_seats'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$row = softwareForRequest($_POST['software_id'] ?? 0);
|
||||
$software_id = intval($row['software_id']);
|
||||
|
||||
$changes = softwareSyncSeats($software_id, $client_id, $_POST['assets'] ?? [], $_POST['contacts'] ?? []);
|
||||
|
||||
if ($changes['added'] || $changes['removed']) {
|
||||
logAudit("Software", "Seats", "$session_name changed seats on license " . escapeSql($row['software_name'] . softwareSeatChangeText($changes)), $client_id, $software_id);
|
||||
}
|
||||
|
||||
$warning = softwareSeatWarning($software_id);
|
||||
$summary = [];
|
||||
if ($changes['added']) {
|
||||
$summary[] = count($changes['added']) . ' added';
|
||||
}
|
||||
if ($changes['removed']) {
|
||||
$summary[] = count($changes['removed']) . ' removed';
|
||||
}
|
||||
flashAlert("Seats on <strong>{$row['software_name']}</strong> " . ($summary ? 'updated - ' . implode(', ', $summary) : 'unchanged') . $warning, $warning ? 'warning' : 'success');
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['renew_software'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$rows = softwareBulkRows($_POST['software_ids'] ?? []);
|
||||
$mode = ($_POST['renew_mode'] ?? '') === 'date' ? 'date' : 'extend';
|
||||
$term = (string) ($_POST['renew_term'] ?? '');
|
||||
$new_date = softwareFormDate($_POST['renew_date'] ?? '');
|
||||
$reference = trim((string) ($_POST['purchase_reference'] ?? ''));
|
||||
|
||||
if (!$rows || ($mode === 'extend' && !isset(SOFTWARE_RENEWAL_TERMS[$term])) || ($mode === 'date' && !$new_date)) {
|
||||
flashAlert('Pick a term or a new renewal date.', 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
$renewed = [];
|
||||
foreach ($rows as $software_id => $row) {
|
||||
$client_id = intval($row['software_client_id']);
|
||||
$until = $mode === 'date' ? $new_date : softwareRenewalExtended($row['software_expire'], $term);
|
||||
$reference_sql = $reference !== '' ? ", software_purchase_reference = '" . escapeSql(mb_substr($reference, 0, 200)) . "'" : '';
|
||||
|
||||
mysqli_query($mysqli, "UPDATE software SET software_expire = '$until' $reference_sql WHERE software_id = $software_id");
|
||||
|
||||
$was = $row['software_expire'] ?: 'no date';
|
||||
logAudit("Software", "Renew", "$session_name renewed license " . escapeSql($row['software_name']) . " until $until (was $was)" . ($reference !== '' ? ', reference ' . escapeSql($reference) : ''), $client_id, $software_id);
|
||||
$renewed[] = [$row['software_name'], $until];
|
||||
}
|
||||
|
||||
if (count($renewed) === 1) {
|
||||
flashAlert("License <strong>{$renewed[0][0]}</strong> renewed until <strong>{$renewed[0][1]}</strong>");
|
||||
} else {
|
||||
$purchase = "'" . $purchase . "'";
|
||||
flashAlert("Renewed <strong>" . count($renewed) . "</strong> licenses");
|
||||
}
|
||||
$expire = escapeSql($_POST['expire']);
|
||||
if (empty($expire)) {
|
||||
$expire = "NULL";
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['add_software_keys'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$row = softwareForRequest($_POST['software_id'] ?? 0);
|
||||
$software_id = intval($row['software_id']);
|
||||
|
||||
$added = softwareAddKeys($software_id, $_POST['keys'] ?? '');
|
||||
|
||||
if ($added) {
|
||||
logAudit("Software", "Keys", "$session_name added $added key" . ($added === 1 ? '' : 's') . " to license " . escapeSql($row['software_name']), $client_id, $software_id);
|
||||
flashAlert("Added <strong>$added</strong> key" . ($added === 1 ? '' : 's') . " to <strong>{$row['software_name']}</strong>");
|
||||
} else {
|
||||
$expire = "'" . $expire . "'";
|
||||
}
|
||||
$notes = escapeSql($_POST['notes']);
|
||||
$vendor = intval($_POST['vendor'] ?? 0);
|
||||
|
||||
$client_id = intval(getFieldById('software', $software_id, 'software_client_id'));
|
||||
|
||||
enforceClientAccess();
|
||||
|
||||
mysqli_query($mysqli,"UPDATE software SET software_name = '$name', software_version = '$version', software_description = '$description', software_type = '$type', software_key = '$key', software_license_type = '$license_type', software_seats = $seats, software_purchase_reference = '$purchase_reference', software_purchase = $purchase, software_expire = $expire, software_notes = '$notes', software_vendor_id = $vendor WHERE software_id = $software_id");
|
||||
|
||||
|
||||
// Update Asset Licenses
|
||||
mysqli_query($mysqli,"DELETE FROM software_assets WHERE software_id = $software_id");
|
||||
if (isset($_POST['assets'])) {
|
||||
foreach($_POST['assets'] as $asset) {
|
||||
$asset = intval($asset);
|
||||
mysqli_query($mysqli,"INSERT INTO software_assets SET software_id = $software_id, asset_id = $asset");
|
||||
}
|
||||
flashAlert("No new keys to add - blank, or <strong>{$row['software_name']}</strong> already has them.", 'warning');
|
||||
}
|
||||
|
||||
// Update Contact Licenses
|
||||
mysqli_query($mysqli,"DELETE FROM software_contacts WHERE software_id = $software_id");
|
||||
if (isset($_POST['contacts'])) {
|
||||
foreach($_POST['contacts'] as $contact) {
|
||||
$contact = intval($contact);
|
||||
mysqli_query($mysqli,"INSERT INTO software_contacts SET software_id = $software_id, contact_id = $contact");
|
||||
}
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['edit_software_key'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$software_key_id = intval($_POST['software_key_id'] ?? 0);
|
||||
$key_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT software_key, software_key_software_id FROM software_keys WHERE software_key_id = $software_key_id"));
|
||||
if (!$key_row) {
|
||||
flashAlert('That key no longer exists.', 'error');
|
||||
redirect();
|
||||
}
|
||||
$row = softwareForRequest($key_row['software_key_software_id']);
|
||||
$software_id = intval($row['software_id']);
|
||||
|
||||
$key = mb_substr(trim((string) ($_POST['key'] ?? '')), 0, 400);
|
||||
if ($key === '') {
|
||||
flashAlert('A key cannot be blank - delete it instead.', 'error');
|
||||
redirect();
|
||||
}
|
||||
mysqli_query($mysqli, "UPDATE software_keys SET software_key = '" . escapeSql($key) . "' WHERE software_key_id = $software_key_id");
|
||||
|
||||
// Who it is handed to - only the client's own, and each one gets a seat if they had none
|
||||
$assets = softwareClientIds('asset', $_POST['assets'] ?? [], $client_id);
|
||||
$contacts = softwareClientIds('contact', $_POST['contacts'] ?? [], $client_id);
|
||||
mysqli_query($mysqli, "DELETE FROM software_key_asset_assignments WHERE software_key_id = $software_key_id");
|
||||
mysqli_query($mysqli, "DELETE FROM software_key_contact_assignments WHERE software_key_id = $software_key_id");
|
||||
$holders = [];
|
||||
foreach ($assets as $asset_id) {
|
||||
mysqli_query($mysqli, "INSERT IGNORE INTO software_key_asset_assignments SET software_key_id = $software_key_id, asset_id = $asset_id");
|
||||
mysqli_query($mysqli, "INSERT IGNORE INTO software_assets SET software_id = $software_id, asset_id = $asset_id");
|
||||
$holders[] = getFieldById('assets', $asset_id, 'asset_name');
|
||||
}
|
||||
foreach ($contacts as $contact_id) {
|
||||
mysqli_query($mysqli, "INSERT IGNORE INTO software_key_contact_assignments SET software_key_id = $software_key_id, contact_id = $contact_id");
|
||||
mysqli_query($mysqli, "INSERT IGNORE INTO software_contacts SET software_id = $software_id, contact_id = $contact_id");
|
||||
$holders[] = getFieldById('contacts', $contact_id, 'contact_name');
|
||||
}
|
||||
|
||||
logAudit("Software", "Edit", "$session_name edited software $name", $client_id, $software_id);
|
||||
logAudit("Software", "Keys", "$session_name updated a key on license " . escapeSql($row['software_name']) . ($holders ? ', handed to ' . escapeSql(implode(', ', $holders)) : ', not handed to anyone'), $client_id, $software_id);
|
||||
|
||||
flashAlert("Software <strong>$name</strong> updated");
|
||||
$warning = softwareSeatWarning($software_id);
|
||||
flashAlert("Key on <strong>{$row['software_name']}</strong> saved" . ($holders ? ' - handed to <strong>' . implode(', ', $holders) . '</strong>' : '') . $warning, $warning ? 'warning' : 'success');
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_GET['delete_software_key'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$software_key_id = intval($_GET['delete_software_key']);
|
||||
$key_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT software_key_software_id FROM software_keys WHERE software_key_id = $software_key_id"));
|
||||
if (!$key_row) {
|
||||
flashAlert('That key no longer exists.', 'error');
|
||||
redirect();
|
||||
}
|
||||
$row = softwareForRequest($key_row['software_key_software_id']);
|
||||
|
||||
mysqli_query($mysqli, "DELETE FROM software_keys WHERE software_key_id = $software_key_id");
|
||||
|
||||
logAudit("Software", "Keys", "$session_name deleted a key from license " . escapeSql($row['software_name']), $client_id, intval($row['software_id']));
|
||||
|
||||
flashAlert("Key deleted from <strong>{$row['software_name']}</strong>", 'error');
|
||||
|
||||
redirect();
|
||||
|
||||
@@ -176,51 +318,35 @@ if (isset($_GET['archive_software'])) {
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$software_id = intval($_GET['archive_software']);
|
||||
$row = softwareForRequest($_GET['archive_software']);
|
||||
$software_id = intval($row['software_id']);
|
||||
|
||||
// Get Software Name and Client ID for logging and alert message
|
||||
$sql = mysqli_query($mysqli,"SELECT software_name, software_client_id FROM software WHERE software_id = $software_id");
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$software_name = escapeSql($row['software_name']);
|
||||
$client_id = intval($row['software_client_id']);
|
||||
// Seats stay put - an archived license drops off every device and user
|
||||
// anyway, and comes back with them if it is restored
|
||||
mysqli_query($mysqli, "UPDATE software SET software_archived_at = NOW() WHERE software_id = $software_id");
|
||||
|
||||
enforceClientAccess();
|
||||
logAudit("Software", "Archive", "$session_name archived license " . escapeSql($row['software_name']), $client_id, $software_id);
|
||||
|
||||
mysqli_query($mysqli,"UPDATE software SET software_archived_at = NOW() WHERE software_id = $software_id");
|
||||
|
||||
// Remove Software Relations
|
||||
mysqli_query($mysqli,"DELETE FROM software_contacts WHERE software_id = $software_id");
|
||||
mysqli_query($mysqli,"DELETE FROM software_assets WHERE software_id = $software_id");
|
||||
|
||||
logAudit("Software", "Archive", "$session_name archived software $software_name and removed all device/user license associations", $client_id, $software_id);
|
||||
|
||||
flashAlert("Software <strong>$software_name</strong> archived and removed all device/user license associations", 'error');
|
||||
flashAlert("License <strong>{$row['software_name']}</strong> archived", 'error');
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if(isset($_GET['restore_software'])){
|
||||
if (isset($_GET['restore_software'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$software_id = intval($_GET['restore_software']);
|
||||
$row = softwareForRequest($_GET['restore_software']);
|
||||
$software_id = intval($row['software_id']);
|
||||
|
||||
// Get Software Name and Client ID for logging and alert message
|
||||
$sql = mysqli_query($mysqli,"SELECT software_name, software_client_id FROM software WHERE software_id = $software_id");
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$software_name = escapeSql($row['software_name']);
|
||||
$client_id = intval($row['software_client_id']);
|
||||
mysqli_query($mysqli, "UPDATE software SET software_archived_at = NULL WHERE software_id = $software_id");
|
||||
|
||||
enforceClientAccess();
|
||||
logAudit("Software", "Restore", "$session_name restored license " . escapeSql($row['software_name']), $client_id, $software_id);
|
||||
|
||||
mysqli_query($mysqli,"UPDATE software SET software_archived_at = NULL WHERE software_id = $software_id");
|
||||
|
||||
logAudit("Software", "Restore", "$session_name restored software $software_name", $client_id, $software_id);
|
||||
|
||||
flashAlert("Software <strong>$software_name</strong> restored");
|
||||
flashAlert("License <strong>{$row['software_name']}</strong> restored");
|
||||
|
||||
redirect();
|
||||
|
||||
@@ -232,21 +358,76 @@ if (isset($_GET['delete_software'])) {
|
||||
|
||||
enforceUserPermission('module_support', 3);
|
||||
|
||||
$software_id = intval($_GET['delete_software']);
|
||||
$row = softwareForRequest($_GET['delete_software']);
|
||||
$software_id = intval($row['software_id']);
|
||||
|
||||
// Get Software Name and Client ID for logging and alert message
|
||||
$sql = mysqli_query($mysqli,"SELECT software_name, software_client_id FROM software WHERE software_id = $software_id");
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$software_name = escapeSql($row['software_name']);
|
||||
$client_id = intval($row['software_client_id']);
|
||||
// Seats, keys and links go with it (foreign keys cascade)
|
||||
mysqli_query($mysqli, "DELETE FROM software WHERE software_id = $software_id");
|
||||
|
||||
enforceClientAccess();
|
||||
logAudit("Software", "Delete", "$session_name deleted license " . escapeSql($row['software_name']) . " with its seats and keys", $client_id);
|
||||
|
||||
mysqli_query($mysqli,"DELETE FROM software WHERE software_id = $software_id");
|
||||
flashAlert("License <strong>{$row['software_name']}</strong> deleted", 'error');
|
||||
|
||||
logAudit("Software", "Delete", "$session_name deleted software $software_name and removed all device/user license associations", $client_id);
|
||||
// From its own page, back to the client's licenses - that page is gone
|
||||
if (strpos($_SERVER['HTTP_REFERER'] ?? '', '/license.php') !== false) {
|
||||
redirect("software.php?client_id=$client_id&archived=1");
|
||||
}
|
||||
redirect();
|
||||
|
||||
flashAlert("Software <strong>$software_name</strong> deleted and removed all device/user license associations", 'error');
|
||||
}
|
||||
|
||||
if (isset($_POST['bulk_archive_software'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$rows = softwareBulkRows($_POST['software_ids'] ?? []);
|
||||
foreach ($rows as $software_id => $row) {
|
||||
$client_id = intval($row['software_client_id']);
|
||||
mysqli_query($mysqli, "UPDATE software SET software_archived_at = NOW() WHERE software_id = $software_id AND software_archived_at IS NULL");
|
||||
logAudit("Software", "Archive", "$session_name archived license " . escapeSql($row['software_name']), $client_id, $software_id);
|
||||
}
|
||||
|
||||
flashAlert("Archived <strong>" . count($rows) . "</strong> license" . (count($rows) === 1 ? '' : 's'), 'error');
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['bulk_restore_software'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_support', 2);
|
||||
|
||||
$rows = softwareBulkRows($_POST['software_ids'] ?? []);
|
||||
foreach ($rows as $software_id => $row) {
|
||||
$client_id = intval($row['software_client_id']);
|
||||
mysqli_query($mysqli, "UPDATE software SET software_archived_at = NULL WHERE software_id = $software_id");
|
||||
logAudit("Software", "Restore", "$session_name restored license " . escapeSql($row['software_name']), $client_id, $software_id);
|
||||
}
|
||||
|
||||
flashAlert("Restored <strong>" . count($rows) . "</strong> license" . (count($rows) === 1 ? '' : 's'));
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['bulk_delete_software'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceUserPermission('module_support', 3);
|
||||
|
||||
$rows = softwareBulkRows($_POST['software_ids'] ?? []);
|
||||
foreach ($rows as $software_id => $row) {
|
||||
$client_id = intval($row['software_client_id']);
|
||||
mysqli_query($mysqli, "DELETE FROM software WHERE software_id = $software_id");
|
||||
logAudit("Software", "Delete", "$session_name deleted license " . escapeSql($row['software_name']) . " with its seats and keys", $client_id);
|
||||
}
|
||||
|
||||
flashAlert("Deleted <strong>" . count($rows) . "</strong> license" . (count($rows) === 1 ? '' : 's'), 'error');
|
||||
|
||||
redirect();
|
||||
|
||||
@@ -310,24 +491,52 @@ if (isExportRequest('export_software')) {
|
||||
$expire_query = '';
|
||||
}
|
||||
|
||||
// Attention tab
|
||||
$status_filters = softwareStatusFilters();
|
||||
$status = (string) ($_POST['status'] ?? '');
|
||||
$status_query = '';
|
||||
if (isset($status_filters[$status])) {
|
||||
$status_query = "AND ({$status_filters[$status]['sql']})";
|
||||
$filter_summary['Showing'] = $status_filters[$status]['label'];
|
||||
}
|
||||
|
||||
// License type
|
||||
$license_type_query = '';
|
||||
if (!empty($_POST['license_type'])) {
|
||||
$license_type_query = "AND software_license_type = '" . escapeSql($_POST['license_type']) . "'";
|
||||
$filter_summary['License Type'] = $_POST['license_type'];
|
||||
}
|
||||
|
||||
// Search Filter
|
||||
$q = escapeSql($_POST['q'] ?? '');
|
||||
if (!empty($q)) {
|
||||
$filter_summary['Search'] = $_POST['q'];
|
||||
}
|
||||
|
||||
// Who is on each license, in the same query - not two more queries per row
|
||||
mysqli_query($mysqli, "SET SESSION group_concat_max_len = 65535");
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT software_description, software_expire, software_id, software_key, software_license_type,
|
||||
software_name, software_notes, software_purchase, software_seats, software_type,
|
||||
software_version FROM software
|
||||
software_name, software_notes, software_purchase, software_purchase_reference, software_seats, software_type,
|
||||
software_version, client_name, vendor_name,
|
||||
" . softwareSeatsUsedSql('software.software_id') . " AS seats_used,
|
||||
(SELECT GROUP_CONCAT(asset_name ORDER BY asset_name SEPARATOR ', ') FROM software_assets AS sa
|
||||
JOIN assets ON assets.asset_id = sa.asset_id AND asset_archived_at IS NULL WHERE sa.software_id = software.software_id) AS assigned_to_assets,
|
||||
(SELECT GROUP_CONCAT(contact_name ORDER BY contact_name SEPARATOR ', ') FROM software_contacts AS sc
|
||||
JOIN contacts ON contacts.contact_id = sc.contact_id AND contact_archived_at IS NULL WHERE sc.software_id = software.software_id) AS assigned_to_contacts,
|
||||
(SELECT GROUP_CONCAT(sk.software_key ORDER BY sk.software_key_id SEPARATOR ', ') FROM software_keys AS sk WHERE sk.software_key_software_id = software.software_id) AS software_keys_list
|
||||
FROM software
|
||||
LEFT JOIN clients ON client_id = software_client_id
|
||||
LEFT JOIN vendors ON vendor_id = software_vendor_id
|
||||
WHERE (software_name LIKE '%$q%' OR software_type LIKE '%$q%' OR software_key LIKE '%$q%' OR client_name LIKE '%$q%')
|
||||
WHERE (software_name LIKE '%$q%' OR software_type LIKE '%$q%' OR software_key LIKE '%$q%' OR client_name LIKE '%$q%' OR vendor_name LIKE '%$q%'
|
||||
OR software.software_id IN (SELECT software_key_software_id FROM software_keys WHERE software_key LIKE '%$q%'))
|
||||
AND $archive_query
|
||||
" . clientScopeSql('software_client_id') . "
|
||||
$client_query
|
||||
$expire_query
|
||||
$status_query
|
||||
$license_type_query
|
||||
ORDER BY software_name ASC"
|
||||
);
|
||||
|
||||
@@ -337,35 +546,19 @@ if (isExportRequest('export_software')) {
|
||||
|
||||
guardExportPdfRowCount($format, $num_rows);
|
||||
|
||||
$export = beginExport('software', $format, $file_name_prepend . 'Software', 'Software', summarizeExportFilters($filter_summary));
|
||||
$export = beginExport('software', $format, $file_name_prepend . 'Licenses', 'Licenses', summarizeExportFilters($filter_summary));
|
||||
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
// Asset and contact licence lists, only when those columns are wanted
|
||||
if (isset($export['columns']['assigned_to_assets'])) {
|
||||
$software_id = intval($row['software_id']);
|
||||
$assigned_to_assets = [];
|
||||
$asset_licenses_sql = mysqli_query($mysqli, "SELECT asset_name FROM software_assets LEFT JOIN assets ON software_assets.asset_id = assets.asset_id WHERE software_id = $software_id");
|
||||
while ($asset_row = mysqli_fetch_assoc($asset_licenses_sql)) {
|
||||
$assigned_to_assets[] = $asset_row['asset_name'];
|
||||
}
|
||||
$row['assigned_to_assets'] = implode(', ', $assigned_to_assets);
|
||||
}
|
||||
if (isset($export['columns']['assigned_to_contacts'])) {
|
||||
$software_id = intval($row['software_id']);
|
||||
$assigned_to_contacts = [];
|
||||
$contact_licenses_sql = mysqli_query($mysqli, "SELECT contact_name FROM software_contacts LEFT JOIN contacts ON software_contacts.contact_id = contacts.contact_id WHERE software_id = $software_id");
|
||||
while ($contact_row = mysqli_fetch_assoc($contact_licenses_sql)) {
|
||||
$assigned_to_contacts[] = $contact_row['contact_name'];
|
||||
}
|
||||
$row['assigned_to_contacts'] = implode(', ', $assigned_to_contacts);
|
||||
}
|
||||
$row['assigned_to_assets'] = (string) $row['assigned_to_assets'];
|
||||
$row['assigned_to_contacts'] = (string) $row['assigned_to_contacts'];
|
||||
$row['software_keys_list'] = (string) $row['software_keys_list'];
|
||||
addExportRow($export, $row);
|
||||
}
|
||||
|
||||
finishExport($export);
|
||||
}
|
||||
|
||||
logAudit("Software", "Export", "$session_name exported $num_rows software(s) to a " . strtoupper($format) . " file", $client_id);
|
||||
logAudit("Software", "Export", "$session_name exported $num_rows license(s) to a " . strtoupper($format) . " file", $client_id);
|
||||
|
||||
exit;
|
||||
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Licenses - one client's, or every license the agent can see from All
|
||||
* Client Docs. Built around the two things an MSP watches: renewals (what is
|
||||
* coming up, what has lapsed) and seats (bought against in use - over means a
|
||||
* true-up, none in use means paying for nothing). Tabs along the top pull
|
||||
* those out; each row shows its seats as a bar and its renewal in colour.
|
||||
* Names open the license page.
|
||||
*/
|
||||
|
||||
// Default Column Sortby Filter
|
||||
$sort = "software_name";
|
||||
$order = "ASC";
|
||||
@@ -34,8 +43,31 @@ if (isset($_GET['client_id'])) {
|
||||
// Perms
|
||||
enforceUserPermission('module_support');
|
||||
|
||||
// Expiring In Filter
|
||||
if (isset($_GET['expire_days']) && !empty($_GET['expire_days'])) {
|
||||
$can_edit = lookupUserPermission('module_support') >= 2;
|
||||
// Deleting for good also needs Admin > Settings to allow destructive deletes
|
||||
$can_delete = lookupUserPermission('module_support') >= 3 && !empty($config_destructive_deletes_enable);
|
||||
|
||||
$client = '';
|
||||
if (!$client_url) {
|
||||
// Client Filter
|
||||
if (!empty($_GET['client'])) {
|
||||
$client = intval($_GET['client']);
|
||||
$client_query = "AND software_client_id = $client";
|
||||
}
|
||||
}
|
||||
|
||||
// Attention tabs - renewing, expired, over seats, nobody on it
|
||||
$status_filters = softwareStatusFilters();
|
||||
$status = isset($status_filters[$_GET['status'] ?? '']) ? $_GET['status'] : '';
|
||||
$_GET['status'] = $status;
|
||||
$status_query = $status ? "AND ({$status_filters[$status]['sql']})" : '';
|
||||
|
||||
// License type filter
|
||||
$license_type_filter = (string) ($_GET['license_type'] ?? '');
|
||||
$license_type_query = $license_type_filter !== '' ? "AND software_license_type = '" . escapeSql($license_type_filter) . "'" : '';
|
||||
|
||||
// Expiring In Filter - the dashboard links here with expire_days=30
|
||||
if (!empty($_GET['expire_days'])) {
|
||||
if ($_GET['expire_days'] == "expired") {
|
||||
$expire_days = "expired";
|
||||
$expire_query = "AND (software_expire IS NOT NULL AND software_expire != '0000-00-00' AND software_expire < CURDATE())";
|
||||
@@ -49,177 +81,296 @@ if (isset($_GET['expire_days']) && !empty($_GET['expire_days'])) {
|
||||
$expire_query = '';
|
||||
}
|
||||
|
||||
if (!$client_url) {
|
||||
// Client Filter
|
||||
if (isset($_GET['client']) & !empty($_GET['client'])) {
|
||||
$client_query = 'AND (software_client_id = ' . intval($_GET['client']) . ')';
|
||||
$client = intval($_GET['client']);
|
||||
} else {
|
||||
// Default - any
|
||||
$client_query = '';
|
||||
$client = '';
|
||||
}
|
||||
// Extra columns, picked from the filter bar
|
||||
$extra_column_options = [
|
||||
'Key' => 'License key',
|
||||
'Purchased' => 'Purchased',
|
||||
'Order_Ref' => 'Order reference',
|
||||
];
|
||||
$extra_columns = array_values(array_intersect(array_keys($extra_column_options), (array) ($_GET['show_column'] ?? [])));
|
||||
$show_key = in_array('Key', $extra_columns, true);
|
||||
$show_purchased = in_array('Purchased', $extra_columns, true);
|
||||
$show_order_ref = in_array('Order_Ref', $extra_columns, true);
|
||||
|
||||
// Only these sort - filter_header.php will hand through any lowercase word, and
|
||||
// an unknown column takes the page down with a mysqli exception
|
||||
$sortable_columns = [
|
||||
'software_name', 'software_type', 'software_license_type', 'seats_used', 'software_seats', 'software_expire',
|
||||
'software_purchase', 'software_purchase_reference', 'vendor_name', 'client_name', 'software_created_at'
|
||||
];
|
||||
if (!in_array($sort, $sortable_columns, true)) {
|
||||
$sort = 'software_name';
|
||||
}
|
||||
// No renewal date sorts last whichever way, so "soonest first" starts with a date
|
||||
$order_by = $sort === 'software_expire' ? "software_expire IS NULL, software_expire $order, software_name ASC" : "$sort $order, software_name ASC";
|
||||
|
||||
$seats_used_sql = softwareSeatsUsedSql('software.software_id');
|
||||
|
||||
// Tab counts - within the client and archive scope, before the search narrows it
|
||||
$count_columns = ["COUNT(*) AS all_count"];
|
||||
foreach ($status_filters as $status_key => $status_filter) {
|
||||
$count_columns[] = "COALESCE(SUM({$status_filter['sql']}), 0) AS {$status_key}_count";
|
||||
}
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "
|
||||
SELECT " . implode(', ', $count_columns) . "
|
||||
FROM software
|
||||
LEFT JOIN clients ON client_id = software_client_id
|
||||
WHERE $archive_query
|
||||
" . clientScopeSql('software_client_id') . "
|
||||
$client_query
|
||||
"));
|
||||
$tab_counts = ['' => intval($row['all_count'])];
|
||||
foreach (array_keys($status_filters) as $status_key) {
|
||||
$tab_counts[$status_key] = intval($row["{$status_key}_count"]);
|
||||
}
|
||||
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT SQL_CALC_FOUND_ROWS client_id, client_name, software_created_at, software_description, software_expire,
|
||||
software_id, software_license_type, software_name, software_seats, software_type,
|
||||
software_version, software_archived_at, vendor_id, vendor_name FROM software
|
||||
software_id, software_license_type, software_name, software_seats, software_type, software_key, software_favorite,
|
||||
software_purchase, software_purchase_reference, software_version, software_archived_at, vendor_id, vendor_name,
|
||||
$seats_used_sql AS seats_used,
|
||||
(SELECT COUNT(*) FROM software_keys WHERE software_key_software_id = software.software_id) AS key_count
|
||||
FROM software
|
||||
LEFT JOIN clients ON client_id = software_client_id
|
||||
LEFT JOIN vendors ON vendor_id = software_vendor_id
|
||||
WHERE (software_name LIKE '%$q%' OR software_type LIKE '%$q%' OR software_key LIKE '%$q%' OR client_name LIKE '%$q%')
|
||||
WHERE (software_name LIKE '%$q%' OR software_description LIKE '%$q%' OR software_type LIKE '%$q%' OR software_license_type LIKE '%$q%'
|
||||
OR software_key LIKE '%$q%' OR software_purchase_reference LIKE '%$q%' OR vendor_name LIKE '%$q%' OR client_name LIKE '%$q%'
|
||||
OR software.software_id IN (SELECT software_key_software_id FROM software_keys WHERE software_key LIKE '%$q%'))
|
||||
AND $archive_query
|
||||
" . clientScopeSql('software_client_id') . "
|
||||
$client_query
|
||||
$status_query
|
||||
$license_type_query
|
||||
$expire_query
|
||||
ORDER BY $sort $order LIMIT $record_from, $record_to");
|
||||
ORDER BY $order_by LIMIT $record_from, $record_to");
|
||||
|
||||
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
|
||||
// License types in use here, for the filter
|
||||
$license_type_options = [];
|
||||
$sql_license_types = mysqli_query($mysqli, "SELECT DISTINCT software_license_type FROM software
|
||||
LEFT JOIN clients ON client_id = software_client_id
|
||||
WHERE software_license_type IS NOT NULL AND software_license_type != '' AND $archive_query
|
||||
" . clientScopeSql('software_client_id') . " $client_query
|
||||
ORDER BY software_license_type ASC");
|
||||
while ($license_type_row = mysqli_fetch_row($sql_license_types)) {
|
||||
$license_type_options[] = $license_type_row[0];
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header bg-dark py-2">
|
||||
<h3 class="card-title mt-2"><i class="fas fa-fw fa-cube me-2"></i>Software & Licenses</h3>
|
||||
<div class="card-tools">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/software/software_add.php?<?= $client_url ?>">
|
||||
<i class="fas fa-plus me-2"></i>New License
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown"></button>
|
||||
<div class="dropdown-menu">
|
||||
<div class="card">
|
||||
<div class="card-header bg-dark py-2">
|
||||
<h3 class="card-title mt-2"><i class="fas fa-fw fa-cube me-2"></i>Licenses</h3>
|
||||
<div class="card-tools">
|
||||
<?php if ($can_edit) { ?>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/software/software_add.php?<?= $client_url ?>">
|
||||
<i class="fas fa-plus me-2"></i>New License
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown"></button>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<a class="dropdown-item text-dark ajax-modal" href="#" data-modal-size="lg"
|
||||
data-modal-url="modals/software/software_add_from_template.php?<?= $client_url ?>">
|
||||
<i class="fas fa-fw fa-puzzle-piece me-2"></i>New from Template
|
||||
</a>
|
||||
<?php if ($num_rows[0] > 0) { ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-dark ajax-modal" href="#"
|
||||
data-modal-url="modals/software/software_add_from_template.php?<?= $client_url ?>">
|
||||
<i class="fas fa-fw fa-puzzle-piece me-2"></i>Create from Template
|
||||
data-modal-url="<?= buildExportModalUrl('modals/software/software_export.php', ['client_id', 'client', 'status', 'license_type', 'expire_days', 'archived', 'q']) ?>">
|
||||
<i class="fa fa-fw fa-download me-2"></i>Export
|
||||
</a>
|
||||
<?php if ($num_rows[0] > 0) { ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-dark ajax-modal" href="#"
|
||||
data-modal-url="<?= buildExportModalUrl('modals/software/software_export.php', ['client_id', 'client', 'expire_days', 'archived', 'q']) ?>">
|
||||
<i class="fa fa-fw fa-download me-2"></i>Export
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($tab_counts['']) { ?>
|
||||
<!-- What needs a look -->
|
||||
<div class="card-header py-2 itflow-asset-tabs">
|
||||
<nav class="nav nav-pills flex-nowrap overflow-auto">
|
||||
<a class="nav-link text-nowrap <?= $status === '' ? 'active' : '' ?>" href="?<?= $url_query_strings_sort ?>&status=">
|
||||
All <span class="badge rounded-pill ms-1"><?= $tab_counts[''] ?></span>
|
||||
</a>
|
||||
<?php foreach ($status_filters as $status_key => $status_filter) {
|
||||
if (!$tab_counts[$status_key] && $status !== $status_key) {
|
||||
continue;
|
||||
}
|
||||
$tab_tone = ['expired' => 'danger', 'over' => 'danger', 'renewing' => 'warning'][$status_key] ?? ''; ?>
|
||||
<a class="nav-link text-nowrap <?= $status === $status_key ? 'active' : '' ?>" href="?<?= $url_query_strings_sort ?>&status=<?= $status_key ?>" title="<?= $status_filter['title'] ?>">
|
||||
<i class="fas fa-fw <?= $status_filter['icon'] ?> me-1 <?= $tab_tone && $status !== $status_key ? "text-$tab_tone" : '' ?>"></i><?= $status_filter['label'] ?> <span class="badge rounded-pill ms-1"><?= $tab_counts[$status_key] ?></span>
|
||||
</a>
|
||||
<?php } ?>
|
||||
</nav>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="card-header py-3">
|
||||
<form autocomplete="off">
|
||||
<?php if ($client_url) { ?>
|
||||
<input type="hidden" name="client_id" value="<?= $client_id ?>">
|
||||
<?php } ?>
|
||||
<input type="hidden" name="status" value="<?= $status ?>">
|
||||
<input type="hidden" name="archived" value="<?= $archived ?>">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="input-group">
|
||||
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search licenses, keys, vendors...">
|
||||
<button class="btn btn-dark" title="Search"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!$client_url) { ?>
|
||||
<div class="col-lg-2 col-md-6">
|
||||
<select class="form-select select2" name="client" onchange="this.form.submit()">
|
||||
<option value="" <?php if ($client == "") { echo "selected"; } ?>>- All Clients -</option>
|
||||
<?php
|
||||
$sql_clients_filter = mysqli_query($mysqli, "
|
||||
SELECT DISTINCT client_id, client_name, client_abbreviation
|
||||
FROM clients
|
||||
JOIN software ON software_client_id = client_id
|
||||
WHERE $archive_query
|
||||
" . clientScopeSql('clients.client_id') . "
|
||||
ORDER BY client_name ASC
|
||||
");
|
||||
while ($row = mysqli_fetch_assoc($sql_clients_filter)) {
|
||||
$client_id_filter = intval($row['client_id']);
|
||||
$client_name_filter = escapeHtml($row['client_name']);
|
||||
?>
|
||||
<option <?php if ($client == $client_id_filter) { echo "selected"; } ?> value="<?= $client_id_filter ?>"<?= selectOptionClient($row['client_id'], $row['client_name'], $row['client_abbreviation'] ?? '') ?>><?= $client_name_filter ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<div class="col-lg-2 col-md-4">
|
||||
<select class="form-select select2" name="license_type" onchange="this.form.submit()">
|
||||
<option value="">- License Type -</option>
|
||||
<?php foreach ($license_type_options as $license_type_option) { ?>
|
||||
<option <?= $license_type_option === $license_type_filter ? 'selected' : '' ?><?= selectOptionData(['icon' => 'fas ' . softwareLicenseTypeIcon($license_type_option)]) ?>><?= escapeHtml($license_type_option) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-4">
|
||||
<select class="form-select select2" name="expire_days" onchange="this.form.submit()">
|
||||
<option value="" <?php if ($expire_days == "") { echo "selected"; } ?>>- Renewal -</option>
|
||||
<option value="expired" <?php if ($expire_days === "expired") { echo "selected"; } ?>>Expired</option>
|
||||
<?php foreach ([7, 30, 45, 60, 90] as $expire_option) { ?>
|
||||
<option value="<?= $expire_option ?>" <?php if ($expire_days === $expire_option) { echo "selected"; } ?>>Renews in <?= $expire_option ?> days</option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-4">
|
||||
<select onchange="this.form.submit()" class="form-select select2" name="show_column[]" data-placeholder="- More columns -" multiple>
|
||||
<?php foreach ($extra_column_options as $column_key => $column_label) { ?>
|
||||
<option value="<?= $column_key ?>" <?php if (in_array($column_key, $extra_columns, true)) { echo 'selected'; } ?>><?= $column_label ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-1 col-md-12 <?= $client_url ? 'col-lg-3' : '' ?>">
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<a href="?<?= $client_url ?>archived=<?= $archived ? 0 : 1 ?>"
|
||||
class="btn <?= $archived ? 'btn-dark' : 'btn-outline-dark' ?>" title="<?= $archived ? 'Back to active licenses' : 'Show archived licenses' ?>">
|
||||
<i class="fa fa-fw fa-archive"></i><span class="d-lg-none ms-2">Archived</span>
|
||||
</a>
|
||||
<?php if ($can_edit) { ?>
|
||||
<div class="dropdown" id="bulkActionButton" hidden>
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-fw fa-layer-group me-2"></i>Bulk Action (<span id="selectedCount"></span>)
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<?php if (!$archived) { ?>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/software/software_renew.php" data-modal-size="lg" data-bulk="true">
|
||||
<i class="fas fa-fw fa-redo-alt me-2"></i>Renew
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-item text-danger confirm-link" type="submit" form="bulkActions" name="bulk_archive_software">
|
||||
<i class="fas fa-fw fa-archive me-2"></i>Archive
|
||||
</button>
|
||||
<?php } else { ?>
|
||||
<button class="dropdown-item text-info" type="submit" form="bulkActions" name="bulk_restore_software">
|
||||
<i class="fas fa-fw fa-redo me-2"></i>Restore
|
||||
</button>
|
||||
<?php if ($can_delete) { ?>
|
||||
<button class="dropdown-item text-danger text-bold confirm-link" type="submit" form="bulkActions" name="bulk_delete_software"
|
||||
data-confirm-text="The licenses go for good, with their seats and keys.">
|
||||
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
||||
</button>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-header py-3">
|
||||
<form autocomplete="off">
|
||||
<?php if($client_url) { ?>
|
||||
<input type="hidden" name="client_id" value="<?= $client_id ?>">
|
||||
<?php } ?>
|
||||
<input type="hidden" name="archived" value="<?= $archived ?>">
|
||||
<div class="row g-2 align-items-center">
|
||||
</form>
|
||||
</div>
|
||||
<form id="bulkActions" action="post.php" method="post">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search Licenses">
|
||||
<button class="btn btn-dark" title="Search"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!$client_url) { ?>
|
||||
<div class="col-md-2">
|
||||
<div class="input-group">
|
||||
<select class="form-select select2" name="client" onchange="this.form.submit()">
|
||||
<option value="" <?php if ($client == "") { echo "selected"; } ?>>- All Clients -</option>
|
||||
|
||||
<?php
|
||||
$sql_clients_filter = mysqli_query($mysqli, "
|
||||
SELECT DISTINCT client_id, client_name, client_abbreviation
|
||||
FROM clients
|
||||
JOIN software ON software_client_id = client_id
|
||||
WHERE $archive_query
|
||||
" . clientScopeSql('clients.client_id') . "
|
||||
ORDER BY client_name ASC
|
||||
");
|
||||
while ($row = mysqli_fetch_assoc($sql_clients_filter)) {
|
||||
$client_id = intval($row['client_id']);
|
||||
$client_name = escapeHtml($row['client_name']);
|
||||
?>
|
||||
<option <?php if ($client == $client_id) { echo "selected"; } ?> value="<?= $client_id ?>"<?= selectOptionClient($row['client_id'], $row['client_name'], $row['client_abbreviation'] ?? '') ?>><?= $client_name ?></option>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="col-md-2">
|
||||
<div class="input-group">
|
||||
<select class="form-select select2" name="expire_days" onchange="this.form.submit()">
|
||||
<option value="" <?php if ($expire_days == "") { echo "selected"; } ?>>- Expiring In -</option>
|
||||
<option value="expired" <?php if ($expire_days === "expired") { echo "selected"; } ?>>Expired</option>
|
||||
<option value="7" <?php if ($expire_days === 7) { echo "selected"; } ?>>7 Days</option>
|
||||
<option value="30" <?php if ($expire_days === 30) { echo "selected"; } ?>>30 Days</option>
|
||||
<option value="45" <?php if ($expire_days === 45) { echo "selected"; } ?>>45 Days</option>
|
||||
<option value="60" <?php if ($expire_days === 60) { echo "selected"; } ?>>60 Days</option>
|
||||
<option value="90" <?php if ($expire_days === 90) { echo "selected"; } ?>>90 Days</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($client_url) { // filler ?>
|
||||
<div class="col-md-2"></div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="float-end">
|
||||
<a href="?<?= $client_url ?>archived=<?php if($archived == 1){ echo 0; } else { echo 1; } ?>"
|
||||
class="btn btn-<?php if($archived == 1){ echo"primary"; } else { echo "default"; } ?>">
|
||||
<i class="fa fa-fw fa-archive me-2"></i>Archived
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-borderless table-hover mb-0">
|
||||
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?> text-nowrap">
|
||||
<table class="table table-hover align-middle mb-0 itflow-asset-table">
|
||||
<thead class="<?php if (!$num_rows[0]) { echo "d-none"; } ?> text-nowrap table-light">
|
||||
<tr>
|
||||
<th class="ps-3">
|
||||
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=software_name&order=<?= $disp ?>">
|
||||
Software <?php if ($sort == 'software_name') { echo $order_icon; } ?>
|
||||
<?php if ($can_edit) { ?>
|
||||
<th class="checkbox-column border-end">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" id="selectAllCheckbox" type="checkbox" onclick="checkAll(this)" title="Select all on this page">
|
||||
</div>
|
||||
</th>
|
||||
<?php } ?>
|
||||
<th>
|
||||
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=software_name&order=<?= $disp ?>">
|
||||
License <?php if ($sort == 'software_name') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=software_type&order=<?= $disp ?>">
|
||||
Type <?php if ($sort == 'software_type') { echo $order_icon; } ?>
|
||||
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=software_license_type&order=<?= $disp ?>">
|
||||
Type <?php if ($sort == 'software_license_type') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=software_license_type&order=<?= $disp ?>">
|
||||
License Type <?php if ($sort == 'software_license_type') { echo $order_icon; } ?>
|
||||
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=seats_used&order=<?= $disp ?>">
|
||||
Seats <?php if ($sort == 'seats_used') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=software_seats&order=<?= $disp ?>">
|
||||
Seats <?php if ($sort == 'software_seats') { echo $order_icon; } ?>
|
||||
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=software_expire&order=<?= $disp ?>">
|
||||
Renews <?php if ($sort == 'software_expire') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<?php if ($show_key) { ?>
|
||||
<th>Key</th>
|
||||
<?php } ?>
|
||||
<?php if ($show_purchased) { ?>
|
||||
<th>
|
||||
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=software_purchase&order=<?= $disp ?>">
|
||||
Purchased <?php if ($sort == 'software_purchase') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<?php } ?>
|
||||
<?php if ($show_order_ref) { ?>
|
||||
<th>
|
||||
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=software_purchase_reference&order=<?= $disp ?>">
|
||||
Order ref <?php if ($sort == 'software_purchase_reference') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<?php } ?>
|
||||
<th>
|
||||
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=software_expire&order=<?= $disp ?>">
|
||||
Expire <?php if ($sort == 'software_expire') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=vendor_name&order=<?= $disp ?>">
|
||||
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=vendor_name&order=<?= $disp ?>">
|
||||
Vendor <?php if ($sort == 'vendor_name') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<?php if (!$client_url) { ?>
|
||||
<th>
|
||||
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=client_name&order=<?= $disp ?>">
|
||||
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=client_name&order=<?= $disp ?>">
|
||||
Client <?php if ($sort == 'client_name') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<?php } ?>
|
||||
<th class="text-center">Action</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -233,113 +384,124 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
$software_description = escapeHtml($row['software_description']);
|
||||
$software_version = escapeHtml($row['software_version']);
|
||||
$software_type = escapeHtml($row['software_type']);
|
||||
$software_license_type = escapeHtml($row['software_license_type']) ?: '-';
|
||||
$software_seats = escapeHtml($row['software_seats']);
|
||||
$software_expire = escapeHtml($row['software_expire']);
|
||||
$software_license_type = escapeHtml($row['software_license_type']);
|
||||
$software_purchase = escapeHtml($row['software_purchase']);
|
||||
$software_purchase_reference = escapeHtml($row['software_purchase_reference']);
|
||||
$software_favorite = intval($row['software_favorite']);
|
||||
$software_archived_at = escapeHtml($row['software_archived_at']);
|
||||
$vendor_name = escapeHtml($row['vendor_name']);
|
||||
$vendor_id = intval($row['vendor_id']);
|
||||
if ($vendor_name) {
|
||||
$vendor_display = "<a class='ajax-modal' href='#' data-modal-url='modals/vendor/vendor.php?id=$vendor_id'>$vendor_name</a>";
|
||||
} else {
|
||||
$vendor_display = "<span class='text-muted'>N/A</span>";
|
||||
}
|
||||
if ($software_expire) {
|
||||
$software_expire_ago = timeAgo($software_expire);
|
||||
$software_expire_display = "<div>$software_expire</div><div><small>$software_expire_ago</small></div>";
|
||||
|
||||
// Convert the expiry date to a timestamp
|
||||
$software_expire_timestamp = strtotime($row['software_expire']);
|
||||
$current_timestamp = time(); // Get current timestamp
|
||||
|
||||
// Calculate the difference in days
|
||||
$days_until_expiry = ($software_expire_timestamp - $current_timestamp) / (60 * 60 * 24);
|
||||
|
||||
// Determine the class based on the number of days until expiry
|
||||
if ($days_until_expiry <= 0) {
|
||||
$tr_class = "table-secondary";
|
||||
} elseif ($days_until_expiry <= 7) {
|
||||
$tr_class = "table-danger";
|
||||
} elseif ($days_until_expiry <= 45) {
|
||||
$tr_class = "table-warning";
|
||||
} else {
|
||||
$tr_class = '';
|
||||
}
|
||||
|
||||
} else {
|
||||
$software_expire_display = "<span class='text-muted'>N/A</span>";
|
||||
$tr_class = '';
|
||||
}
|
||||
|
||||
$software_created_at = escapeHtml($row['software_created_at']);
|
||||
|
||||
$seat_count = 0;
|
||||
|
||||
// Asset Licenses
|
||||
$asset_licenses_sql = mysqli_query($mysqli, "SELECT asset_id FROM software_assets WHERE software_id = $software_id");
|
||||
$asset_licenses_array = array();
|
||||
while ($row = mysqli_fetch_assoc($asset_licenses_sql)) {
|
||||
$asset_licenses_array[] = intval($row['asset_id']);
|
||||
$seat_count = $seat_count + 1;
|
||||
}
|
||||
$asset_licenses = implode(',', $asset_licenses_array);
|
||||
|
||||
// Contact Licenses
|
||||
$contact_licenses_sql = mysqli_query($mysqli, "SELECT contact_id FROM software_contacts WHERE software_id = $software_id");
|
||||
$contact_licenses_array = array();
|
||||
while ($row = mysqli_fetch_assoc($contact_licenses_sql)) {
|
||||
$contact_licenses_array[] = intval($row['contact_id']);
|
||||
$seat_count = $seat_count + 1;
|
||||
}
|
||||
$contact_licenses = implode(',', $contact_licenses_array);
|
||||
|
||||
|
||||
$key_count = intval($row['key_count']);
|
||||
$seats = softwareSeats($row['seats_used'], $row['software_seats']);
|
||||
$renewal = softwareRenewal($row['software_expire']);
|
||||
$license_url = "license.php?client_id=$client_id&software_id=$software_id";
|
||||
|
||||
?>
|
||||
<tr class="<?= $tr_class ?>">
|
||||
<td class="ps-3">
|
||||
<a class="text-dark ajax-modal" href="#" data-modal-url="modals/software/software_edit.php?id=<?= $software_id ?>">
|
||||
<div class="d-flex">
|
||||
<i class="fa fa-fw fa-2x fa-cube me-3"></i>
|
||||
<div class="flex-grow-1">
|
||||
<div><?= "$software_name <span>$software_version</span>" ?></div>
|
||||
<div><small class="text-secondary"><?= $software_description ?></small></div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<tr>
|
||||
<?php if ($can_edit) { ?>
|
||||
<td class="checkbox-column bg-light border-end">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input bulk-select" type="checkbox" name="software_ids[]" value="<?= $software_id ?>" aria-label="Select <?= $software_name ?>">
|
||||
</div>
|
||||
</td>
|
||||
<?php } ?>
|
||||
<!-- The whole cell opens the license -->
|
||||
<td class="position-relative itflow-asset-name-cell">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<?= softwareTile($row['software_type']) ?>
|
||||
<div class="min-w-0">
|
||||
<a class="fw-semibold text-body stretched-link" href="<?= $license_url ?>"><?= $software_name ?></a>
|
||||
<?php if ($software_version) { ?><span class="text-body-secondary ms-1"><?= $software_version ?></span><?php } ?>
|
||||
<?php if ($software_favorite) { ?><i class="fas fa-star text-warning ms-1" title="Favorite"></i><?php } ?>
|
||||
<?php if ($software_archived_at) { ?><span class="badge rounded-pill text-bg-secondary fw-normal ms-1">Archived</span><?php } ?>
|
||||
<div class="small text-body-secondary text-truncate itflow-asset-desc" title="<?= $software_description ?: $software_type ?>"><?= $software_description ?: $software_type ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
<?php if ($software_license_type) { ?>
|
||||
<div><i class="fas fa-fw <?= softwareLicenseTypeIcon($row['software_license_type']) ?> text-secondary me-1"></i><?= $software_license_type ?></div>
|
||||
<?php } else { ?>
|
||||
<div class="text-body-secondary">-</div>
|
||||
<?php } ?>
|
||||
<?php if ($software_description) { ?><div class="small text-body-secondary"><?= $software_type ?></div><?php } ?>
|
||||
</td>
|
||||
<td class="itflow-seats-cell"><?= softwareSeatBar($seats) ?></td>
|
||||
<td class="text-nowrap">
|
||||
<?php if ($renewal) { ?>
|
||||
<div><?= escapeHtml($renewal['date']) ?></div>
|
||||
<?php if ($renewal['state'] === 'active') { ?>
|
||||
<div class="small text-body-secondary"><?= escapeHtml($renewal['text']) ?></div>
|
||||
<?php } else { ?>
|
||||
<?= softwareRenewalPill($renewal) ?>
|
||||
<?php } ?>
|
||||
<?php } else { ?>
|
||||
<span class="text-body-secondary">-</span>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<?php if ($show_key) { ?>
|
||||
<td class="itflow-license-key-cell">
|
||||
<?= $row['software_key'] ? softwareKeyHtml($row['software_key']) : '' ?>
|
||||
<?php if ($key_count) { ?>
|
||||
<div class="small text-body-secondary"><?= $key_count ?> individual key<?= $key_count === 1 ? '' : 's' ?></div>
|
||||
<?php } elseif (!$row['software_key']) { ?>
|
||||
<span class="text-body-secondary">-</span>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<?php } ?>
|
||||
<?php if ($show_purchased) { ?>
|
||||
<td class="text-nowrap"><?= $software_purchase ?: '<span class="text-body-secondary">-</span>' ?></td>
|
||||
<?php } ?>
|
||||
<?php if ($show_order_ref) { ?>
|
||||
<td class="text-nowrap">
|
||||
<?php if ($software_purchase_reference) { ?>
|
||||
<?= $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>
|
||||
<?php } else { ?>
|
||||
<span class="text-body-secondary">-</span>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<?php } ?>
|
||||
<td>
|
||||
<?php if ($vendor_name) { ?>
|
||||
<a class="ajax-modal" href="#" data-modal-url="modals/vendor/vendor.php?id=<?= $vendor_id ?>"><?= $vendor_name ?></a>
|
||||
<?php } else { ?>
|
||||
<span class="text-body-secondary">-</span>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td><?= $software_type ?></td>
|
||||
<td><?= $software_license_type ?></td>
|
||||
<td><?= "$seat_count / $software_seats" ?></td>
|
||||
<td><?= $software_expire_display ?></td>
|
||||
<td><?= $vendor_display ?></td>
|
||||
<?php if (!$client_url) { ?>
|
||||
<td><a href="software.php?client_id=<?= $client_id ?>"><?= $client_name ?></a></td>
|
||||
<?php } ?>
|
||||
<td>
|
||||
<div class="dropdown dropstart text-center">
|
||||
<button class="btn btn-secondary btn-sm" title="More actions" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<td class="text-end">
|
||||
<div class="dropdown dropstart">
|
||||
<button class="btn btn-sm btn-light" title="More actions" type="button" data-bs-toggle="dropdown"><i class="fas fa-fw fa-ellipsis-v"></i></button>
|
||||
<div class="dropdown-menu">
|
||||
<?php if (!$software_archived_at) { ?>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/software/software_edit.php?id=<?= $software_id ?>">
|
||||
<i class="fas fa-fw fa-edit me-2"></i>Edit
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/software/software.php?id=<?= $software_id ?>">
|
||||
<i class="fas fa-fw fa-eye text-secondary me-2"></i>Quick look
|
||||
</a>
|
||||
<?php if ($can_edit && !$software_archived_at) { ?>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/software/software_edit.php?id=<?= $software_id ?>">
|
||||
<i class="fas fa-fw fa-edit text-secondary me-2"></i>Edit
|
||||
</a>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/software/software_seats.php?id=<?= $software_id ?>">
|
||||
<i class="fas fa-fw fa-users text-secondary me-2"></i>Seats
|
||||
</a>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/software/software_renew.php?id=<?= $software_id ?>">
|
||||
<i class="fas fa-fw fa-redo-alt text-secondary me-2"></i>Renew
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger confirm-link" href="post.php?archive_software=<?= $software_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-archive me-2"></i>Archive and<br><small>Remove Licenses</small>
|
||||
<i class="fas fa-fw fa-archive me-2"></i>Archive
|
||||
</a>
|
||||
<?php } else { ?>
|
||||
<a class="dropdown-item text-info confirm-link" href="post.php?restore_software=<?= $software_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-redo me-2"></i>Restore
|
||||
</a>
|
||||
<?php } ?>
|
||||
<?php if ($session_user_role == 3) { ?>
|
||||
<?php if ($config_destructive_deletes_enable) { ?>
|
||||
<?php } elseif ($can_edit) { ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_software=<?= $software_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-trash me-2"></i>Delete and<br><small>Remove Licenses</small></a>
|
||||
<a class="dropdown-item" href="post.php?restore_software=<?= $software_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_software=<?= $software_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"
|
||||
data-confirm-text="<?= $software_name ?> goes for good, with its seats and keys.">
|
||||
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
||||
</a>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
@@ -347,18 +509,22 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
<?php } ?>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php require_once "../includes/filter_footer.php";
|
||||
?>
|
||||
</div>
|
||||
</form>
|
||||
<?php if (!$num_rows[0]) { ?>
|
||||
<div class="itflow-asset-empty">
|
||||
<i class="fas fa-cube fa-2x mb-2 opacity-50"></i>
|
||||
<div><?= $tab_counts[''] ? 'No licenses match.' : 'No licenses yet.' ?></div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<?php require_once "../includes/filter_footer.php"; ?>
|
||||
</div>
|
||||
|
||||
<script src="../js/bulk_actions.js"></script>
|
||||
|
||||
<?php
|
||||
require_once "../includes/footer.php";
|
||||
|
||||
@@ -4463,3 +4463,72 @@ img.itflow-contact-avatar {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
/* Licenses - agent/software.php, agent/license.php, agent/modals/software/
|
||||
|
||||
A license's tile is its software type's icon on that type's colour - the
|
||||
asset tones (functions/software.php softwareTypeStyle()). Seats are a
|
||||
count over a thin bar: blue with seats free, amber full, red over, grey
|
||||
with none in use (softwareSeatBar()). */
|
||||
.itflow-seats {
|
||||
min-width: 6.5rem;
|
||||
max-width: 11rem;
|
||||
font-size: .875rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.itflow-seats-bar {
|
||||
height: .3rem;
|
||||
margin: .25rem 0 .2rem;
|
||||
background-color: var(--bs-secondary-bg);
|
||||
}
|
||||
.itflow-seats-glance .itflow-seats {
|
||||
max-width: 14rem;
|
||||
}
|
||||
.itflow-seats-glance .itflow-seats-bar {
|
||||
margin-top: .3rem;
|
||||
}
|
||||
/* Keys - monospace, cut short on screen, copied whole */
|
||||
.itflow-license-key {
|
||||
max-width: 16rem;
|
||||
font-size: .82rem;
|
||||
}
|
||||
.itflow-license-key-cell {
|
||||
min-width: 0;
|
||||
max-width: 20rem;
|
||||
}
|
||||
.itflow-license-main-key {
|
||||
background-color: var(--bs-tertiary-bg);
|
||||
}
|
||||
.itflow-license-main-key .itflow-license-key {
|
||||
max-width: 28rem;
|
||||
}
|
||||
/* Devices / Users headings inside the Seats tab */
|
||||
.itflow-license-section {
|
||||
padding: .6rem 1rem .4rem;
|
||||
border-bottom: 1px solid var(--bs-border-color-translucent);
|
||||
font-size: .75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--bs-secondary-color);
|
||||
}
|
||||
.itflow-license-section .badge {
|
||||
background-color: var(--bs-secondary-bg);
|
||||
color: var(--bs-secondary-color);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.itflow-asset-related .table + .itflow-license-section,
|
||||
.table-responsive + .itflow-license-section {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
.itflow-license-holder-sub {
|
||||
padding-left: 1.85rem;
|
||||
}
|
||||
/* Linked documents and files on the license page sit in the pane's padding */
|
||||
.itflow-license-links {
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
.itflow-license-links .itflow-link-row {
|
||||
margin: 0 -.4rem;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ require_once __DIR__ . '/functions/network.php';
|
||||
require_once __DIR__ . '/functions/rack.php';
|
||||
require_once __DIR__ . '/functions/asset.php';
|
||||
require_once __DIR__ . '/functions/contact.php';
|
||||
require_once __DIR__ . '/functions/software.php';
|
||||
require_once __DIR__ . '/functions/links.php';
|
||||
require_once __DIR__ . '/functions/auth.php';
|
||||
require_once __DIR__ . '/functions/logging.php';
|
||||
|
||||
@@ -81,6 +81,16 @@ function clientOverviewAttention($client_id, $access, $renewals) {
|
||||
}
|
||||
}
|
||||
|
||||
if ($access['docs']) {
|
||||
// More devices and users on a license than seats bought - a true-up to bill, or seats to free
|
||||
$items[] = [
|
||||
'count' => dashboardCount("SELECT COUNT(software_id) FROM software WHERE software_client_id = $client_id AND software_archived_at IS NULL
|
||||
AND " . softwareStatusFilters()['over']['sql']),
|
||||
'label' => 'Licenses over seats', 'icon' => 'cube', 'tone' => 'danger',
|
||||
'url' => "/agent/software.php?client_id=$client_id&status=over",
|
||||
];
|
||||
}
|
||||
|
||||
if ($access['docs'] && $renewals) {
|
||||
$expired = count(array_filter($renewals['items'], function ($r) { return $r['days'] < 0; }));
|
||||
$soon = count($renewals['items']) - $expired;
|
||||
@@ -145,11 +155,11 @@ function clientOverviewContacts($client_id, $limit = 6) {
|
||||
return $contacts;
|
||||
}
|
||||
|
||||
/** Starred assets and credentials. */
|
||||
/** Starred assets, licenses and credentials. */
|
||||
function clientOverviewFavorites($client_id, $access) {
|
||||
global $mysqli;
|
||||
|
||||
$favorites = ['assets' => [], 'credentials' => []];
|
||||
$favorites = ['assets' => [], 'licenses' => [], 'credentials' => []];
|
||||
|
||||
if ($access['docs']) {
|
||||
$sql = mysqli_query($mysqli, "SELECT asset_id, asset_name, asset_type, asset_make, asset_model, interface_ip
|
||||
@@ -160,6 +170,14 @@ function clientOverviewFavorites($client_id, $access) {
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$favorites['assets'][] = $row;
|
||||
}
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT software_id, software_name, software_type, software_license_type, software_key, software_expire
|
||||
FROM software
|
||||
WHERE software_client_id = $client_id AND software_favorite = 1 AND software_archived_at IS NULL
|
||||
ORDER BY software_name");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$favorites['licenses'][] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
if ($access['credentials']) {
|
||||
@@ -260,7 +278,7 @@ function clientOverviewRenewals($client_id, $days = 45) {
|
||||
'Certificate', "/agent/certificates.php?client_id=$client_id&q="],
|
||||
['software', 'cube', "SELECT software_id AS id, software_name AS name, software_expire AS d FROM software
|
||||
WHERE software_client_id = $client_id AND software_archived_at IS NULL AND software_expire IS NOT NULL AND software_expire <= $window",
|
||||
'License', "/agent/software.php?client_id=$client_id&q="],
|
||||
'License', "/agent/license.php?client_id=$client_id&software_id="],
|
||||
['warranty', 'shield-alt', "SELECT asset_id AS id, asset_name AS name, asset_warranty_expire AS d FROM assets
|
||||
WHERE asset_client_id = $client_id AND asset_archived_at IS NULL AND asset_warranty_expire IS NOT NULL AND asset_warranty_expire <= $window",
|
||||
'Warranty', "/agent/asset.php?client_id=$client_id&asset_id="],
|
||||
@@ -279,7 +297,7 @@ function clientOverviewRenewals($client_id, $days = 45) {
|
||||
'kind' => $kind, 'icon' => $icon, 'label' => $label,
|
||||
'name' => $row['name'], 'date' => $row['d'],
|
||||
'days' => (int) $today->diff($date)->format('%r%a'),
|
||||
'url' => in_array($kind, ['warranty', 'retire'], true) ? $url . intval($row['id']) : $url . rawurlencode($row['name']),
|
||||
'url' => in_array($kind, ['warranty', 'retire', 'software'], true) ? $url . intval($row['id']) : $url . rawurlencode($row['name']),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@ function dashboardAttention($access) {
|
||||
$items[] = [
|
||||
'count' => dashboardCount("SELECT COUNT(software_id) FROM software WHERE software_archived_at IS NULL AND software_expire $soon " . clientScopeSql('software_client_id')),
|
||||
'label' => 'Licenses expiring', 'icon' => 'cube', 'tone' => 'warning', 'detail' => 'Next 30 days',
|
||||
'url' => '/agent/software.php?expire_days=30&sort=software_expire&order=ASC',
|
||||
'url' => '/agent/software.php?status=renewing&sort=software_expire&order=ASC',
|
||||
];
|
||||
$items[] = [
|
||||
'count' => dashboardCount("SELECT COUNT(asset_id) FROM assets WHERE asset_archived_at IS NULL AND asset_warranty_expire $soon " . clientScopeSql('asset_client_id')),
|
||||
|
||||
@@ -182,7 +182,7 @@ function getExportColumns($export_type) {
|
||||
'credential_uri' => ['label' => 'URI', 'weight' => 2],
|
||||
],
|
||||
|
||||
// assigned_to_assets / assigned_to_contacts are built by the handler
|
||||
// seats_used, assigned_to_assets / assigned_to_contacts and software_keys_list are built by the handler's query
|
||||
'software' => [
|
||||
'software_name' => ['label' => 'Name', 'weight' => 2],
|
||||
'software_version' => ['label' => 'Version'],
|
||||
@@ -190,11 +190,16 @@ function getExportColumns($export_type) {
|
||||
'software_type' => ['label' => 'Type'],
|
||||
'software_license_type' => ['label' => 'License Type'],
|
||||
'software_seats' => ['label' => 'Seats', 'format' => 'number'],
|
||||
'seats_used' => ['label' => 'Seats Used', 'format' => 'number'],
|
||||
'software_key' => ['label' => 'Key', 'weight' => 2],
|
||||
'software_keys_list' => ['label' => 'Individual Keys', 'weight' => 3, 'default' => false],
|
||||
'assigned_to_assets' => ['label' => 'Assets', 'weight' => 2],
|
||||
'assigned_to_contacts' => ['label' => 'Contacts', 'weight' => 2],
|
||||
'vendor_name' => ['label' => 'Vendor', 'default' => false],
|
||||
'software_purchase_reference' => ['label' => 'Order Ref', 'default' => false],
|
||||
'software_purchase' => ['label' => 'Purchased'],
|
||||
'software_expire' => ['label' => 'Expires'],
|
||||
'client_name' => ['label' => 'Client', 'default' => false],
|
||||
'software_notes' => ['label' => 'Notes', 'weight' => 3],
|
||||
],
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
/*
|
||||
* Links between documents, files and the things they are about - the assets,
|
||||
* contacts, licenses and vendors a document covers, the files attached to a
|
||||
* document, the assets and contacts a file belongs to.
|
||||
* document, the assets and contacts a file belongs to - and the seats on a
|
||||
* license (the devices and users on it) and the credentials and files that
|
||||
* go with it.
|
||||
*
|
||||
* Every link is a row in a two-column table (asset_documents, document_files,
|
||||
* contact_files...) and they all work the same way, so they are described once
|
||||
@@ -12,6 +14,9 @@
|
||||
* (agent/post/link.php) and one list (linkGroupsHtml()) on the document page
|
||||
* and the file and document quick looks.
|
||||
*
|
||||
* A type with a 'view' module (credentials) is only offered, listed or
|
||||
* linked for an agent who may see that module.
|
||||
*
|
||||
* A link only ever joins two records of the same client, and never to an
|
||||
* archived one; linkAdd() checks both rather than trusting the form.
|
||||
*/
|
||||
@@ -23,13 +28,16 @@ const LINK_ENTITIES = [
|
||||
'document' => ['table' => 'documents', 'id' => 'document_id', 'name' => 'document_name', 'client' => 'document_client_id', 'archived' => 'document_archived_at', 'label' => 'document', 'plural' => 'Documents', 'icon' => 'fa-file-alt'],
|
||||
'file' => ['table' => 'files', 'id' => 'file_id', 'name' => 'file_name', 'client' => 'file_client_id', 'archived' => 'file_archived_at', 'label' => 'file', 'plural' => 'Files', 'icon' => 'fa-paperclip'],
|
||||
'software' => ['table' => 'software', 'id' => 'software_id', 'name' => 'software_name', 'client' => 'software_client_id', 'archived' => 'software_archived_at', 'label' => 'license', 'plural' => 'Licenses', 'icon' => 'fa-cube'],
|
||||
'credential' => ['table' => 'credentials', 'id' => 'credential_id', 'name' => 'credential_name', 'client' => 'credential_client_id', 'archived' => 'credential_archived_at', 'label' => 'credential', 'plural' => 'Credentials', 'icon' => 'fa-key', 'view' => 'module_credential'],
|
||||
'vendor' => ['table' => 'vendors', 'id' => 'vendor_id', 'name' => 'vendor_name', 'client' => 'vendor_client_id', 'archived' => 'vendor_archived_at', 'label' => 'vendor', 'plural' => 'Vendors', 'icon' => 'fa-building'],
|
||||
];
|
||||
|
||||
/**
|
||||
* Which pairs can be linked, keyed by the two types in alphabetical order, and
|
||||
* the permission it takes. Linking a contact to a file has always been a
|
||||
* contacts permission; everything else is documentation, a support one.
|
||||
* contacts permission, a credential to a license a credentials one;
|
||||
* everything else is documentation, a support one. A device or user linked
|
||||
* to a license holds a seat on it.
|
||||
*/
|
||||
const LINK_RELATIONS = [
|
||||
'asset|document' => ['table' => 'asset_documents', 'module' => 'module_support'],
|
||||
@@ -39,6 +47,10 @@ const LINK_RELATIONS = [
|
||||
'document|vendor' => ['table' => 'vendor_documents', 'module' => 'module_support'],
|
||||
'asset|file' => ['table' => 'asset_files', 'module' => 'module_support'],
|
||||
'contact|file' => ['table' => 'contact_files', 'module' => 'module_client'],
|
||||
'asset|software' => ['table' => 'software_assets', 'module' => 'module_support'],
|
||||
'contact|software' => ['table' => 'software_contacts', 'module' => 'module_support'],
|
||||
'credential|software' => ['table' => 'software_credentials', 'module' => 'module_credential'],
|
||||
'file|software' => ['table' => 'software_files', 'module' => 'module_support'],
|
||||
];
|
||||
|
||||
/** The relation between two types, or null when they cannot be linked. */
|
||||
@@ -48,11 +60,17 @@ function linkRelation($a, $b) {
|
||||
return LINK_RELATIONS[implode('|', $pair)] ?? null;
|
||||
}
|
||||
|
||||
/** What a $from record can be linked to, in the order the pages list them. */
|
||||
/**
|
||||
* What a $from record can be linked to, in the order the pages list them -
|
||||
* a license's seats first - leaving out types this agent may not see.
|
||||
*/
|
||||
function linkTypesFor($from) {
|
||||
$order = ['file', 'document', 'asset', 'contact', 'software', 'vendor'];
|
||||
$order = $from === 'software'
|
||||
? ['asset', 'contact', 'credential', 'document', 'file']
|
||||
: ['file', 'document', 'asset', 'contact', 'software', 'credential', 'vendor'];
|
||||
return array_values(array_filter($order, function ($to) use ($from) {
|
||||
return $to !== $from && linkRelation($from, $to);
|
||||
$view = LINK_ENTITIES[$to]['view'] ?? null;
|
||||
return $to !== $from && linkRelation($from, $to) && (!$view || lookupUserPermission($view) >= 1);
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -83,7 +101,8 @@ function linkDisplayColumns($type) {
|
||||
'contact' => 'contact_title, contact_photo, contact_client_id',
|
||||
'document' => 'document_description, document_updated_at, document_created_at',
|
||||
'file' => 'file_ext, file_size, file_mime_type, file_description',
|
||||
'software' => 'software_type, software_version',
|
||||
'software' => 'software_type, software_version, software_license_type, software_seats, software_expire, ' . softwareSeatsUsedSql('t.software_id') . ' AS seats_used',
|
||||
'credential' => 'credential_description, credential_uri, credential_username, credential_otp_secret, credential_favorite',
|
||||
'vendor' => 'vendor_description',
|
||||
][$type] ?? '';
|
||||
}
|
||||
@@ -152,9 +171,9 @@ function linkCounts($from, $id) {
|
||||
return $counts;
|
||||
}
|
||||
|
||||
/** The audit log entry a link change is filed under - the document or file side of it. */
|
||||
/** The audit log entry a link change is filed under - the document, file or license side of it. */
|
||||
function linkLogSide($from, $id, $to, $other_id) {
|
||||
foreach (['document', 'file'] as $side) {
|
||||
foreach (['document', 'file', 'software'] as $side) {
|
||||
if ($from === $side) {
|
||||
return [ucfirst($side), $id];
|
||||
}
|
||||
@@ -217,6 +236,14 @@ function linkRemove($from, $id, $to, $other_id) {
|
||||
if (mysqli_affected_rows($mysqli) < 1) {
|
||||
return '';
|
||||
}
|
||||
// A device or user off a license gives back any of its keys they had
|
||||
foreach (['asset', 'contact'] as $holder) {
|
||||
if ([$from, $to] === ['software', $holder]) {
|
||||
softwareFreeKeys($source['id'], $holder, $other['id']);
|
||||
} elseif ([$from, $to] === [$holder, 'software']) {
|
||||
softwareFreeKeys($other['id'], $holder, $source['id']);
|
||||
}
|
||||
}
|
||||
[$log_type, $log_id] = linkLogSide($from, intval($source['id']), $to, intval($other['id']));
|
||||
$log_from = $log_type === ucfirst($from) ? $source['name'] : $other['name'];
|
||||
$log_other = $log_type === ucfirst($from) ? $other['name'] : $source['name'];
|
||||
@@ -272,9 +299,20 @@ function linkRowHtml($to, $row, $client_id) {
|
||||
$detail = 'Updated ' . timeAgo($row['document_updated_at'] ?: $row['document_created_at']);
|
||||
break;
|
||||
case 'software':
|
||||
$lead = '<span class="itflow-link-icon" aria-hidden="true"><i class="fas fa-fw fa-cube"></i></span>';
|
||||
$link = "<a class=\"ajax-modal text-truncate\" href=\"#\" data-modal-url=\"modals/software/software_edit.php?id=$id\">$name</a>";
|
||||
$detail = trim($row['software_type'] . ' ' . $row['software_version']);
|
||||
$lead = softwareTile($row['software_type'], 'sm');
|
||||
$link = "<a class=\"ajax-modal text-truncate\" href=\"#\" data-modal-size=\"lg\" data-modal-url=\"modals/software/software.php?id=$id\">$name</a>";
|
||||
$seats = softwareSeats($row['seats_used'] ?? 0, $row['software_seats'] ?? 0);
|
||||
$renewal = softwareRenewal($row['software_expire'] ?? '');
|
||||
$detail = implode(' · ', array_filter([
|
||||
$row['software_license_type'] ?? '',
|
||||
$seats['state'] === 'unlimited' ? '' : $seats['used'] . ' / ' . $seats['total'] . ' seats',
|
||||
$renewal && $renewal['state'] !== 'active' ? $renewal['text'] : '',
|
||||
]));
|
||||
break;
|
||||
case 'credential':
|
||||
$lead = '<span class="itflow-link-icon" aria-hidden="true"><i class="fas fa-fw fa-key"></i></span>';
|
||||
$link = "<a class=\"ajax-modal text-truncate\" href=\"#\" data-modal-url=\"modals/credential/credential_edit.php?id=$id\">$name</a>";
|
||||
$detail = $row['credential_description'] ?: $row['credential_uri'];
|
||||
break;
|
||||
case 'vendor':
|
||||
$lead = '<span class="itflow-link-icon" aria-hidden="true"><i class="fas fa-fw fa-building"></i></span>';
|
||||
|
||||
@@ -214,6 +214,32 @@ function globalSearchGroups($terms, $deep) {
|
||||
},
|
||||
];
|
||||
|
||||
// Licenses by name, vendor and order reference - and by key, so a key read
|
||||
// off a sticker or a screen finds the license it belongs to
|
||||
$groups[] = [
|
||||
'key' => 'licenses', 'label' => 'Licenses', 'icon' => 'cube', 'module' => 'module_support',
|
||||
'select' => 'software.software_id, software_name, software_version, software_license_type, software_expire, software_client_id, client_name, vendor_name',
|
||||
'from' => 'software
|
||||
LEFT JOIN clients ON software_client_id = client_id
|
||||
LEFT JOIN vendors ON vendor_id = software_vendor_id',
|
||||
'where' => "software_archived_at IS NULL
|
||||
AND (software_name LIKE '%$like%' OR software_description LIKE '%$like%' OR software_key LIKE '%$like%'
|
||||
OR software_purchase_reference LIKE '%$like%' OR vendor_name LIKE '%$like%'
|
||||
OR software.software_id IN (SELECT software_key_software_id FROM software_keys WHERE software_key LIKE '%$like%'))",
|
||||
'scope' => 'software_client_id',
|
||||
'order' => "(software_key = '$eq') DESC, (software_name = '$eq') DESC, (software_name LIKE '$like%') DESC, software_name",
|
||||
'list_url' => '/agent/software.php?' . http_build_query(['q' => $q]),
|
||||
'item' => function ($row) {
|
||||
$renewal = softwareRenewal($row['software_expire']);
|
||||
return [
|
||||
'title' => trim($row['software_name'] . ' ' . $row['software_version']),
|
||||
'subtitle' => implode(' · ', array_filter([$row['software_license_type'], $row['vendor_name'], $renewal && $renewal['state'] !== 'active' ? $renewal['text'] : ''])),
|
||||
'context' => $row['client_name'],
|
||||
'url' => globalSearchClientUrl('license.php', $row['software_client_id'], ['software_id' => intval($row['software_id'])]),
|
||||
];
|
||||
},
|
||||
];
|
||||
|
||||
// Name and description only. Usernames and passwords are encrypted at rest,
|
||||
// and the results page reveals passwords through the audited endpoint, never
|
||||
// in the markup.
|
||||
|
||||
609
functions/software.php
Normal file
609
functions/software.php
Normal file
@@ -0,0 +1,609 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Licenses - the software table and everything hung off it: the devices and
|
||||
* users holding a seat (software_assets, software_contacts), the individual
|
||||
* keys and who each one is handed to (software_keys and its two assignment
|
||||
* tables), and the documents, files and credentials linked to it
|
||||
* (functions/links.php).
|
||||
*
|
||||
* What the license list, the license page and quick look, and the asset and
|
||||
* contact pages share: how a renewal date and a seat count read, the icon a
|
||||
* kind of software gets, the seat and key queries, and keeping seats in step
|
||||
* when a form or a key assignment changes them.
|
||||
*
|
||||
* A seat is a device or user on the license. Archived devices and users keep
|
||||
* their seat row but do not use a seat - the same as they drop out of every
|
||||
* other list - and get it back if they are restored. A license with no seat
|
||||
* count (blank or 0) has no limit.
|
||||
*/
|
||||
|
||||
/** License types offered on the form, with their icons - the common MSP ones first. */
|
||||
const SOFTWARE_LICENSE_TYPES = [
|
||||
'Subscription' => 'fa-sync-alt',
|
||||
'Device' => 'fa-desktop',
|
||||
'User' => 'fa-user',
|
||||
'Site' => 'fa-building',
|
||||
'Concurrent' => 'fa-users',
|
||||
'Perpetual' => 'fa-infinity',
|
||||
'Trial' => 'fa-hourglass-half',
|
||||
'Usage-based' => 'fa-tachometer-alt',
|
||||
'Open Source' => 'fa-code-branch',
|
||||
];
|
||||
|
||||
/** The icon for a license type, typed or from the list. */
|
||||
function softwareLicenseTypeIcon($license_type) {
|
||||
$license_type = html_entity_decode((string) $license_type, ENT_QUOTES, 'UTF-8');
|
||||
return SOFTWARE_LICENSE_TYPES[$license_type] ?? 'fa-certificate';
|
||||
}
|
||||
|
||||
/**
|
||||
* The icon and colour for a kind of software (Admin > Categories, software
|
||||
* types). An icon set on the category wins; otherwise it is read from the
|
||||
* name, so "Security Software" is a red shield and "Software as a Service" a
|
||||
* cloud. The colours are the asset tones (css: .itflow-asset-tone-*).
|
||||
*/
|
||||
function softwareTypeStyle($software_type) {
|
||||
global $mysqli;
|
||||
static $category_icons = null;
|
||||
|
||||
if ($category_icons === null) {
|
||||
$category_icons = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT category_name, category_icon FROM categories
|
||||
WHERE category_type = 'software_type' AND category_icon IS NOT NULL AND category_icon != ''");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
if (preg_match('/^fa-[a-z0-9-]+$/', $row['category_icon'])) {
|
||||
$category_icons[strtolower($row['category_name'])] = $row['category_icon'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$type = strtolower(trim(html_entity_decode((string) $software_type, ENT_QUOTES, 'UTF-8')));
|
||||
|
||||
// First match wins - "Operating System" before "System Software"
|
||||
$styles = [
|
||||
'saas' => ['fa-cloud', 'wireless'],
|
||||
'service' => ['fa-cloud', 'wireless'],
|
||||
'cloud' => ['fa-cloud', 'wireless'],
|
||||
'productivity' => ['fa-briefcase', 'workstation'],
|
||||
'office' => ['fa-briefcase', 'workstation'],
|
||||
'security' => ['fa-shield-alt', 'firewall'],
|
||||
'antivirus' => ['fa-shield-alt', 'firewall'],
|
||||
'backup' => ['fa-hdd', 'network'],
|
||||
'web' => ['fa-globe', 'network'],
|
||||
'mobile' => ['fa-mobile-alt', 'phone'],
|
||||
'operating' => ['fa-layer-group', 'virtual'],
|
||||
'system' => ['fa-cogs', 'server'],
|
||||
'desktop' => ['fa-window-maximize', 'peripheral'],
|
||||
];
|
||||
$style = ['fa-cube', 'other'];
|
||||
foreach ($styles as $needle => $candidate) {
|
||||
if ($type !== '' && strpos($type, $needle) !== false) {
|
||||
$style = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isset($category_icons[$type])) {
|
||||
$style[0] = $category_icons[$type];
|
||||
}
|
||||
|
||||
return ['icon' => $style[0], 'tone' => $style[1]];
|
||||
}
|
||||
|
||||
/** The tile for a license - its software type's icon on that type's colour. $size '' | 'sm' | 'lg'. */
|
||||
function softwareTile($software_type, $size = '') {
|
||||
$style = softwareTypeStyle($software_type);
|
||||
$size_class = in_array($size, ['sm', 'lg'], true) ? " itflow-asset-icon-$size" : '';
|
||||
return '<span class="itflow-asset-icon' . $size_class . ' itflow-asset-tone-' . $style['tone'] . '" aria-hidden="true"><i class="fas fa-fw ' . $style['icon'] . '"></i></span>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a renewal date stands - null without one. 'state' is expired, soon
|
||||
* (within $soon_days) or active; 'short' fits a pill, 'text' a sentence.
|
||||
*/
|
||||
function softwareRenewal($expire, $soon_days = 30) {
|
||||
$expire = trim((string) $expire);
|
||||
if ($expire === '' || strpos($expire, '0000-00-00') === 0) {
|
||||
return null;
|
||||
}
|
||||
$date = DateTime::createFromFormat('!Y-m-d', substr($expire, 0, 10));
|
||||
if (!$date) {
|
||||
return null;
|
||||
}
|
||||
$days = (int) (new DateTime('today'))->diff($date)->format('%r%a');
|
||||
$ymd = $date->format('Y-m-d');
|
||||
|
||||
if ($days < 0) {
|
||||
$ago = -$days;
|
||||
$text = $ago === 1 ? 'Expired yesterday' : ($ago < 60 ? "Expired $ago days ago" : 'Expired ' . timeAgo($ymd));
|
||||
return ['state' => 'expired', 'tone' => 'danger', 'short' => 'Expired', 'text' => $text, 'days' => $days, 'date' => $ymd];
|
||||
}
|
||||
if ($days <= $soon_days) {
|
||||
$text = $days === 0 ? 'Renews today' : ($days === 1 ? 'Renews tomorrow' : "Renews in $days days");
|
||||
return ['state' => 'soon', 'tone' => $days <= 7 ? 'danger' : 'warning', 'short' => $text, 'text' => $text, 'days' => $days, 'date' => $ymd];
|
||||
}
|
||||
return ['state' => 'active', 'tone' => 'success', 'short' => 'Active', 'text' => 'Renews ' . timeAgo($ymd), 'days' => $days, 'date' => $ymd];
|
||||
}
|
||||
|
||||
/** A renewal as a soft pill - expired red, soon amber, otherwise green. */
|
||||
function softwareRenewalPill($renewal, $use_text = false) {
|
||||
if (!$renewal) {
|
||||
return '';
|
||||
}
|
||||
$tone = $renewal['tone'];
|
||||
$icon = ['expired' => 'fa-calendar-times', 'soon' => 'fa-hourglass-half', 'active' => 'fa-calendar-check'][$renewal['state']];
|
||||
return '<span class="badge rounded-pill bg-' . $tone . '-subtle text-' . $tone . '-emphasis border border-' . $tone . '-subtle fw-normal" title="' . escapeHtml($renewal['date']) . '">'
|
||||
. '<i class="fas ' . $icon . ' me-1"></i>' . escapeHtml($use_text ? $renewal['text'] : $renewal['short']) . '</span>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Seats in use against seats bought. 'state' is over, full, unused, ok - or
|
||||
* unlimited when the license has no seat count.
|
||||
*/
|
||||
function softwareSeats($used, $total) {
|
||||
$used = intval($used);
|
||||
$total = intval($total);
|
||||
if ($total <= 0) {
|
||||
return ['used' => $used, 'total' => 0, 'free' => null, 'over' => 0, 'pct' => 0, 'state' => 'unlimited', 'tone' => 'secondary',
|
||||
'text' => $used === 1 ? '1 in use' : "$used in use", 'title' => "$used in use, no seat limit"];
|
||||
}
|
||||
$over = max(0, $used - $total);
|
||||
$free = max(0, $total - $used);
|
||||
$state = $over ? 'over' : ($free === 0 ? 'full' : ($used === 0 ? 'unused' : 'ok'));
|
||||
$text = ['over' => "$over over", 'full' => 'All in use', 'unused' => 'None in use', 'ok' => "$free free"][$state];
|
||||
return [
|
||||
'used' => $used, 'total' => $total, 'free' => $free, 'over' => $over,
|
||||
'pct' => min(100, (int) round($used / $total * 100)),
|
||||
'state' => $state,
|
||||
'tone' => ['over' => 'danger', 'full' => 'warning', 'unused' => 'secondary', 'ok' => 'primary'][$state],
|
||||
'text' => $text,
|
||||
'title' => "$used of $total seats in use" . ($over ? " - $over over" : ''),
|
||||
];
|
||||
}
|
||||
|
||||
/** Seats as "3 / 5" over a thin bar, and what is left - or "4 in use" for a license without a limit. */
|
||||
function softwareSeatBar($seats, $show_text = true) {
|
||||
$text = $show_text ? '<div class="small text-' . ($seats['state'] === 'over' ? 'danger-emphasis fw-semibold' : 'body-secondary') . '">' . escapeHtml($seats['text']) . '</div>' : '';
|
||||
if ($seats['state'] === 'unlimited') {
|
||||
return '<div class="itflow-seats" title="' . escapeHtml($seats['title']) . '"><span class="fw-semibold">' . $seats['used'] . '</span> <span class="small text-body-secondary">in use</span>'
|
||||
. ($show_text ? '<div class="small text-body-secondary">No seat limit</div>' : '') . '</div>';
|
||||
}
|
||||
return '<div class="itflow-seats" title="' . escapeHtml($seats['title']) . '">'
|
||||
. '<div class="d-flex align-items-baseline gap-1"><span class="fw-semibold' . ($seats['state'] === 'over' ? ' text-danger-emphasis' : '') . '">' . $seats['used'] . '</span><span class="text-body-secondary">/ ' . $seats['total'] . '</span></div>'
|
||||
. '<div class="progress itflow-seats-bar" role="progressbar" aria-label="Seats in use" aria-valuenow="' . $seats['used'] . '" aria-valuemin="0" aria-valuemax="' . $seats['total'] . '">'
|
||||
. '<div class="progress-bar bg-' . $seats['tone'] . '" style="width: ' . $seats['pct'] . '%"></div></div>'
|
||||
. $text . '</div>';
|
||||
}
|
||||
|
||||
/** SQL for the seats in use on the license in $software_column - devices plus users, archived ones left out. */
|
||||
function softwareSeatsUsedSql($software_column = 'software.software_id') {
|
||||
return "((SELECT COUNT(*) FROM software_assets AS seat_a JOIN assets AS seat_aa ON seat_aa.asset_id = seat_a.asset_id AND seat_aa.asset_archived_at IS NULL WHERE seat_a.software_id = $software_column)
|
||||
+ (SELECT COUNT(*) FROM software_contacts AS seat_c JOIN contacts AS seat_cc ON seat_cc.contact_id = seat_c.contact_id AND seat_cc.contact_archived_at IS NULL WHERE seat_c.software_id = $software_column))";
|
||||
}
|
||||
|
||||
/**
|
||||
* The list's attention tabs - each a WHERE fragment on the software table.
|
||||
* Renewing is the same 30 days the dashboard counts.
|
||||
*/
|
||||
function softwareStatusFilters() {
|
||||
$used = softwareSeatsUsedSql('software.software_id');
|
||||
return [
|
||||
'renewing' => ['label' => 'Renewing soon', 'icon' => 'fa-hourglass-half', 'title' => 'Renewing in the next 30 days',
|
||||
'sql' => "software_expire IS NOT NULL AND software_expire BETWEEN CURDATE() AND CURDATE() + INTERVAL 30 DAY"],
|
||||
'expired' => ['label' => 'Expired', 'icon' => 'fa-calendar-times', 'title' => 'Past their renewal date',
|
||||
'sql' => "software_expire IS NOT NULL AND software_expire != '0000-00-00' AND software_expire < CURDATE()"],
|
||||
'over' => ['label' => 'Over seats', 'icon' => 'fa-exclamation-triangle', 'title' => 'More devices and users than seats bought',
|
||||
'sql' => "software_seats > 0 AND $used > software_seats"],
|
||||
'unused' => ['label' => 'No seats assigned', 'icon' => 'fa-user-slash', 'title' => 'Nobody and nothing on them yet',
|
||||
'sql' => "$used = 0"],
|
||||
];
|
||||
}
|
||||
|
||||
/** A license key, monospace, with a copy button - long keys are cut short on screen but copy whole. */
|
||||
function softwareKeyHtml($key, $class = '') {
|
||||
$key = escapeHtml($key);
|
||||
if ($key === '') {
|
||||
return '';
|
||||
}
|
||||
return '<span class="d-inline-flex align-items-center mw-100 ' . $class . '"><span class="font-monospace text-truncate itflow-license-key" title="' . $key . '">' . $key . '</span>'
|
||||
. '<button type="button" class="itflow-copy clipboardjs" title="Copy key" data-clipboard-text="' . $key . '"><i class="far fa-copy"></i></button></span>';
|
||||
}
|
||||
|
||||
/** One license by id with its client, vendor and seats in use - raw, or null. */
|
||||
function softwareRow($software_id) {
|
||||
global $mysqli;
|
||||
$software_id = intval($software_id);
|
||||
if (!$software_id) {
|
||||
return null;
|
||||
}
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT software.*, client_id, client_name,
|
||||
vendor_id, vendor_name, vendor_description, vendor_account_number, vendor_phone, vendor_phone_country_code, vendor_extension,
|
||||
vendor_email, vendor_website, vendor_hours, vendor_sla, vendor_archived_at,
|
||||
" . softwareSeatsUsedSql('software.software_id') . " AS seats_used
|
||||
FROM software
|
||||
LEFT JOIN clients ON client_id = software_client_id
|
||||
LEFT JOIN vendors ON vendor_id = software_vendor_id
|
||||
WHERE software_id = $software_id
|
||||
LIMIT 1"));
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The devices and users on a license, each with the keys handed to them.
|
||||
* Archived ones are left out - they do not use a seat.
|
||||
*/
|
||||
function softwareSeatHolders($software_id) {
|
||||
global $mysqli;
|
||||
$software_id = intval($software_id);
|
||||
|
||||
$holders = ['assets' => [], 'contacts' => []];
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT assets.asset_id, asset_name, asset_type, asset_make, asset_model, asset_client_id,
|
||||
asset_contact_id, contacts.contact_name AS asset_contact_name, contacts.contact_photo AS asset_contact_photo,
|
||||
(SELECT GROUP_CONCAT(k.software_key ORDER BY k.software_key_id SEPARATOR '\n') FROM software_keys AS k
|
||||
JOIN software_key_asset_assignments AS ka ON ka.software_key_id = k.software_key_id
|
||||
WHERE k.software_key_software_id = $software_id AND ka.asset_id = assets.asset_id) AS held_keys
|
||||
FROM software_assets
|
||||
JOIN assets ON assets.asset_id = software_assets.asset_id
|
||||
LEFT JOIN contacts ON contacts.contact_id = asset_contact_id
|
||||
WHERE software_assets.software_id = $software_id AND asset_archived_at IS NULL
|
||||
ORDER BY asset_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$row['held_keys'] = $row['held_keys'] !== null ? explode("\n", $row['held_keys']) : [];
|
||||
$holders['assets'][] = $row;
|
||||
}
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT contacts.contact_id, contact_name, contact_title, contact_email, contact_photo, contact_client_id,
|
||||
(SELECT GROUP_CONCAT(k.software_key ORDER BY k.software_key_id SEPARATOR '\n') FROM software_keys AS k
|
||||
JOIN software_key_contact_assignments AS kc ON kc.software_key_id = k.software_key_id
|
||||
WHERE k.software_key_software_id = $software_id AND kc.contact_id = contacts.contact_id) AS held_keys
|
||||
FROM software_contacts
|
||||
JOIN contacts ON contacts.contact_id = software_contacts.contact_id
|
||||
WHERE software_contacts.software_id = $software_id AND contact_archived_at IS NULL
|
||||
ORDER BY contact_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$row['held_keys'] = $row['held_keys'] !== null ? explode("\n", $row['held_keys']) : [];
|
||||
$holders['contacts'][] = $row;
|
||||
}
|
||||
|
||||
return $holders;
|
||||
}
|
||||
|
||||
/** A license's individual keys, oldest first, each with the devices and users it is handed to. */
|
||||
function softwareKeys($software_id) {
|
||||
global $mysqli;
|
||||
$software_id = intval($software_id);
|
||||
|
||||
$keys = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT software_key_id, software_key FROM software_keys WHERE software_key_software_id = $software_id ORDER BY software_key_id ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$keys[intval($row['software_key_id'])] = ['id' => intval($row['software_key_id']), 'key' => $row['software_key'], 'assets' => [], 'contacts' => []];
|
||||
}
|
||||
if (!$keys) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ids = implode(',', array_keys($keys));
|
||||
$sql = mysqli_query($mysqli, "SELECT ka.software_key_id, assets.asset_id, asset_name, asset_type, asset_archived_at FROM software_key_asset_assignments AS ka
|
||||
JOIN assets ON assets.asset_id = ka.asset_id WHERE ka.software_key_id IN ($ids) ORDER BY asset_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$keys[intval($row['software_key_id'])]['assets'][] = $row;
|
||||
}
|
||||
$sql = mysqli_query($mysqli, "SELECT kc.software_key_id, contacts.contact_id, contact_name, contact_photo, contact_client_id, contact_archived_at FROM software_key_contact_assignments AS kc
|
||||
JOIN contacts ON contacts.contact_id = kc.contact_id WHERE kc.software_key_id IN ($ids) ORDER BY contact_name ASC");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$keys[intval($row['software_key_id'])]['contacts'][] = $row;
|
||||
}
|
||||
|
||||
return array_values($keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* The licenses one device ($kind 'asset') or user ('contact') holds a seat
|
||||
* on - not archived - with each license's seats in use and the keys handed to
|
||||
* this holder. For the Licenses tab on the asset and contact pages and quick looks.
|
||||
*/
|
||||
function softwareHeldBy($kind, $holder_id) {
|
||||
global $mysqli;
|
||||
$holder_id = intval($holder_id);
|
||||
if ($kind === 'asset') {
|
||||
[$seat_table, $holder_column, $assign_table] = ['software_assets', 'asset_id', 'software_key_asset_assignments'];
|
||||
} elseif ($kind === 'contact') {
|
||||
[$seat_table, $holder_column, $assign_table] = ['software_contacts', 'contact_id', 'software_key_contact_assignments'];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT software.software_id, software_name, software_version, software_type, software_license_type,
|
||||
software_seats, software_expire, software_key, software_client_id, vendor_name,
|
||||
" . softwareSeatsUsedSql('software.software_id') . " AS seats_used,
|
||||
(SELECT GROUP_CONCAT(k.software_key ORDER BY k.software_key_id SEPARATOR '\n') FROM software_keys AS k
|
||||
JOIN $assign_table AS a ON a.software_key_id = k.software_key_id
|
||||
WHERE k.software_key_software_id = software.software_id AND a.$holder_column = $holder_id) AS held_keys
|
||||
FROM $seat_table AS h
|
||||
JOIN software ON software.software_id = h.software_id
|
||||
LEFT JOIN vendors ON vendor_id = software_vendor_id
|
||||
WHERE h.$holder_column = $holder_id AND software_archived_at IS NULL
|
||||
ORDER BY software_name ASC");
|
||||
$rows = [];
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$row['held_keys'] = $row['held_keys'] !== null ? explode("\n", $row['held_keys']) : [];
|
||||
$rows[] = $row;
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Licenses tab on the asset and contact pages: each license this device
|
||||
* or user holds, opening its quick look, with its seats, renewal and the key
|
||||
* to install - theirs if one was handed to them, otherwise the license key.
|
||||
* $rows from softwareHeldBy().
|
||||
*/
|
||||
function softwareHeldTable($rows, $kind, $holder_id, $can_edit) {
|
||||
$holder_id = intval($holder_id);
|
||||
$html = '<div class="table-responsive"><table class="table table-hover align-middle mb-0"><thead class="text-nowrap table-light"><tr>'
|
||||
. '<th>License</th><th>Seats</th><th>Renews</th><th>Key</th><th></th></tr></thead><tbody>';
|
||||
foreach ($rows as $row) {
|
||||
$software_id = intval($row['software_id']);
|
||||
$renewal = softwareRenewal($row['software_expire']);
|
||||
$sub = implode(' · ', array_filter([$row['software_license_type'], $row['software_version']]));
|
||||
$keys = $row['held_keys'];
|
||||
$key_html = '';
|
||||
foreach ($keys as $key) {
|
||||
$key_html .= '<div>' . softwareKeyHtml($key) . '</div>';
|
||||
}
|
||||
if (!$keys && $row['software_key']) {
|
||||
$key_html = softwareKeyHtml($row['software_key']) . '<div class="small text-body-secondary">License key</div>';
|
||||
}
|
||||
$renew_html = $renewal
|
||||
? escapeHtml($renewal['date']) . '<div class="small text-' . ($renewal['tone'] === 'success' ? 'body-secondary' : $renewal['tone'] . '-emphasis') . '">' . escapeHtml($renewal['text']) . '</div>'
|
||||
: '<span class="text-body-secondary">-</span>';
|
||||
$unlink = $can_edit
|
||||
? '<a href="post.php?unlink_item&from=' . $kind . '&id=' . $holder_id . '&to=software&other=' . $software_id . '&csrf_token=' . escapeHtml($_SESSION['csrf_token'] ?? '') . '" class="btn btn-sm btn-light confirm-link" title="Take off this license" data-confirm-text="Frees the seat, and any key it had."><i class="fas fa-fw fa-unlink"></i></a>'
|
||||
: '';
|
||||
$html .= '<tr>'
|
||||
. '<td><div class="d-flex align-items-center gap-2">' . softwareTile($row['software_type'], 'sm') . '<div class="min-w-0">'
|
||||
. '<a class="fw-semibold ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/software/software.php?id=' . $software_id . '">' . escapeHtml($row['software_name']) . '</a>'
|
||||
. ($sub !== '' ? '<div class="small text-body-secondary">' . escapeHtml($sub) . '</div>' : '')
|
||||
. '</div></div></td>'
|
||||
. '<td>' . softwareSeatBar(softwareSeats($row['seats_used'], $row['software_seats'])) . '</td>'
|
||||
. '<td class="text-nowrap">' . $renew_html . '</td>'
|
||||
. '<td class="itflow-license-key-cell">' . ($key_html !== '' ? $key_html : '<span class="text-body-secondary">-</span>') . '</td>'
|
||||
. '<td class="text-end">' . $unlink . '</td>'
|
||||
. '</tr>';
|
||||
}
|
||||
return $html . '</tbody></table></div>';
|
||||
}
|
||||
|
||||
/** The same, as the short list in the asset and contact quick looks. */
|
||||
function softwareHeldList($rows) {
|
||||
$html = '<ul class="list-unstyled mb-0 itflow-links">';
|
||||
foreach ($rows as $row) {
|
||||
$seats = softwareSeats($row['seats_used'], $row['software_seats']);
|
||||
$renewal = softwareRenewal($row['software_expire']);
|
||||
$detail = implode(' · ', array_filter([
|
||||
$row['software_license_type'],
|
||||
$seats['state'] === 'unlimited' ? '' : $seats['used'] . ' / ' . $seats['total'] . ' seats',
|
||||
$renewal ? $renewal['text'] : '',
|
||||
]));
|
||||
$key = $row['held_keys'][0] ?? $row['software_key'];
|
||||
$tone = $seats['state'] === 'over' || ($renewal && $renewal['state'] === 'expired') ? 'danger-emphasis' : ($renewal && $renewal['state'] === 'soon' ? 'warning-emphasis' : 'body-secondary');
|
||||
$html .= '<li class="itflow-link-row">' . softwareTile($row['software_type'], '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/software/software.php?id=' . intval($row['software_id']) . '">' . escapeHtml($row['software_name']) . '</a></div>'
|
||||
. ($detail !== '' ? '<div class="small text-' . $tone . ' text-truncate">' . escapeHtml($detail) . '</div>' : '') . '</div>'
|
||||
. ($key ? '<span class="small text-nowrap">' . softwareKeyHtml($key) . '</span>' : '')
|
||||
. '</li>';
|
||||
}
|
||||
return $html . '</ul>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Of $ids, the devices ($kind 'asset') or users ('contact') that belong to
|
||||
* $client_id and are not archived - what a form may put on a license.
|
||||
*/
|
||||
function softwareClientIds($kind, $ids, $client_id) {
|
||||
global $mysqli;
|
||||
$ids = array_values(array_unique(array_filter(array_map('intval', (array) $ids))));
|
||||
$client_id = intval($client_id);
|
||||
if (!$ids || !$client_id) {
|
||||
return [];
|
||||
}
|
||||
[$table, $id_column, $client_column, $archived_column] = $kind === 'asset'
|
||||
? ['assets', 'asset_id', 'asset_client_id', 'asset_archived_at']
|
||||
: ['contacts', 'contact_id', 'contact_client_id', 'contact_archived_at'];
|
||||
$sql = mysqli_query($mysqli, "SELECT $id_column FROM $table WHERE $id_column IN (" . implode(',', $ids) . ") AND $client_column = $client_id AND $archived_column IS NULL");
|
||||
$valid = [];
|
||||
while ($row = mysqli_fetch_row($sql)) {
|
||||
$valid[] = intval($row[0]);
|
||||
}
|
||||
return $valid;
|
||||
}
|
||||
|
||||
/** Take back any of this license's keys handed to a device or user that has lost its seat. */
|
||||
function softwareFreeKeys($software_id, $kind, $holder_id) {
|
||||
global $mysqli;
|
||||
$software_id = intval($software_id);
|
||||
$holder_id = intval($holder_id);
|
||||
[$assign_table, $holder_column] = $kind === 'asset'
|
||||
? ['software_key_asset_assignments', 'asset_id']
|
||||
: ['software_key_contact_assignments', 'contact_id'];
|
||||
mysqli_query($mysqli, "DELETE a FROM $assign_table AS a JOIN software_keys AS k ON k.software_key_id = a.software_key_id
|
||||
WHERE k.software_key_software_id = $software_id AND a.$holder_column = $holder_id");
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a license's devices and users exactly these - adding the new ones and
|
||||
* dropping the rest (their keys go back to the pool). Only the license's own
|
||||
* client's, unarchived records are taken; archived holders are left alone.
|
||||
* Returns the names added and removed, for the log.
|
||||
*/
|
||||
function softwareSyncSeats($software_id, $client_id, $asset_ids, $contact_ids) {
|
||||
global $mysqli;
|
||||
$software_id = intval($software_id);
|
||||
|
||||
$changes = ['added' => [], 'removed' => []];
|
||||
$kinds = [
|
||||
'asset' => ['software_assets', 'assets', 'asset_id', 'asset_name', 'asset_archived_at', $asset_ids],
|
||||
'contact' => ['software_contacts', 'contacts', 'contact_id', 'contact_name', 'contact_archived_at', $contact_ids],
|
||||
];
|
||||
foreach ($kinds as $kind => [$seat_table, $table, $id_column, $name_column, $archived_column, $wanted]) {
|
||||
$wanted = softwareClientIds($kind, $wanted, $client_id);
|
||||
|
||||
$current = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT s.$id_column, $name_column FROM $seat_table AS s JOIN $table AS t ON t.$id_column = s.$id_column
|
||||
WHERE s.software_id = $software_id AND t.$archived_column IS NULL");
|
||||
while ($row = mysqli_fetch_row($sql)) {
|
||||
$current[intval($row[0])] = $row[1];
|
||||
}
|
||||
|
||||
foreach (array_diff($wanted, array_keys($current)) as $id) {
|
||||
mysqli_query($mysqli, "INSERT IGNORE INTO $seat_table SET software_id = $software_id, $id_column = $id");
|
||||
if (mysqli_affected_rows($mysqli) > 0) {
|
||||
$changes['added'][] = getFieldById($table, $id, $name_column);
|
||||
}
|
||||
}
|
||||
foreach (array_diff(array_keys($current), $wanted) as $id) {
|
||||
mysqli_query($mysqli, "DELETE FROM $seat_table WHERE software_id = $software_id AND $id_column = $id");
|
||||
softwareFreeKeys($software_id, $kind, $id);
|
||||
$changes['removed'][] = $current[$id];
|
||||
}
|
||||
}
|
||||
return $changes;
|
||||
}
|
||||
|
||||
/** One line for the log about seats a save changed - '' when it changed none. */
|
||||
function softwareSeatChangeText($changes) {
|
||||
$parts = [];
|
||||
if ($changes['added']) {
|
||||
$parts[] = 'added ' . implode(', ', $changes['added']);
|
||||
}
|
||||
if ($changes['removed']) {
|
||||
$parts[] = 'removed ' . implode(', ', $changes['removed']);
|
||||
}
|
||||
return $parts ? ' (seats: ' . implode('; ', $parts) . ')' : '';
|
||||
}
|
||||
|
||||
/** A warning to add to the alert when a license has gone over its seats, or ''. */
|
||||
function softwareSeatWarning($software_id) {
|
||||
$row = softwareRow($software_id);
|
||||
if (!$row) {
|
||||
return '';
|
||||
}
|
||||
$seats = softwareSeats($row['seats_used'], $row['software_seats']);
|
||||
if ($seats['state'] !== 'over') {
|
||||
return '';
|
||||
}
|
||||
// Raw name - flash messages are escaped when shown (alertMessageHtml)
|
||||
return ' - <strong>' . $row['software_name'] . '</strong> is now over its seats, ' . $seats['used'] . ' of ' . $seats['total'] . ' in use';
|
||||
}
|
||||
|
||||
/** Add keys to a license, one per line of $text - blank lines and keys it already has are skipped. Returns how many were added. */
|
||||
function softwareAddKeys($software_id, $text) {
|
||||
global $mysqli;
|
||||
$software_id = intval($software_id);
|
||||
|
||||
$existing = [];
|
||||
$sql = mysqli_query($mysqli, "SELECT software_key FROM software_keys WHERE software_key_software_id = $software_id");
|
||||
while ($row = mysqli_fetch_row($sql)) {
|
||||
$existing[strtolower($row[0])] = true;
|
||||
}
|
||||
|
||||
$added = 0;
|
||||
foreach (preg_split('/\r\n|\r|\n/', (string) $text) as $line) {
|
||||
$key = mb_substr(trim($line), 0, 400);
|
||||
if ($key === '' || isset($existing[strtolower($key)])) {
|
||||
continue;
|
||||
}
|
||||
$existing[strtolower($key)] = true;
|
||||
mysqli_query($mysqli, "INSERT INTO software_keys SET software_key = '" . escapeSql($key) . "', software_key_software_id = $software_id");
|
||||
$added++;
|
||||
}
|
||||
return $added;
|
||||
}
|
||||
|
||||
/** Who did what to a license - its audit log entries, newest first. */
|
||||
function softwareHistory($software_id, $limit = 50) {
|
||||
global $mysqli;
|
||||
$software_id = intval($software_id);
|
||||
$limit = max(1, intval($limit));
|
||||
$sql = mysqli_query($mysqli, "SELECT log_action, log_description, log_created_at, user_name, user_avatar, user_id FROM logs
|
||||
LEFT JOIN users ON user_id = log_user_id
|
||||
WHERE log_type = 'Software' AND log_entity_id = $software_id
|
||||
ORDER BY log_id DESC LIMIT $limit");
|
||||
$rows = [];
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$rows[] = $row;
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/** Date from a form - Y-m-d or null. */
|
||||
function softwareFormDate($value) {
|
||||
$value = trim((string) $value);
|
||||
$date = DateTime::createFromFormat('!Y-m-d', $value);
|
||||
return $date && $date->format('Y-m-d') === $value ? $value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The license fields a New / Edit form posts, as a SQL SET list - only the
|
||||
* client's own vendor, real dates, a whole number of seats (blank is no
|
||||
* limit). Null when there is no name. Fields missing from the post are blank,
|
||||
* never a PHP warning.
|
||||
*/
|
||||
function softwareFormSet($post, $client_id) {
|
||||
$text = function ($field, $length = 200) use ($post) {
|
||||
$value = trim((string) ($post[$field] ?? ''));
|
||||
return "'" . escapeSql($length ? mb_substr($value, 0, $length) : $value) . "'";
|
||||
};
|
||||
|
||||
$name = trim((string) ($post['name'] ?? ''));
|
||||
if ($name === '') {
|
||||
return null;
|
||||
}
|
||||
$seats = trim((string) ($post['seats'] ?? ''));
|
||||
$purchase = softwareFormDate($post['purchase'] ?? '');
|
||||
$expire = softwareFormDate($post['expire'] ?? '');
|
||||
|
||||
$fields = [
|
||||
'software_name' => $text('name'),
|
||||
'software_version' => $text('version'),
|
||||
'software_description' => $text('description', 0),
|
||||
'software_type' => $text('type'),
|
||||
'software_license_type' => $text('license_type'),
|
||||
'software_key' => $text('key'),
|
||||
'software_seats' => $seats === '' ? 'NULL' : max(0, intval($seats)),
|
||||
'software_purchase_reference' => $text('purchase_reference'),
|
||||
'software_purchase' => $purchase ? "'$purchase'" : 'NULL',
|
||||
'software_expire' => $expire ? "'$expire'" : 'NULL',
|
||||
'software_notes' => $text('notes', 0),
|
||||
'software_favorite' => empty($post['favorite']) ? 0 : 1,
|
||||
'software_vendor_id' => assetClientOwnedId('vendor', $post['vendor'] ?? 0, $client_id),
|
||||
];
|
||||
$set = [];
|
||||
foreach ($fields as $column => $value) {
|
||||
$set[] = "$column = $value";
|
||||
}
|
||||
return implode(', ', $set);
|
||||
}
|
||||
|
||||
/**
|
||||
* A renewal date moved on by a term ('1m', '3m', '6m', '1y', '2y', '3y') -
|
||||
* from the current date, or from today if there is none or it has passed.
|
||||
* Null for a term it does not know.
|
||||
*/
|
||||
function softwareRenewalExtended($expire, $term) {
|
||||
$intervals = ['1m' => 'P1M', '3m' => 'P3M', '6m' => 'P6M', '1y' => 'P1Y', '2y' => 'P2Y', '3y' => 'P3Y'];
|
||||
if (!isset($intervals[$term])) {
|
||||
return null;
|
||||
}
|
||||
$today = new DateTime('today');
|
||||
$from = softwareFormDate(substr((string) $expire, 0, 10));
|
||||
$base = $from ? new DateTime($from) : $today;
|
||||
if ($base < $today) {
|
||||
$base = $today;
|
||||
}
|
||||
return $base->add(new DateInterval($intervals[$term]))->format('Y-m-d');
|
||||
}
|
||||
|
||||
/** The renewal terms the Renew modal offers. */
|
||||
const SOFTWARE_RENEWAL_TERMS = ['1m' => '1 month', '3m' => '3 months', '6m' => '6 months', '1y' => '1 year', '2y' => '2 years', '3y' => '3 years'];
|
||||
Reference in New Issue
Block a user