Files
itflow/client/documents.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

133 lines
7.6 KiB
PHP

<?php
/*
* Client Portal
* Documents - the guides and records shared with the client, in tabs by
* folder, searchable by name, description and what they say, 25 a page.
* New document and Upload a file go to document_add.php.
*
* SCOPING: primary and technical contacts (portalItGate()); only documents
* marked visible to the client, keyed to $session_client_id.
*/
header("Content-Security-Policy: default-src 'self'");
require_once "includes/inc_all.php";
portalItGate();
$per_page = 25;
$q = trim((string) ($_GET['q'] ?? ''));
$visible = "document_client_id = $session_client_id AND document_client_visible = 1 AND document_archived_at IS NULL";
// The folders that hold something shared - and the documents in none
$folders = [];
$sql = mysqli_query($mysqli, "SELECT document_folder_id, COALESCE(folder_name, '') AS folder_name, COUNT(*) AS n FROM documents
LEFT JOIN folders ON folder_id = document_folder_id AND folder_client_id = $session_client_id
WHERE $visible GROUP BY document_folder_id, folder_name ORDER BY folder_name = '', folder_name ASC");
$all_count = 0;
while ($row = mysqli_fetch_assoc($sql)) {
$folders[intval($row['document_folder_id'])] = ['name' => $row['folder_name'] !== '' ? $row['folder_name'] : 'Not in a folder', 'count' => intval($row['n'])];
$all_count += intval($row['n']);
}
$folder = isset($_GET['folder']) && isset($folders[intval($_GET['folder'])]) ? intval($_GET['folder']) : null;
$where = $visible . ($folder !== null ? " AND document_folder_id = $folder" : '');
if ($q !== '') {
$q_sql = escapeSql($q);
$where .= " AND (document_name LIKE '%$q_sql%' OR document_description LIKE '%$q_sql%' OR document_content_raw LIKE '%$q_sql%')";
}
$total = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM documents WHERE $where"))[0]);
$pages = max(1, (int) ceil($total / $per_page));
$page = min(max(1, intval($_GET['page'] ?? 1)), $pages);
$offset = ($page - 1) * $per_page;
$documents = mysqli_query($mysqli, "SELECT document_id, document_name, document_description, document_created_at, document_updated_at, folder_name,
(SELECT file_ext FROM document_files JOIN files ON files.file_id = document_files.file_id WHERE document_files.document_id = documents.document_id ORDER BY files.file_id LIMIT 1) AS file_ext
FROM documents
LEFT JOIN folders ON folder_id = document_folder_id AND folder_client_id = $session_client_id
WHERE $where
ORDER BY COALESCE(document_updated_at, document_created_at) DESC, document_name ASC
LIMIT $per_page OFFSET $offset");
/** This page's address with some of its settings changed. */
function documentsUrl(array $changes) {
global $folder, $q;
$params = array_merge(['folder' => $folder, 'q' => $q !== '' ? $q : null], $changes);
$params = array_filter($params, function ($v) { return $v !== null && $v !== ''; });
return 'documents.php' . ($params ? '?' . http_build_query($params) : '');
}
?>
<div class="itflow-portal-page-head">
<div>
<h1>Documents</h1>
<p>Guides and records we share with you</p>
</div>
<div class="d-flex flex-wrap align-items-center gap-2">
<form method="get" class="d-flex" role="search">
<?php if ($folder !== null) { ?><input type="hidden" name="folder" value="<?= $folder ?>"><?php } ?>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-fw fa-search"></i></span>
<input type="search" class="form-control" name="q" value="<?= escapeHtml($q) ?>" placeholder="Search documents" aria-label="Search documents" maxlength="200">
</div>
</form>
<div class="btn-group">
<a class="btn btn-primary" href="document_add.php"><i class="fas fa-plus me-2"></i>New document</a>
<a class="btn btn-outline-primary" href="document_add.php?upload=1"><i class="fas fa-upload me-2"></i>Upload a file</a>
</div>
</div>
</div>
<?php if (count($folders) > 1) { ?>
<nav class="itflow-portal-tabs" aria-label="Folders">
<a class="<?= $folder === null ? 'active' : '' ?>" href="<?= escapeHtml(documentsUrl(['folder' => null, 'page' => null])) ?>"><i class="fas fa-list"></i>All <span class="badge rounded-pill"><?= $all_count ?></span></a>
<?php foreach ($folders as $id => $f) { ?>
<a class="<?= $folder === $id ? 'active' : '' ?>" href="<?= escapeHtml(documentsUrl(['folder' => $id, 'page' => null])) ?>"><i class="fas <?= $id ? 'fa-folder' : 'fa-file' ?>"></i><?= escapeHtml($f['name']) ?> <span class="badge rounded-pill"><?= $f['count'] ?></span></a>
<?php } ?>
</nav>
<?php } ?>
<section class="itflow-portal-panel">
<?php if (!$total) { ?>
<?php if ($q !== '') { ?>
<?= emptyStateHtml(['thing' => 'documents', 'filtered' => true, 'match_text' => 'No document mentions &ldquo;' . escapeHtml($q) . '&rdquo;.', 'clear_url' => documentsUrl(['q' => null, 'page' => null])]) ?>
<?php } else { ?>
<?= emptyStateHtml(['title' => 'No documents yet', 'text' => 'Guides and records we share with you show here - and you can add your own.', 'icon' => 'fa-folder-open', 'filtered' => false,
'actions' => [['label' => 'New document', 'href' => 'document_add.php'], ['label' => 'Upload a file', 'href' => 'document_add.php?upload=1', 'icon' => 'fa-upload']]]) ?>
<?php } ?>
<?php } else { ?>
<ul class="itflow-portal-list">
<?php while ($row = mysqli_fetch_assoc($documents)) {
$updated = $row['document_updated_at'] ?: $row['document_created_at']; ?>
<li>
<a class="itflow-portal-ticket" href="document.php?id=<?= intval($row['document_id']) ?>">
<?= portalTile($row['file_ext'] !== null ? 'fa-' . getFileIcon($row['file_ext']) : 'fa-file-alt') ?>
<span class="itflow-portal-ticket-main">
<span class="itflow-portal-ticket-subject d-block"><?= escapeHtml($row['document_name']) ?></span>
<span class="itflow-portal-ticket-meta">
<?php if (!empty($row['folder_name']) && $folder === null) { ?><span><i class="fas fa-folder me-1"></i><?= escapeHtml($row['folder_name']) ?></span><?php } ?>
<?php if (trim((string) $row['document_description']) !== '') { ?><span class="text-truncate"><?= escapeHtml($row['document_description']) ?></span><?php } ?>
</span>
</span>
<span class="itflow-portal-ticket-side">
<small title="<?= escapeHtml($updated) ?>">Updated <?= escapeHtml(date('M j, Y', strtotime($updated))) ?></small>
</span>
</a>
</li>
<?php } ?>
</ul>
<?php if ($pages > 1) { ?>
<div class="itflow-portal-pager">
<span><?= $offset + 1 ?>-<?= min($offset + $per_page, $total) ?> of <?= $total ?></span>
<div class="btn-group">
<a class="btn btn-sm btn-outline-secondary<?= $page <= 1 ? ' disabled' : '' ?>" href="<?= escapeHtml(documentsUrl(['page' => $page - 1 > 1 ? $page - 1 : null])) ?>" aria-label="Previous page"><i class="fas fa-chevron-left"></i></a>
<a class="btn btn-sm btn-outline-secondary<?= $page >= $pages ? ' disabled' : '' ?>" href="<?= escapeHtml(documentsUrl(['page' => $page + 1])) ?>" aria-label="Next page"><i class="fas fa-chevron-right"></i></a>
</div>
</div>
<?php } ?>
<?php } ?>
</section>
<?php
require_once "includes/footer.php";