prepare('SELECT * FROM jobs WHERE id = ?'); $stmt->execute([$job_id]); $job = $stmt->fetch(); if (!$job) { http_response_code(404); echo 'Job not found'; return; } // Vendor-side requests are scoped to their own jobs. if ($actor !== 'ICG' && (int) $job['vendor_id'] !== $vendor_id) { http_response_code(403); echo 'Wrong vendor'; return; } // --- Button-driven state transitions --- if ($action !== '') { $allowed = [ 'acknowledge' => ['ack', '', ['vendor']], 'mark_finished' => ['status', 'Finished', ['vendor']], 'mark_shipped' => ['status', 'Shipped', ['vendor']], 'mark_received' => ['status', 'Received', ['ICG']], ]; if (!isset($allowed[$action])) { http_response_code(400); echo 'Unknown action'; return; } [$col, $new, $roles] = $allowed[$action]; $role = $actor === 'ICG' ? 'ICG' : 'vendor'; if (!in_array($role, $roles, true)) { http_response_code(403); echo 'Action not allowed for this role'; return; } apply_job_change($job, $col, $new, $actor); echo 'Success'; return; } // --- Field edits (ICG only) --- if ($actor !== 'ICG') { http_response_code(403); echo 'Edits restricted to ICG'; return; } $editable = ['job', 'material', 'description', 'qty', 'due_date']; if (!in_array($column, $editable, true)) { http_response_code(400); echo 'Unknown column'; return; } $value = trim($value); if ($column === 'qty') { if ($value === '' || !is_numeric($value)) { http_response_code(400); echo 'Qty must be a number'; return; } $value = (string) (int) $value; } elseif ($column === 'due_date') { $value = parse_due_date($value); } apply_job_change($job, $column, $value, $actor); // An ICG edit to a content field bumps an already-acknowledged row back to // 'changed' so the vendor sees it at the top of their list. We leave 'new' // alone — the row still needs acknowledgement, just for a different reason. if ($job['ack'] === '') { apply_job_change($job, 'ack', 'changed', $actor); } echo 'Success';