render.php 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. <?php
  2. require_once __DIR__ . '/db.php';
  3. function h(?string $s): string {
  4. return htmlspecialchars($s ?? '', ENT_QUOTES, 'UTF-8');
  5. }
  6. // Render the job table for a given audience.
  7. // $audience: 'ICG' or 'vendor'
  8. // $vendor_id: required when $audience === 'vendor'
  9. function render_jobs_table(string $audience, ?int $vendor_id = null): string {
  10. $pdo = db();
  11. // Filter matches the original: open jobs OR anything touched in the last 4 days.
  12. $where = "(j.status != 'Received') OR (j.updated_at > datetime('now', '-4 days'))";
  13. $params = [];
  14. if ($audience === 'vendor') {
  15. $where = "j.vendor_id = ? AND ($where)";
  16. $params[] = $vendor_id;
  17. }
  18. $sql = "
  19. SELECT j.*, v.slug AS vendor_slug, v.name AS vendor_name,
  20. CASE WHEN j.due_date IS NULL THEN ''
  21. ELSE CAST(CAST(strftime('%m', j.due_date) AS INTEGER) AS TEXT) || '-' ||
  22. CAST(CAST(strftime('%d', j.due_date) AS INTEGER) AS TEXT)
  23. END AS due_short,
  24. (SELECT MAX(changed_at) FROM job_history
  25. WHERE job_id = j.id AND field = 'status' AND new_value = 'Finished') AS finished_at,
  26. (SELECT MAX(changed_at) FROM job_history
  27. WHERE job_id = j.id AND field = 'status' AND new_value = 'Shipped') AS shipped_at,
  28. (SELECT MAX(changed_at) FROM job_history
  29. WHERE job_id = j.id AND field = 'status' AND new_value = 'Received') AS received_at
  30. FROM jobs j
  31. JOIN vendors v ON v.id = j.vendor_id
  32. WHERE $where
  33. ORDER BY (j.ack != '') DESC,
  34. (j.status = '') DESC,
  35. (j.status = 'Finished') DESC,
  36. (j.status = 'Shipped') DESC,
  37. j.due_date IS NULL,
  38. j.due_date ASC,
  39. j.job
  40. ";
  41. $stmt = $pdo->prepare($sql);
  42. $stmt->execute($params);
  43. $rows = $stmt->fetchAll();
  44. ob_start();
  45. ?>
  46. <table class="jobs">
  47. <thead>
  48. <tr>
  49. <th>Job</th>
  50. <th>Material</th>
  51. <th>Description</th>
  52. <th class="right">Qty</th>
  53. <th class="center">Due</th>
  54. <?php if ($audience === 'ICG'): ?><th>Vendor</th><?php endif; ?>
  55. <th class="center">Ack</th>
  56. <th class="center">Status</th>
  57. <th class="center">Action</th>
  58. </tr>
  59. </thead>
  60. <tbody>
  61. <?php if (!$rows): ?>
  62. <tr><td colspan="<?= $audience === 'ICG' ? 9 : 8 ?>" class="empty">No active jobs.</td></tr>
  63. <?php endif; ?>
  64. <?php foreach ($rows as $r):
  65. $editable = ($audience === 'ICG');
  66. $jobClass = strlen($r['job']) > 9 ? 'smaller' : '';
  67. $statusClass = $r['status'] !== '' ? 'status-' . strtolower($r['status']) : '';
  68. $ackClass = '';
  69. $ackLabel = '';
  70. if ($r['ack'] === 'new') { $ackClass = 'ack-new'; $ackLabel = 'NEW'; }
  71. elseif ($r['ack'] === 'changed') { $ackClass = 'ack-changed'; $ackLabel = 'CHANGED'; }
  72. ?>
  73. <tr data-job-id="<?= (int) $r['id'] ?>" class="<?= h($statusClass) ?>">
  74. <td class="<?= h($jobClass) ?>"><?= render_field($r, 'job', $editable) ?></td>
  75. <td><?= render_field($r, 'material', $editable) ?></td>
  76. <td class="center"><?= render_field($r, 'description', $editable) ?></td>
  77. <td class="right"><?= render_field($r, 'qty', $editable) ?></td>
  78. <td class="center"><?= render_due($r, $editable) ?></td>
  79. <?php if ($audience === 'ICG'): ?><td><?= h($r['vendor_name']) ?></td><?php endif; ?>
  80. <td class="center <?= h($ackClass) ?>"><?= h($ackLabel) ?></td>
  81. <td class="center"><?= render_status($r, $audience) ?></td>
  82. <td class="center actions"><?= render_actions($r, $audience) ?></td>
  83. </tr>
  84. <?php endforeach; ?>
  85. </tbody>
  86. </table>
  87. <?php
  88. return ob_get_clean();
  89. }
  90. function render_field(array $r, string $col, bool $editable): string {
  91. $val = (string) ($r[$col] ?? '');
  92. if (!$editable) {
  93. return h($val);
  94. }
  95. return '<span class="editable" data-column="' . h($col) . '" tabindex="0">'
  96. . ($val === '' ? '&mdash;' : h($val))
  97. . '</span>';
  98. }
  99. function render_due(array $r, bool $editable): string {
  100. $display = $r['due_short'] !== '' ? h($r['due_short']) : '&mdash;';
  101. if (!$editable) return $display;
  102. return '<span class="editable" data-column="due_date" data-raw="' . h($r['due_date']) . '" tabindex="0">'
  103. . $display . '</span>';
  104. }
  105. function render_status(array $r, string $audience): string {
  106. $out = h($r['status']);
  107. $stamp = null;
  108. if ($audience === 'ICG') {
  109. if ($r['status'] === 'Finished') $stamp = $r['finished_at'] ?? null;
  110. elseif ($r['status'] === 'Shipped') $stamp = $r['shipped_at'] ?? null;
  111. } else { // vendor
  112. if ($r['status'] === 'Received') $stamp = $r['received_at'] ?? null;
  113. }
  114. if ($stamp) {
  115. $out .= '<div class="status-date">' . h(format_short_date($stamp)) . '</div>';
  116. }
  117. return $out;
  118. }
  119. // Convert a UTC 'YYYY-MM-DD HH:MM:SS' from datetime('now') into 'M/D' in the
  120. // server's local timezone for display.
  121. function format_short_date(?string $ts): string {
  122. if (!$ts) return '';
  123. try {
  124. $dt = new DateTimeImmutable($ts . ' UTC');
  125. return $dt->setTimezone(new DateTimeZone(date_default_timezone_get()))->format('n/j');
  126. } catch (Exception $e) {
  127. return '';
  128. }
  129. }
  130. function render_actions(array $r, string $audience): string {
  131. $btns = [];
  132. if ($audience === 'vendor') {
  133. if ($r['ack'] === 'new' || $r['ack'] === 'changed') {
  134. $btns[] = '<button class="btn btn-ack" data-action="acknowledge">Acknowledge</button>';
  135. }
  136. if ($r['status'] === '') {
  137. $btns[] = '<button class="btn btn-finish" data-action="mark_finished">Mark Finished</button>';
  138. } elseif ($r['status'] === 'Finished') {
  139. $btns[] = '<button class="btn btn-ship" data-action="mark_shipped">Mark Shipped</button>';
  140. }
  141. if (in_array($r['status'], ['', 'Finished'], true) && (int) $r['qty'] > 1) {
  142. $btns[] = '<button class="btn btn-partial" data-action="partial_ship" data-qty="' . (int) $r['qty'] . '">Partial Ship</button>';
  143. }
  144. if (in_array($r['status'], ['Finished', 'Shipped'], true)) {
  145. $btns[] = '<button class="btn btn-undo" data-action="undo">Undo</button>';
  146. }
  147. } else { // ICG
  148. if ($r['status'] === 'Shipped') {
  149. $btns[] = '<button class="btn btn-receive" data-action="mark_received">Mark Received</button>';
  150. }
  151. if ($r['status'] === 'Received') {
  152. $btns[] = '<span class="received">Received</span>';
  153. }
  154. }
  155. return implode(' ', $btns);
  156. }
  157. function render_messages(int $vendor_id, ?int $since_id = null): string {
  158. $pdo = db();
  159. if ($since_id !== null) {
  160. $stmt = $pdo->prepare('SELECT * FROM messages WHERE vendor_id = ? AND id > ? ORDER BY id ASC');
  161. $stmt->execute([$vendor_id, $since_id]);
  162. } else {
  163. $stmt = $pdo->prepare('SELECT * FROM messages WHERE vendor_id = ? ORDER BY id ASC');
  164. $stmt->execute([$vendor_id]);
  165. }
  166. $rows = $stmt->fetchAll();
  167. ob_start();
  168. foreach ($rows as $m) {
  169. $klass = $m['author'] === 'ICG' ? 'msg msg-icg' : 'msg msg-vendor';
  170. echo '<div class="' . $klass . '" data-msg-id="' . (int)$m['id'] . '">';
  171. echo '<span class="msg-author">' . h($m['author']) . '</span>';
  172. echo '<span class="msg-time">' . h($m['posted_at']) . '</span>';
  173. echo '<div class="msg-body">' . nl2br(h($m['body'])) . '</div>';
  174. echo '</div>';
  175. }
  176. return ob_get_clean();
  177. }
  178. function max_message_id(int $vendor_id): int {
  179. $stmt = db()->prepare('SELECT COALESCE(MAX(id), 0) FROM messages WHERE vendor_id = ?');
  180. $stmt->execute([$vendor_id]);
  181. return (int) $stmt->fetchColumn();
  182. }