Files
itflow/client/asset.php
johnnyq 5e77edb077 Client portal stage 3: Your IT - Devices with tabs by kind (Computers, Servers, Network, Phones and tablets, Other) with counts, an Everyone's or Mine filter, search and warranty badges when expired or expiring; a device page with make and model, serial, OS, user, location, purchase date and age, warranty, status, open tickets and licenses on it, Report a problem opening a new ticket with the device picked and plain contacts reaching their own devices from home; Licenses with All, Renewing soon and Expired tabs showing seats in use and renewal state without license keys; Domains and certificates on one page with registrar, DNS, website and email vendors, expiry and auto-renew, and a certificates tab with short-lived certificates shown as renewing themselves, the old certificates page redirecting there; Documents as a help library of client-visible documents with folder tabs and full-text search, a document page keeping its own tables and colours under a widened style policy, files to open or download with the first PDF or image shown inline, Print and Ask us about it, and new documents written in the editor or uploaded as a file with an agent notification; Contacts listing everyone primary first with roles and search, and the primary contact alone adding and editing contacts, giving portal access with an emailed set-password or Microsoft 365 invite, resending it, turning access off and marking someone as left, which turns off access and opens an offboarding ticket, all audited and notified; home renewals and devices linking to their pages; fixes a technical contact being able to change the primary contact's login email, revoked contacts staying signed in, portal documents recording the contact id as author, the upload stub going in unescaped and gated pages leaving a bare header; check_login ends sessions without a sign-in method, header.php buffers output so gates redirect properly, enforceContactCan sends contacts home with a message instead of logging them out, add_contact and edit_contact replaced by portal_contact_save, portal_contact_access and portal_contact_left, new Your IT helpers in client/functions.php and portal tile, roles, PDF and document styles; changelog updated
2026-10-17 02:40:59 -04:00

150 lines
8.0 KiB
PHP

