|
|
@@ -5,15 +5,35 @@ require_once __DIR__ . '/lib/render.php';
|
|
|
$actor = current_actor('ICG');
|
|
|
|
|
|
$job_filter = isset($_GET['job']) ? (int) $_GET['job'] : 0;
|
|
|
-$limit = isset($_GET['limit']) ? max(50, min(1000, (int) $_GET['limit'])) : 200;
|
|
|
+$q = trim((string) ($_GET['q'] ?? ''));
|
|
|
+$limit = isset($_GET['limit']) ? max(50, min(1000, (int) $_GET['limit'])) : 200;
|
|
|
+
|
|
|
+$clauses = [];
|
|
|
+$params = [];
|
|
|
|
|
|
-$where = '1=1';
|
|
|
-$params = [];
|
|
|
if ($job_filter > 0) {
|
|
|
- $where = 'h.job_id = ?';
|
|
|
- $params[] = $job_filter;
|
|
|
+ $clauses[] = 'h.job_id = ?';
|
|
|
+ $params[] = $job_filter;
|
|
|
+}
|
|
|
+
|
|
|
+if ($q !== '') {
|
|
|
+ // SQLite LIKE is case-insensitive for ASCII. Escape user wildcards so a
|
|
|
+ // literal "%" or "_" doesn't blow the search open.
|
|
|
+ $needle = '%' . strtr($q, ['\\' => '\\\\', '%' => '\\%', '_' => '\\_']) . '%';
|
|
|
+ $clauses[] =
|
|
|
+ "(j.job LIKE ? ESCAPE '\\'
|
|
|
+ OR j.material LIKE ? ESCAPE '\\'
|
|
|
+ OR j.description LIKE ? ESCAPE '\\'
|
|
|
+ OR v.name LIKE ? ESCAPE '\\'
|
|
|
+ OR h.actor LIKE ? ESCAPE '\\'
|
|
|
+ OR h.field LIKE ? ESCAPE '\\'
|
|
|
+ OR h.old_value LIKE ? ESCAPE '\\'
|
|
|
+ OR h.new_value LIKE ? ESCAPE '\\')";
|
|
|
+ array_push($params, $needle, $needle, $needle, $needle, $needle, $needle, $needle, $needle);
|
|
|
}
|
|
|
|
|
|
+$where = $clauses ? implode(' AND ', $clauses) : '1=1';
|
|
|
+
|
|
|
$sql = "
|
|
|
SELECT h.*, j.job AS job_label, j.description AS job_description,
|
|
|
v.slug AS vendor_slug, v.name AS vendor_name
|
|
|
@@ -28,6 +48,11 @@ $stmt = db()->prepare($sql);
|
|
|
$stmt->execute($params);
|
|
|
$rows = $stmt->fetchAll();
|
|
|
|
|
|
+function preserve_qs(array $keep): string {
|
|
|
+ if (!$keep) return '';
|
|
|
+ return '?' . http_build_query($keep);
|
|
|
+}
|
|
|
+
|
|
|
function fmt_long_date(?string $ts): string {
|
|
|
if (!$ts) return '';
|
|
|
try {
|
|
|
@@ -108,6 +133,32 @@ table.log th {
|
|
|
.log-filter a { color: var(--c-accent-strong); }
|
|
|
.log-desc { max-width: 24em; }
|
|
|
.log-muted { color: var(--c-muted); }
|
|
|
+
|
|
|
+.log-search {
|
|
|
+ display: flex;
|
|
|
+ gap: 0.5rem;
|
|
|
+ align-items: center;
|
|
|
+ margin: 0.75rem 0 0.5rem;
|
|
|
+}
|
|
|
+.log-search input[type="text"] {
|
|
|
+ flex: 0 1 28rem;
|
|
|
+ font-size: 1rem;
|
|
|
+ padding: 0.4rem 0.6rem;
|
|
|
+ border: 1px solid #ccc;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+.log-search input[type="text"]:focus { outline: 2px solid var(--c-info); }
|
|
|
+.log-search button {
|
|
|
+ font-size: 0.95rem;
|
|
|
+ padding: 0.45rem 1rem;
|
|
|
+ border: 0;
|
|
|
+ border-radius: 4px;
|
|
|
+ background: var(--c-accent-strong);
|
|
|
+ color: #fff;
|
|
|
+ font-weight: 600;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+.log-clear { color: var(--c-muted); font-size: 0.9rem; }
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
@@ -117,14 +168,39 @@ table.log th {
|
|
|
<span class="who"><a href="PDQ.php">← Back to schedule</a></span>
|
|
|
</div>
|
|
|
|
|
|
-<?php if ($job_filter > 0): ?>
|
|
|
+<form class="log-search" method="get" action="log.php">
|
|
|
+ <input type="text" name="q" value="<?= h($q) ?>" placeholder="Search job, material, description, actor, status…" autofocus>
|
|
|
+ <?php if ($job_filter > 0): ?>
|
|
|
+ <input type="hidden" name="job" value="<?= (int) $job_filter ?>">
|
|
|
+ <?php endif; ?>
|
|
|
+ <button type="submit">Search</button>
|
|
|
+ <?php if ($q !== '' || $job_filter > 0): ?>
|
|
|
+ <a class="log-clear" href="log.php">Clear</a>
|
|
|
+ <?php endif; ?>
|
|
|
+</form>
|
|
|
+
|
|
|
<p class="log-filter">
|
|
|
- Showing events for job #<?= (int) $job_filter ?>.
|
|
|
- <a href="log.php">Show everything →</a>
|
|
|
+ <?php
|
|
|
+ $bits = [];
|
|
|
+ if ($job_filter > 0) {
|
|
|
+ $bits[] = 'job <strong>#' . (int) $job_filter . '</strong>';
|
|
|
+ }
|
|
|
+ if ($q !== '') {
|
|
|
+ $bits[] = 'matching <strong>“' . h($q) . '”</strong>';
|
|
|
+ }
|
|
|
+ if ($bits) {
|
|
|
+ echo count($rows) . ' event' . (count($rows) === 1 ? '' : 's') . ' ' . implode(' · ', $bits) . '.';
|
|
|
+ if ($job_filter > 0) {
|
|
|
+ echo ' <a href="' . h(preserve_qs($q !== '' ? ['q' => $q] : [])) . '">drop job filter</a>';
|
|
|
+ }
|
|
|
+ if ($q !== '') {
|
|
|
+ echo ' <a href="' . h(preserve_qs($job_filter > 0 ? ['job' => $job_filter] : [])) . '">drop search</a>';
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ echo 'Latest ' . count($rows) . ' events. Click a job number to filter, or search above.';
|
|
|
+ }
|
|
|
+ ?>
|
|
|
</p>
|
|
|
-<?php else: ?>
|
|
|
-<p class="log-filter">Latest <?= count($rows) ?> events. Click a job number to filter.</p>
|
|
|
-<?php endif; ?>
|
|
|
|
|
|
<table class="log">
|
|
|
<thead>
|
|
|
@@ -148,7 +224,7 @@ table.log th {
|
|
|
<tr>
|
|
|
<td class="log-ts"><?= fmt_long_date($h['changed_at']) ?></td>
|
|
|
<td class="log-actor <?= $actorKlass ?>"><?= h($h['actor']) ?></td>
|
|
|
- <td class="log-job"><a href="?job=<?= (int) $h['job_id'] ?>"><?= $jobLabel ?></a></td>
|
|
|
+ <td class="log-job"><a href="<?= h(preserve_qs(['job' => (int) $h['job_id']] + ($q !== '' ? ['q' => $q] : []))) ?>"><?= $jobLabel ?></a></td>
|
|
|
<td class="log-desc"><?= $desc === '' ? '<span class="log-muted">—</span>' : h($desc) ?></td>
|
|
|
<td><?= fmt_event($h) ?></td>
|
|
|
</tr>
|