log.php 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. <?php
  2. require_once __DIR__ . '/lib/identity.php';
  3. require_once __DIR__ . '/lib/render.php';
  4. $actor = current_actor('ICG');
  5. $job_filter = isset($_GET['job']) ? (int) $_GET['job'] : 0;
  6. $q = trim((string) ($_GET['q'] ?? ''));
  7. $limit = isset($_GET['limit']) ? max(50, min(1000, (int) $_GET['limit'])) : 200;
  8. $clauses = [];
  9. $params = [];
  10. if ($job_filter > 0) {
  11. $clauses[] = 'h.job_id = ?';
  12. $params[] = $job_filter;
  13. }
  14. if ($q !== '') {
  15. // SQLite LIKE is case-insensitive for ASCII. Escape user wildcards so a
  16. // literal "%" or "_" doesn't blow the search open.
  17. $needle = '%' . strtr($q, ['\\' => '\\\\', '%' => '\\%', '_' => '\\_']) . '%';
  18. $clauses[] =
  19. "(j.job LIKE ? ESCAPE '\\'
  20. OR j.material LIKE ? ESCAPE '\\'
  21. OR j.description LIKE ? ESCAPE '\\'
  22. OR v.name LIKE ? ESCAPE '\\'
  23. OR h.actor LIKE ? ESCAPE '\\'
  24. OR h.field LIKE ? ESCAPE '\\'
  25. OR h.old_value LIKE ? ESCAPE '\\'
  26. OR h.new_value LIKE ? ESCAPE '\\')";
  27. array_push($params, $needle, $needle, $needle, $needle, $needle, $needle, $needle, $needle);
  28. }
  29. $where = $clauses ? implode(' AND ', $clauses) : '1=1';
  30. $sql = "
  31. SELECT h.*, j.job AS job_label, j.description AS job_description,
  32. v.slug AS vendor_slug, v.name AS vendor_name
  33. FROM job_history h
  34. LEFT JOIN jobs j ON j.id = h.job_id
  35. LEFT JOIN vendors v ON v.id = j.vendor_id
  36. WHERE $where
  37. ORDER BY h.id DESC
  38. LIMIT $limit
  39. ";
  40. $stmt = db()->prepare($sql);
  41. $stmt->execute($params);
  42. $rows = $stmt->fetchAll();
  43. function preserve_qs(array $keep): string {
  44. if (!$keep) return '';
  45. return '?' . http_build_query($keep);
  46. }
  47. function fmt_long_date(?string $ts): string {
  48. if (!$ts) return '';
  49. try {
  50. $dt = new DateTimeImmutable($ts . ' UTC');
  51. return $dt->setTimezone(new DateTimeZone(date_default_timezone_get()))
  52. ->format('Y-m-d g:i a');
  53. } catch (Exception $e) {
  54. return h($ts);
  55. }
  56. }
  57. function fmt_event(array $h): string {
  58. $f = $h['field'];
  59. $old = $h['old_value'];
  60. $new = $h['new_value'];
  61. if ($f === 'created') {
  62. return 'Created (vendor: ' . h((string) $new) . ')';
  63. }
  64. if ($f === 'deleted') {
  65. return 'Deleted';
  66. }
  67. if ($f === 'partial_ship') {
  68. return 'Partial ship: split off ' . h((string) $new) . ' (was qty ' . h((string) $old) . ')';
  69. }
  70. $labels = [
  71. 'ack' => 'Acknowledgement',
  72. 'status' => 'Status',
  73. 'job' => 'Job #',
  74. 'material' => 'Material',
  75. 'description' => 'Description',
  76. 'qty' => 'Qty',
  77. 'due_date' => 'Due date',
  78. ];
  79. $label = $labels[$f] ?? $f;
  80. $oldDisp = ($old === null || $old === '') ? '∅' : h((string) $old);
  81. $newDisp = ($new === null || $new === '') ? '∅' : h((string) $new);
  82. return $label . ': ' . $oldDisp . ' &rarr; ' . $newDisp;
  83. }
  84. ?><!doctype html>
  85. <html lang="en">
  86. <head>
  87. <meta charset="utf-8">
  88. <title>Activity Log</title>
  89. <link rel="stylesheet" href="assets/app.css">
  90. <style>
  91. table.log {
  92. border-collapse: collapse;
  93. width: 100%;
  94. max-width: 1100px;
  95. font-size: 0.95rem;
  96. }
  97. table.log th, table.log td {
  98. padding: 0.4rem 0.7rem;
  99. border-bottom: 1px solid #eee;
  100. text-align: left;
  101. vertical-align: top;
  102. }
  103. table.log th {
  104. background: var(--c-accent-strong);
  105. color: #fff;
  106. font-weight: 600;
  107. font-size: 0.85rem;
  108. text-transform: uppercase;
  109. letter-spacing: 0.05em;
  110. }
  111. .log-ts { white-space: nowrap; color: var(--c-muted); font-variant-numeric: tabular-nums; }
  112. .log-actor { font-weight: 600; }
  113. .log-actor-icg { color: var(--c-accent-strong); }
  114. .log-actor-vend { color: var(--c-info); }
  115. .log-job { font-weight: 600; }
  116. .log-job a { color: inherit; text-decoration: none; }
  117. .log-job a:hover { text-decoration: underline; }
  118. .log-filter {
  119. margin: 0.5rem 0 1rem;
  120. color: var(--c-muted);
  121. }
  122. .log-filter a { color: var(--c-accent-strong); }
  123. .log-desc { max-width: 24em; }
  124. .log-muted { color: var(--c-muted); }
  125. .log-search {
  126. display: flex;
  127. gap: 0.5rem;
  128. align-items: center;
  129. margin: 0.75rem 0 0.5rem;
  130. }
  131. .log-search input[type="text"] {
  132. flex: 0 1 28rem;
  133. font-size: 1rem;
  134. padding: 0.4rem 0.6rem;
  135. border: 1px solid #ccc;
  136. border-radius: 4px;
  137. }
  138. .log-search input[type="text"]:focus { outline: 2px solid var(--c-info); }
  139. .log-search button {
  140. font-size: 0.95rem;
  141. padding: 0.45rem 1rem;
  142. border: 0;
  143. border-radius: 4px;
  144. background: var(--c-accent-strong);
  145. color: #fff;
  146. font-weight: 600;
  147. cursor: pointer;
  148. }
  149. .log-clear { color: var(--c-muted); font-size: 0.9rem; }
  150. </style>
  151. </head>
  152. <body>
  153. <div class="topbar">
  154. <h1>Activity Log</h1>
  155. <span class="who"><a href="PDQ.php">&larr; Back to schedule</a></span>
  156. </div>
  157. <form class="log-search" method="get" action="log.php">
  158. <input type="text" name="q" value="<?= h($q) ?>" placeholder="Search job, material, description, actor, status…" autofocus>
  159. <?php if ($job_filter > 0): ?>
  160. <input type="hidden" name="job" value="<?= (int) $job_filter ?>">
  161. <?php endif; ?>
  162. <button type="submit">Search</button>
  163. <?php if ($q !== '' || $job_filter > 0): ?>
  164. <a class="log-clear" href="log.php">Clear</a>
  165. <?php endif; ?>
  166. </form>
  167. <p class="log-filter">
  168. <?php
  169. $bits = [];
  170. if ($job_filter > 0) {
  171. $bits[] = 'job <strong>#' . (int) $job_filter . '</strong>';
  172. }
  173. if ($q !== '') {
  174. $bits[] = 'matching <strong>&ldquo;' . h($q) . '&rdquo;</strong>';
  175. }
  176. if ($bits) {
  177. echo count($rows) . ' event' . (count($rows) === 1 ? '' : 's') . ' ' . implode(' &middot; ', $bits) . '.';
  178. if ($job_filter > 0) {
  179. echo ' <a href="' . h(preserve_qs($q !== '' ? ['q' => $q] : [])) . '">drop job filter</a>';
  180. }
  181. if ($q !== '') {
  182. echo ' <a href="' . h(preserve_qs($job_filter > 0 ? ['job' => $job_filter] : [])) . '">drop search</a>';
  183. }
  184. } else {
  185. echo 'Latest ' . count($rows) . ' events. Click a job number to filter, or search above.';
  186. }
  187. ?>
  188. </p>
  189. <table class="log">
  190. <thead>
  191. <tr>
  192. <th>When</th>
  193. <th>Who</th>
  194. <th>Job</th>
  195. <th>Description</th>
  196. <th>Event</th>
  197. </tr>
  198. </thead>
  199. <tbody>
  200. <?php if (!$rows): ?>
  201. <tr><td colspan="5" class="empty">No events recorded.</td></tr>
  202. <?php endif; ?>
  203. <?php foreach ($rows as $h):
  204. $actorKlass = $h['actor'] === 'ICG' ? 'log-actor-icg' : 'log-actor-vend';
  205. $jobLabel = $h['job_label'] !== null ? '#' . (int) $h['job_id'] . ' ' . h($h['job_label']) : '#' . (int) $h['job_id'] . ' (deleted)';
  206. $desc = (string) ($h['job_description'] ?? '');
  207. ?>
  208. <tr>
  209. <td class="log-ts"><?= fmt_long_date($h['changed_at']) ?></td>
  210. <td class="log-actor <?= $actorKlass ?>"><?= h($h['actor']) ?></td>
  211. <td class="log-job"><a href="<?= h(preserve_qs(['job' => (int) $h['job_id']] + ($q !== '' ? ['q' => $q] : []))) ?>"><?= $jobLabel ?></a></td>
  212. <td class="log-desc"><?= $desc === '' ? '<span class="log-muted">&mdash;</span>' : h($desc) ?></td>
  213. <td><?= fmt_event($h) ?></td>
  214. </tr>
  215. <?php endforeach; ?>
  216. </tbody>
  217. </table>
  218. </body>
  219. </html>