فهرست منبع

added activity log

Bernn 1 هفته پیش
والد
کامیت
77a0b6782f
6فایلهای تغییر یافته به همراه183 افزوده شده و 9 حذف شده
  1. 5 1
      PDQ.php
  2. 1 0
      assets/app.css
  3. 4 0
      bin/jobs_add.php
  4. 9 1
      bin/jobs_partial_ship.php
  5. 7 7
      lib/jobs.php
  6. 157 0
      log.php

+ 5 - 1
PDQ.php

@@ -24,7 +24,11 @@ window.PDQ = {
 
 <div class="topbar">
     <h1>PDQ Schedule</h1>
-    <span class="who">Signed in as <strong>ICG</strong> &middot; <span id="sync-time">Loading…</span></span>
+    <span class="who">
+        <a href="log.php">Activity log</a>
+        &middot; Signed in as <strong>ICG</strong>
+        &middot; <span id="sync-time">Loading…</span>
+    </span>
 </div>
 
 <div class="add-row">

+ 1 - 0
assets/app.css

@@ -128,6 +128,7 @@ input.inline-edit {
     border-radius: 6px;
     margin-bottom: 1.5rem;
     background: #fff;
+    max-width: 720px;
 }
 .thread h2 {
     margin: 0;

+ 4 - 0
bin/jobs_add.php

@@ -23,6 +23,10 @@ $stmt = $pdo->prepare(
 $stmt->execute([$vendor['id']]);
 $new_id = (int) $pdo->lastInsertId();
 
+$pdo->prepare(
+    'INSERT INTO job_history(job_id, field, old_value, new_value, actor) VALUES (?, ?, ?, ?, ?)'
+)->execute([$new_id, 'created', null, $vendor['slug'], $actor]);
+
 if (!empty($_POST['ajax'])) {
     echo $new_id;
 } else {

+ 9 - 1
bin/jobs_partial_ship.php

@@ -81,13 +81,21 @@ try {
     );
     $upd->execute([$remaining, $job_id]);
 
-    // Audit trail: new row's status, and the qty deduction on the original.
+    // Audit trail covering both rows so the log page can reconstruct the split.
     $hist = $pdo->prepare(
         'INSERT INTO job_history (job_id, field, old_value, new_value, actor)
          VALUES (?, ?, ?, ?, ?)'
     );
+    // The new row was created (this is its first appearance). new_value is the
+    // vendor slug, matching the convention from bin/jobs_add.php. The actor is
+    // the vendor performing the partial ship, so it doubles as the vendor slug.
+    $hist->execute([$new_id, 'created', null, $actor, $actor]);
+    // The new row starts Shipped (transition from default '').
     $hist->execute([$new_id, 'status', '',                   'Shipped',           $actor]);
+    // The original row lost qty.
     $hist->execute([$job_id, 'qty',    (string) $current_qty, (string) $remaining, $actor]);
+    // A linking marker so the log can show "split off N units into job #X".
+    $hist->execute([$job_id, 'partial_ship', (string) $current_qty, (string) $partial . ' -> #' . $new_id, $actor]);
 
     $pdo->exec('COMMIT');
     echo 'Success';

+ 7 - 7
lib/jobs.php

@@ -27,13 +27,13 @@ function apply_job_change(array $job, string $col, $new, string $actor): void {
         $stmt = $pdo->prepare("UPDATE jobs SET $col = ?, updated_at = datetime('now') WHERE id = ?");
         $stmt->execute([$new, $job['id']]);
 
-        if (in_array($col, ['status', 'ack'], true)) {
-            $hist = $pdo->prepare(
-                'INSERT INTO job_history(job_id, field, old_value, new_value, actor)
-                 VALUES (?, ?, ?, ?, ?)'
-            );
-            $hist->execute([$job['id'], $col, (string) $old, (string) $new, $actor]);
-        }
+        // Every field change is audited — the log page reads this table.
+        $hist = $pdo->prepare(
+            'INSERT INTO job_history(job_id, field, old_value, new_value, actor)
+             VALUES (?, ?, ?, ?, ?)'
+        );
+        $hist->execute([$job['id'], $col, (string) $old, (string) $new, $actor]);
+
         $pdo->exec('COMMIT');
     } catch (Throwable $e) {
         $pdo->exec('ROLLBACK');

+ 157 - 0
log.php

@@ -0,0 +1,157 @@
+<?php
+require_once __DIR__ . '/lib/identity.php';
+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;
+
+$where = '1=1';
+$params = [];
+if ($job_filter > 0) {
+    $where = 'h.job_id = ?';
+    $params[] = $job_filter;
+}
+
+$sql = "
+    SELECT h.*, j.job AS job_label, j.description AS job_description,
+           v.slug AS vendor_slug, v.name AS vendor_name
+    FROM job_history h
+    LEFT JOIN jobs    j ON j.id = h.job_id
+    LEFT JOIN vendors v ON v.id = j.vendor_id
+    WHERE $where
+    ORDER BY h.id DESC
+    LIMIT $limit
+";
+$stmt = db()->prepare($sql);
+$stmt->execute($params);
+$rows = $stmt->fetchAll();
+
+function fmt_long_date(?string $ts): string {
+    if (!$ts) return '';
+    try {
+        $dt = new DateTimeImmutable($ts . ' UTC');
+        return $dt->setTimezone(new DateTimeZone(date_default_timezone_get()))
+                  ->format('Y-m-d g:i a');
+    } catch (Exception $e) {
+        return h($ts);
+    }
+}
+
+function fmt_event(array $h): string {
+    $f   = $h['field'];
+    $old = $h['old_value'];
+    $new = $h['new_value'];
+
+    if ($f === 'created') {
+        return 'Created (vendor: ' . h((string) $new) . ')';
+    }
+    if ($f === 'partial_ship') {
+        return 'Partial ship: split off ' . h((string) $new) . ' (was qty ' . h((string) $old) . ')';
+    }
+    $labels = [
+        'ack'         => 'Acknowledgement',
+        'status'      => 'Status',
+        'job'         => 'Job #',
+        'material'    => 'Material',
+        'description' => 'Description',
+        'qty'         => 'Qty',
+        'due_date'    => 'Due date',
+    ];
+    $label = $labels[$f] ?? $f;
+    $oldDisp = ($old === null || $old === '') ? '∅' : h((string) $old);
+    $newDisp = ($new === null || $new === '') ? '∅' : h((string) $new);
+    return $label . ': ' . $oldDisp . ' &rarr; ' . $newDisp;
+}
+?><!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Activity Log</title>
+<link rel="stylesheet" href="assets/app.css">
+<style>
+table.log {
+    border-collapse: collapse;
+    width: 100%;
+    max-width: 1100px;
+    font-size: 0.95rem;
+}
+table.log th, table.log td {
+    padding: 0.4rem 0.7rem;
+    border-bottom: 1px solid #eee;
+    text-align: left;
+    vertical-align: top;
+}
+table.log th {
+    background: var(--c-accent-strong);
+    color: #fff;
+    font-weight: 600;
+    font-size: 0.85rem;
+    text-transform: uppercase;
+    letter-spacing: 0.05em;
+}
+.log-ts     { white-space: nowrap; color: var(--c-muted); font-variant-numeric: tabular-nums; }
+.log-actor  { font-weight: 600; }
+.log-actor-icg  { color: var(--c-accent-strong); }
+.log-actor-vend { color: var(--c-info); }
+.log-job   { font-weight: 600; }
+.log-job a { color: inherit; text-decoration: none; }
+.log-job a:hover { text-decoration: underline; }
+.log-filter {
+    margin: 0.5rem 0 1rem;
+    color: var(--c-muted);
+}
+.log-filter a { color: var(--c-accent-strong); }
+.log-desc  { max-width: 24em; }
+.log-muted { color: var(--c-muted); }
+</style>
+</head>
+<body>
+
+<div class="topbar">
+    <h1>Activity Log</h1>
+    <span class="who"><a href="PDQ.php">&larr; Back to schedule</a></span>
+</div>
+
+<?php if ($job_filter > 0): ?>
+<p class="log-filter">
+    Showing events for job #<?= (int) $job_filter ?>.
+    <a href="log.php">Show everything &rarr;</a>
+</p>
+<?php else: ?>
+<p class="log-filter">Latest <?= count($rows) ?> events. Click a job number to filter.</p>
+<?php endif; ?>
+
+<table class="log">
+    <thead>
+    <tr>
+        <th>When</th>
+        <th>Who</th>
+        <th>Job</th>
+        <th>Description</th>
+        <th>Event</th>
+    </tr>
+    </thead>
+    <tbody>
+    <?php if (!$rows): ?>
+        <tr><td colspan="5" class="empty">No events recorded.</td></tr>
+    <?php endif; ?>
+    <?php foreach ($rows as $h):
+        $actorKlass = $h['actor'] === 'ICG' ? 'log-actor-icg' : 'log-actor-vend';
+        $jobLabel = $h['job_label'] !== null ? '#' . (int) $h['job_id'] . ' ' . h($h['job_label']) : '#' . (int) $h['job_id'] . ' (deleted)';
+        $desc = (string) ($h['job_description'] ?? '');
+    ?>
+        <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-desc"><?= $desc === '' ? '<span class="log-muted">&mdash;</span>' : h($desc) ?></td>
+            <td><?= fmt_event($h) ?></td>
+        </tr>
+    <?php endforeach; ?>
+    </tbody>
+</table>
+
+</body>
+</html>