log.php 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  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. $limit = isset($_GET['limit']) ? max(50, min(1000, (int) $_GET['limit'])) : 200;
  7. $where = '1=1';
  8. $params = [];
  9. if ($job_filter > 0) {
  10. $where = 'h.job_id = ?';
  11. $params[] = $job_filter;
  12. }
  13. $sql = "
  14. SELECT h.*, j.job AS job_label, j.description AS job_description,
  15. v.slug AS vendor_slug, v.name AS vendor_name
  16. FROM job_history h
  17. LEFT JOIN jobs j ON j.id = h.job_id
  18. LEFT JOIN vendors v ON v.id = j.vendor_id
  19. WHERE $where
  20. ORDER BY h.id DESC
  21. LIMIT $limit
  22. ";
  23. $stmt = db()->prepare($sql);
  24. $stmt->execute($params);
  25. $rows = $stmt->fetchAll();
  26. function fmt_long_date(?string $ts): string {
  27. if (!$ts) return '';
  28. try {
  29. $dt = new DateTimeImmutable($ts . ' UTC');
  30. return $dt->setTimezone(new DateTimeZone(date_default_timezone_get()))
  31. ->format('Y-m-d g:i a');
  32. } catch (Exception $e) {
  33. return h($ts);
  34. }
  35. }
  36. function fmt_event(array $h): string {
  37. $f = $h['field'];
  38. $old = $h['old_value'];
  39. $new = $h['new_value'];
  40. if ($f === 'created') {
  41. return 'Created (vendor: ' . h((string) $new) . ')';
  42. }
  43. if ($f === 'deleted') {
  44. return 'Deleted';
  45. }
  46. if ($f === 'partial_ship') {
  47. return 'Partial ship: split off ' . h((string) $new) . ' (was qty ' . h((string) $old) . ')';
  48. }
  49. $labels = [
  50. 'ack' => 'Acknowledgement',
  51. 'status' => 'Status',
  52. 'job' => 'Job #',
  53. 'material' => 'Material',
  54. 'description' => 'Description',
  55. 'qty' => 'Qty',
  56. 'due_date' => 'Due date',
  57. ];
  58. $label = $labels[$f] ?? $f;
  59. $oldDisp = ($old === null || $old === '') ? '∅' : h((string) $old);
  60. $newDisp = ($new === null || $new === '') ? '∅' : h((string) $new);
  61. return $label . ': ' . $oldDisp . ' &rarr; ' . $newDisp;
  62. }
  63. ?><!doctype html>
  64. <html lang="en">
  65. <head>
  66. <meta charset="utf-8">
  67. <title>Activity Log</title>
  68. <link rel="stylesheet" href="assets/app.css">
  69. <style>
  70. table.log {
  71. border-collapse: collapse;
  72. width: 100%;
  73. max-width: 1100px;
  74. font-size: 0.95rem;
  75. }
  76. table.log th, table.log td {
  77. padding: 0.4rem 0.7rem;
  78. border-bottom: 1px solid #eee;
  79. text-align: left;
  80. vertical-align: top;
  81. }
  82. table.log th {
  83. background: var(--c-accent-strong);
  84. color: #fff;
  85. font-weight: 600;
  86. font-size: 0.85rem;
  87. text-transform: uppercase;
  88. letter-spacing: 0.05em;
  89. }
  90. .log-ts { white-space: nowrap; color: var(--c-muted); font-variant-numeric: tabular-nums; }
  91. .log-actor { font-weight: 600; }
  92. .log-actor-icg { color: var(--c-accent-strong); }
  93. .log-actor-vend { color: var(--c-info); }
  94. .log-job { font-weight: 600; }
  95. .log-job a { color: inherit; text-decoration: none; }
  96. .log-job a:hover { text-decoration: underline; }
  97. .log-filter {
  98. margin: 0.5rem 0 1rem;
  99. color: var(--c-muted);
  100. }
  101. .log-filter a { color: var(--c-accent-strong); }
  102. .log-desc { max-width: 24em; }
  103. .log-muted { color: var(--c-muted); }
  104. </style>
  105. </head>
  106. <body>
  107. <div class="topbar">
  108. <h1>Activity Log</h1>
  109. <span class="who"><a href="PDQ.php">&larr; Back to schedule</a></span>
  110. </div>
  111. <?php if ($job_filter > 0): ?>
  112. <p class="log-filter">
  113. Showing events for job #<?= (int) $job_filter ?>.
  114. <a href="log.php">Show everything &rarr;</a>
  115. </p>
  116. <?php else: ?>
  117. <p class="log-filter">Latest <?= count($rows) ?> events. Click a job number to filter.</p>
  118. <?php endif; ?>
  119. <table class="log">
  120. <thead>
  121. <tr>
  122. <th>When</th>
  123. <th>Who</th>
  124. <th>Job</th>
  125. <th>Description</th>
  126. <th>Event</th>
  127. </tr>
  128. </thead>
  129. <tbody>
  130. <?php if (!$rows): ?>
  131. <tr><td colspan="5" class="empty">No events recorded.</td></tr>
  132. <?php endif; ?>
  133. <?php foreach ($rows as $h):
  134. $actorKlass = $h['actor'] === 'ICG' ? 'log-actor-icg' : 'log-actor-vend';
  135. $jobLabel = $h['job_label'] !== null ? '#' . (int) $h['job_id'] . ' ' . h($h['job_label']) : '#' . (int) $h['job_id'] . ' (deleted)';
  136. $desc = (string) ($h['job_description'] ?? '');
  137. ?>
  138. <tr>
  139. <td class="log-ts"><?= fmt_long_date($h['changed_at']) ?></td>
  140. <td class="log-actor <?= $actorKlass ?>"><?= h($h['actor']) ?></td>
  141. <td class="log-job"><a href="?job=<?= (int) $h['job_id'] ?>"><?= $jobLabel ?></a></td>
  142. <td class="log-desc"><?= $desc === '' ? '<span class="log-muted">&mdash;</span>' : h($desc) ?></td>
  143. <td><?= fmt_event($h) ?></td>
  144. </tr>
  145. <?php endforeach; ?>
  146. </tbody>
  147. </table>
  148. </body>
  149. </html>