log.php 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  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 === 'partial_ship') {
  44. return 'Partial ship: split off ' . h((string) $new) . ' (was qty ' . h((string) $old) . ')';
  45. }
  46. $labels = [
  47. 'ack' => 'Acknowledgement',
  48. 'status' => 'Status',
  49. 'job' => 'Job #',
  50. 'material' => 'Material',
  51. 'description' => 'Description',
  52. 'qty' => 'Qty',
  53. 'due_date' => 'Due date',
  54. ];
  55. $label = $labels[$f] ?? $f;
  56. $oldDisp = ($old === null || $old === '') ? '∅' : h((string) $old);
  57. $newDisp = ($new === null || $new === '') ? '∅' : h((string) $new);
  58. return $label . ': ' . $oldDisp . ' &rarr; ' . $newDisp;
  59. }
  60. ?><!doctype html>
  61. <html lang="en">
  62. <head>
  63. <meta charset="utf-8">
  64. <title>Activity Log</title>
  65. <link rel="stylesheet" href="assets/app.css">
  66. <style>
  67. table.log {
  68. border-collapse: collapse;
  69. width: 100%;
  70. max-width: 1100px;
  71. font-size: 0.95rem;
  72. }
  73. table.log th, table.log td {
  74. padding: 0.4rem 0.7rem;
  75. border-bottom: 1px solid #eee;
  76. text-align: left;
  77. vertical-align: top;
  78. }
  79. table.log th {
  80. background: var(--c-accent-strong);
  81. color: #fff;
  82. font-weight: 600;
  83. font-size: 0.85rem;
  84. text-transform: uppercase;
  85. letter-spacing: 0.05em;
  86. }
  87. .log-ts { white-space: nowrap; color: var(--c-muted); font-variant-numeric: tabular-nums; }
  88. .log-actor { font-weight: 600; }
  89. .log-actor-icg { color: var(--c-accent-strong); }
  90. .log-actor-vend { color: var(--c-info); }
  91. .log-job { font-weight: 600; }
  92. .log-job a { color: inherit; text-decoration: none; }
  93. .log-job a:hover { text-decoration: underline; }
  94. .log-filter {
  95. margin: 0.5rem 0 1rem;
  96. color: var(--c-muted);
  97. }
  98. .log-filter a { color: var(--c-accent-strong); }
  99. .log-desc { max-width: 24em; }
  100. .log-muted { color: var(--c-muted); }
  101. </style>
  102. </head>
  103. <body>
  104. <div class="topbar">
  105. <h1>Activity Log</h1>
  106. <span class="who"><a href="PDQ.php">&larr; Back to schedule</a></span>
  107. </div>
  108. <?php if ($job_filter > 0): ?>
  109. <p class="log-filter">
  110. Showing events for job #<?= (int) $job_filter ?>.
  111. <a href="log.php">Show everything &rarr;</a>
  112. </p>
  113. <?php else: ?>
  114. <p class="log-filter">Latest <?= count($rows) ?> events. Click a job number to filter.</p>
  115. <?php endif; ?>
  116. <table class="log">
  117. <thead>
  118. <tr>
  119. <th>When</th>
  120. <th>Who</th>
  121. <th>Job</th>
  122. <th>Description</th>
  123. <th>Event</th>
  124. </tr>
  125. </thead>
  126. <tbody>
  127. <?php if (!$rows): ?>
  128. <tr><td colspan="5" class="empty">No events recorded.</td></tr>
  129. <?php endif; ?>
  130. <?php foreach ($rows as $h):
  131. $actorKlass = $h['actor'] === 'ICG' ? 'log-actor-icg' : 'log-actor-vend';
  132. $jobLabel = $h['job_label'] !== null ? '#' . (int) $h['job_id'] . ' ' . h($h['job_label']) : '#' . (int) $h['job_id'] . ' (deleted)';
  133. $desc = (string) ($h['job_description'] ?? '');
  134. ?>
  135. <tr>
  136. <td class="log-ts"><?= fmt_long_date($h['changed_at']) ?></td>
  137. <td class="log-actor <?= $actorKlass ?>"><?= h($h['actor']) ?></td>
  138. <td class="log-job"><a href="?job=<?= (int) $h['job_id'] ?>"><?= $jobLabel ?></a></td>
  139. <td class="log-desc"><?= $desc === '' ? '<span class="log-muted">&mdash;</span>' : h($desc) ?></td>
  140. <td><?= fmt_event($h) ?></td>
  141. </tr>
  142. <?php endforeach; ?>
  143. </tbody>
  144. </table>
  145. </body>
  146. </html>