<?php
/*
* Client Portal
* A device - what it is, who uses it and where, its warranty, the licenses
* on it and its open tickets, with Report a problem. A primary or technical
* contact opens any of their company's devices; anyone else the ones they use.
*
* SCOPING: portalAssetScopeSql().
*/
header("Content-Security-Policy: default-src 'self'");
require_once "includes/inc_all.php";
$can_it = $config_module_enable_itdoc && contactCan('itdoc');
$back = $can_it ? ['assets.php', 'Devices'] : ['index.php', 'Home'];
$asset_id = intval($_GET['id'] ?? 0);
$asset = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT asset_id, asset_name, asset_description, asset_type, asset_make, asset_model, asset_serial, asset_os,
asset_status, asset_purchase_date, asset_warranty_expire, asset_install_date, asset_uri_client, asset_contact_id, contact_name, location_name
FROM assets
LEFT JOIN contacts ON contact_id = asset_contact_id AND contact_archived_at IS NULL
LEFT JOIN locations ON location_id = asset_location_id AND location_archived_at IS NULL
WHERE asset_id = $asset_id AND " . portalAssetScopeSql() . " LIMIT 1"));
if (!$asset) { ?>
<a class="itflow-portal-back" href="<?= $back[0] ?>"><i class="fas fa-chevron-left"></i><?= $back[1] ?></a>
<section class="itflow-portal-panel">
<?= emptyStateHtml(['title' => "That device isn't here", 'text' => 'It may have been retired, or it is not one you can see.', 'icon' => 'fa-desktop', 'filtered' => false,
'actions' => [['label' => $back[1], 'href' => $back[0], 'icon' => 'fa-arrow-left']]]) ?>
</section>
<?php
require_once "includes/footer.php";
exit;
}
$warranty = assetWarranty($asset['asset_warranty_expire']);
$what = trim($asset['asset_make'] . ' ' . $asset['asset_model']);
$link = escapeUrl($asset['asset_uri_client']);
$licenses = $can_it ? softwareHeldBy('asset', $asset_id) : [];
// Its open tickets - the ones this contact may see
$tickets = mysqli_query($mysqli, "SELECT " . portalTicketColumnsSql() . " FROM tickets
LEFT JOIN ticket_statuses ON ticket_status = ticket_status_id
WHERE " . portalTicketScopeSql() . " AND tickets.ticket_archived_at IS NULL AND " . TICKET_OPEN_SQL . "
AND (tickets.ticket_asset_id = $asset_id OR EXISTS (SELECT 1 FROM ticket_assets WHERE ticket_assets.ticket_id = tickets.ticket_id AND ticket_assets.asset_id = $asset_id))
ORDER BY COALESCE(tickets.ticket_updated_at, tickets.ticket_created_at) DESC LIMIT 10");
$facts = array_filter([
'Kind' => escapeHtml($asset['asset_type']),
'Make and model' => escapeHtml($what),
'Serial number' => $asset['asset_serial'] !== '' && $asset['asset_serial'] !== null ? '<span class="font-monospace">' . escapeHtml($asset['asset_serial']) . '</span>' : '',
'Operating system' => escapeHtml($asset['asset_os']),
'Used by' => !empty($asset['contact_name']) ? escapeHtml($asset['contact_name']) . (intval($asset['asset_contact_id']) === $session_contact_id ? ' (you)' : '') : '',
'Location' => escapeHtml($asset['location_name'] ?? ''),
'Purchased' => !empty($asset['asset_purchase_date']) ? escapeHtml(date('M j, Y', strtotime($asset['asset_purchase_date']))) . (assetAge($asset['asset_purchase_date']) !== '' ? ' <span class="text-body-secondary">(' . escapeHtml(assetAge($asset['asset_purchase_date'])) . ')</span>' : '') : '',
'Warranty' => $warranty ? portalStateBadge(['label' => $warranty['text'], 'icon' => 'fa-shield-alt', 'class' => ['danger' => 'itflow-portal-state-late', 'warning' => 'itflow-portal-state-you', 'success' => 'itflow-portal-state-resolved'][$warranty['tone']]]) : '',
'Status' => escapeHtml($asset['asset_status']),
], function ($value) {
return trim((string) $value) !== '';
});
?>
<a class="itflow-portal-back" href="<?= $back[0] ?>"><i class="fas fa-chevron-left"></i><?= $back[1] ?></a>
<div class="itflow-portal-page-head">
<div class="d-flex align-items-center gap-3 min-w-0">
<?= portalTile('fa-' . getAssetIcon($asset['asset_type']), 'lg') ?>
<div class="min-w-0">
<h1 class="text-truncate"><?= escapeHtml($asset['asset_name']) ?></h1>
<p><?= escapeHtml(implode(' · ', array_filter([$asset['asset_type'], $what]))) ?></p>
</div>
</div>
<div class="d-flex flex-wrap gap-2">
<?php if ($link) { ?>
<a class="btn btn-outline-secondary" href="<?= $link ?>" target="_blank" rel="noopener noreferrer"><i class="fas fa-external-link-alt me-2"></i>Open</a>
<?php } ?>
<a class="btn btn-primary" href="ticket_add.php?asset=<?= $asset_id ?>"><i class="fas fa-life-ring me-2"></i>Report a problem</a>
</div>
</div>
<div class="row g-4">
<div class="col-lg-7">
<section class="itflow-portal-panel">
<div class="itflow-portal-panel-head">
<h2><i class="fas fa-info-circle"></i>Details</h2>
</div>
<div class="itflow-portal-panel-body">
<?php if (trim((string) $asset['asset_description']) !== '') { ?>
<p class="mb-3"><?= escapeHtml($asset['asset_description']) ?></p>
<?php } ?>
<dl class="itflow-portal-facts">
<?php foreach ($facts as $label => $value) { ?>
<div><dt><?= $label ?></dt><dd><?= $value ?></dd></div>
<?php } ?>
</dl>
</div>
</section>
</div>
<div class="col-lg-5">
<section class="itflow-portal-panel mb-4">
<div class="itflow-portal-panel-head">
<h2><i class="fas fa-life-ring"></i>Open tickets about it</h2>
</div>
<?php if (!mysqli_num_rows($tickets)) { ?>
<?= emptyStateHtml(['title' => 'Nothing open', 'text' => 'Something wrong with it? Report a problem and we will pick it up.', 'icon' => 'fa-check', 'filtered' => false]) ?>
<?php } else { ?>
<ul class="itflow-portal-list">
<?php while ($row = mysqli_fetch_assoc($tickets)) { ?>
<li>
<a class="itflow-portal-ticket" href="ticket.php?id=<?= intval($row['ticket_id']) ?>">
<span class="itflow-portal-ticket-main">
<span class="itflow-portal-ticket-subject d-block"><?= escapeHtml($row['ticket_subject']) ?></span>
<span class="itflow-portal-ticket-meta"><span><?= escapeHtml($row['ticket_prefix'] . $row['ticket_number']) ?></span></span>
</span>
<span class="itflow-portal-ticket-side"><?= portalTicketStateHtml($row) ?></span>
</a>
</li>
<?php } ?>
</ul>
<?php } ?>
</section>
<?php if ($licenses) { ?>
<section class="itflow-portal-panel">
<div class="itflow-portal-panel-head">
<h2><i class="fas fa-cube"></i>Licenses on it</h2>
<a href="licenses.php">All licenses</a>
</div>
<ul class="itflow-portal-list">
<?php foreach ($licenses as $license) {
$renewal = portalRenewalState(softwareRenewal($license['software_expire']), 'Active'); ?>
<li class="itflow-portal-todo">
<span class="itflow-portal-todo-main">
<div><?= escapeHtml(trim($license['software_name'] . ' ' . $license['software_version'])) ?></div>
<div><?= escapeHtml(implode(' · ', array_filter([$license['software_type'], $license['software_license_type'], $license['vendor_name']]))) ?></div>
</span>
<?= $renewal ? portalStateBadge($renewal) : '' ?>
</li>
<?php } ?>
</ul>
</section>
<?php } ?>
</div>
</div>
<?php
require_once "includes/footer.php";