diff --git a/api/ops.php b/api/ops.php new file mode 100644 index 0000000..c0d92db --- /dev/null +++ b/api/ops.php @@ -0,0 +1,115 @@ + false, 'error' => 'Unauthorized']); + exit; +} + +$userId = $_SESSION['user_id']; +$data = json_decode(file_get_contents('php://input'), true); + +if (!$data || !isset($data['opId']) || !isset($data['action'])) { + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'error' => 'Invalid data']); + exit; +} + +$opId = $data['opId']; +$action = $data['action']; +$reason = $data['reason'] ?? null; + +try { + $db->beginTransaction(); + + // Fetch current op state + $stmt = $db->prepare("SELECT * FROM operations WHERE id = ?"); + $stmt->execute([$opId]); + $op = $stmt->fetch(); + + if (!$op) { + throw new Exception("Operation not found"); + } + + $eventType = ''; + $statusUpdate = ''; + $now = date('Y-m-d H:i:s'); + + switch ($action) { + case 'start': + $statusUpdate = "status = 'in_progress', assigned_worker_id = $userId, start_time = IFNULL(start_time, '$now')"; + $eventType = 'start'; + // If it was stalled, event type is 'resume' + if ($op['status'] === 'stalled') { + $eventType = 'resume'; + } + break; + + case 'stall': + $statusUpdate = "status = 'stalled', stall_reason = " . $db->quote($reason); + $eventType = 'stalled'; + break; + + case 'done': + $statusUpdate = "status = 'completed', end_time = '$now'"; + $eventType = 'completed'; + break; + + default: + throw new Exception("Invalid action"); + } + + // Update operation + $db->exec("UPDATE operations SET $statusUpdate WHERE id = $opId"); + + // Log time study event + $stmt = $db->prepare("INSERT INTO time_study_events (operation_id, user_id, event_type, reason) VALUES (?, ?, ?, ?)"); + $stmt->execute([$opId, $userId, $eventType, $reason]); + + // Post-completion logic (if done) + if ($action === 'done') { + // Check if all operations for this component are done + $compId = $op['component_id']; + $pendingOps = $db->prepare("SELECT COUNT(*) FROM operations WHERE component_id = ? AND status != 'completed'"); + $pendingOps->execute([$compId]); + if ($pendingOps->fetchColumn() == 0) { + // Component is done + $db->exec("UPDATE components SET status = 'completed' WHERE id = $compId"); + + // Check if all components for this job are done + $stmt = $db->prepare("SELECT job_id FROM components WHERE id = ?"); + $stmt->execute([$compId]); + $jobId = $stmt->fetchColumn(); + + $pendingComps = $db->prepare("SELECT COUNT(*) FROM components WHERE job_id = ? AND status != 'completed'"); + $pendingComps->execute([$jobId]); + if ($pendingComps->fetchColumn() == 0) { + // Job is done + $db->exec("UPDATE jobs SET status = 'completed' WHERE id = $jobId"); + } else { + // Job is in_progress if not already + $db->exec("UPDATE jobs SET status = 'in_progress' WHERE id = $jobId"); + } + } else { + // Component is in_progress if not already + $db->exec("UPDATE components SET status = 'in_progress' WHERE id = $compId"); + + // Job is in_progress + $stmt = $db->prepare("SELECT job_id FROM components WHERE id = ?"); + $stmt->execute([$compId]); + $jobId = $stmt->fetchColumn(); + $db->exec("UPDATE jobs SET status = 'in_progress' WHERE id = $jobId"); + } + } + + $db->commit(); + echo json_encode(['success' => true]); + +} catch (Exception $e) { + $db->rollBack(); + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} diff --git a/dashboard.php b/dashboard.php index 64b800f..8dea93e 100644 --- a/dashboard.php +++ b/dashboard.php @@ -21,6 +21,54 @@ if ($role === 'worker') { $assignedProcesses = json_decode($res['assigned_processes'] ?? '[]', true); } +// Stats for Admin +$stats = [ + 'active_jobs' => 0, + 'pending_ops' => 0, + 'inventory_alerts' => 0, + 'active_workers' => 0 +]; + +if ($role === 'admin') { + $stats['active_jobs'] = $db->query("SELECT COUNT(*) FROM jobs WHERE status = 'in_progress'")->fetchColumn(); + $stats['pending_ops'] = $db->query("SELECT COUNT(*) FROM operations WHERE status = 'pending'")->fetchColumn(); + $stats['inventory_alerts'] = $db->query("SELECT COUNT(*) FROM inventory WHERE stock_level <= reorder_level")->fetchColumn(); + $stats['active_workers'] = $db->query("SELECT COUNT(DISTINCT assigned_worker_id) FROM operations WHERE status = 'in_progress'")->fetchColumn(); + + // Fetch recent jobs + $recentJobs = $db->query("SELECT * FROM jobs ORDER BY created_at DESC LIMIT 5")->fetchAll(); +} + +// Queue for Worker or Admin View +$queue = []; +if ($role === 'worker') { + if (!empty($assignedProcesses)) { + $placeholders = implode(',', array_fill(0, count($assignedProcesses), '?')); + $stmt = $db->prepare(" + SELECT o.*, c.name as component_name, j.name as job_name + FROM operations o + JOIN components c ON o.component_id = c.id + JOIN jobs j ON c.job_id = j.id + WHERE o.status IN ('pending', 'in_progress', 'stalled') + AND o.process_type IN ($placeholders) + ORDER BY o.priority DESC, o.created_at ASC + "); + $stmt->execute($assignedProcesses); + $queue = $stmt->fetchAll(); + } +} else { + // Admin sees all in-progress or stalled ops + $queue = $db->query(" + SELECT o.*, c.name as component_name, j.name as job_name, u.name as worker_name + FROM operations o + JOIN components c ON o.component_id = c.id + JOIN jobs j ON c.job_id = j.id + LEFT JOIN users u ON o.assigned_worker_id = u.id + WHERE o.status IN ('in_progress', 'stalled') + ORDER BY o.created_at ASC + ")->fetchAll(); +} + ?> @@ -39,6 +87,9 @@ if ($role === 'worker') { --accent: #3b82f6; --text: #334155; --border: #e2e8f0; + --success: #10b981; + --warning: #f59e0b; + --danger: #ef4444; } body { font-family: 'Inter', system-ui, -apple-system, sans-serif; @@ -130,6 +181,42 @@ if ($role === 'worker') { border-radius: 4px; margin-right: 0.5rem; } + .op-card { + padding: 1.25rem; + border-left: 4px solid #cbd5e1; + transition: transform 0.1s; + } + .op-card:hover { + transform: translateY(-2px); + } + .op-card.status-in_progress { border-left-color: var(--success); } + .op-card.status-stalled { border-left-color: var(--warning); } + .op-card.status-pending { border-left-color: #cbd5e1; } + + .op-meta { + font-size: 0.75rem; + color: #64748b; + margin-bottom: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.025em; + font-weight: 600; + } + .op-title { + font-weight: 700; + font-size: 1rem; + color: var(--primary); + margin-bottom: 0.5rem; + } + .op-job { + font-size: 0.875rem; + color: #64748b; + } + .btn-action { + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + padding: 0.5rem 1rem; + } @@ -137,13 +224,13 @@ if ($role === 'worker') { +
+ My Skills: + + + +
+
-
-
-
- Active Processes: - - - -
-
+ +
+
No pending operations for your assigned processes.
-

New jobs will appear here when ready for production.

-
+ + +
+
+
• Priority
+
+
+ / +
+ +
+ + + + + + + + +
+
+
+ +
+
Active Jobs
-
0
+
Pending Ops
-
0
+
Inventory Alerts
-
0
+
+ +
Active Workers
-
2
+
-
-
Production Status
-
-
No production data available yet.
- -
+
+
+
+
+ Live Production Status + Active Ops +
+
+ +
No operations currently in progress.
+ +
+ + + + + + + + + + + + + + + + + + + +
OperationWorkerStatusStarted
+
+
+
+ + + +
+
+ +
+
+
+
+
+
Recent Jobs
+
+
+ +
+
+ + +
+
Due: • Qty:
+
+ + +
No jobs created yet.
+ +
+
+ +
+
+ + + + - + \ No newline at end of file diff --git a/db/migrate_002_manufacturing_core.php b/db/migrate_002_manufacturing_core.php new file mode 100644 index 0000000..eec1a16 --- /dev/null +++ b/db/migrate_002_manufacturing_core.php @@ -0,0 +1,108 @@ +exec("CREATE TABLE IF NOT EXISTS jobs ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + quantity INT DEFAULT 1, + due_date DATE DEFAULT NULL, + serial_number VARCHAR(100) UNIQUE, + status ENUM('planned', 'in_progress', 'completed') DEFAULT 'planned', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )"); + + // 2. Components (BOM structure) + $db->exec("CREATE TABLE IF NOT EXISTS components ( + id INT AUTO_INCREMENT PRIMARY KEY, + job_id INT NOT NULL, + parent_id INT DEFAULT NULL, + name VARCHAR(255) NOT NULL, + status ENUM('pending', 'in_progress', 'completed') DEFAULT 'pending', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (job_id) REFERENCES jobs(id) ON DELETE CASCADE, + FOREIGN KEY (parent_id) REFERENCES components(id) ON DELETE CASCADE + )"); + + // 3. Operations + $db->exec("CREATE TABLE IF NOT EXISTS operations ( + id INT AUTO_INCREMENT PRIMARY KEY, + component_id INT NOT NULL, + name VARCHAR(255) NOT NULL, + process_type VARCHAR(100) NOT NULL, -- e.g. cutting, welding + status ENUM('pending', 'in_progress', 'stalled', 'completed') DEFAULT 'pending', + assigned_worker_id INT DEFAULT NULL, + priority INT DEFAULT 0, + start_time DATETIME DEFAULT NULL, + end_time DATETIME DEFAULT NULL, + stall_reason TEXT DEFAULT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (component_id) REFERENCES components(id) ON DELETE CASCADE, + FOREIGN KEY (assigned_worker_id) REFERENCES users(id) ON DELETE SET NULL + )"); + + // 4. Inventory + $db->exec("CREATE TABLE IF NOT EXISTS inventory ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + category ENUM('material', 'consumable', 'hardware') NOT NULL, + stock_level DECIMAL(10,2) DEFAULT 0, + reorder_level DECIMAL(10,2) DEFAULT 0, + unit VARCHAR(50) DEFAULT 'pcs', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )"); + + // 5. Inventory Transactions + $db->exec("CREATE TABLE IF NOT EXISTS inventory_transactions ( + id INT AUTO_INCREMENT PRIMARY KEY, + inventory_id INT NOT NULL, + type ENUM('in', 'out') NOT NULL, + quantity DECIMAL(10,2) NOT NULL, + user_id INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (inventory_id) REFERENCES inventory(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) + )"); + + // 6. Time Study Events + $db->exec("CREATE TABLE IF NOT EXISTS time_study_events ( + id INT AUTO_INCREMENT PRIMARY KEY, + operation_id INT NOT NULL, + user_id INT NOT NULL, + event_type ENUM('start', 'stalled', 'completed', 'resume') NOT NULL, + reason TEXT DEFAULT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (operation_id) REFERENCES operations(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) + )"); + + echo "Manufacturing core tables created successfully.\n"; + + // Seed some initial data if empty + $stmt = $db->query("SELECT COUNT(*) FROM jobs"); + if ($stmt->fetchColumn() == 0) { + // Sample Job + $db->exec("INSERT INTO jobs (name, quantity, due_date, serial_number, status) VALUES ('Project ALPHA', 10, '2026-03-15', 'SN-001', 'planned')"); + $jobId = $db->lastInsertId(); + + // Sample Component + $db->exec("INSERT INTO components (job_id, name, status) VALUES ($jobId, 'Main Chassis', 'pending')"); + $compId = $db->lastInsertId(); + + // Sample Operations + $db->exec("INSERT INTO operations (component_id, name, process_type, status, priority) VALUES ($compId, 'Cut steel plate', 'cutting', 'pending', 1)"); + $db->exec("INSERT INTO operations (component_id, name, process_type, status, priority) VALUES ($compId, 'Weld corners', 'welding', 'pending', 2)"); + + // Sample Inventory + $db->exec("INSERT INTO inventory (name, category, stock_level, reorder_level) VALUES ('Steel Plate 5mm', 'material', 50, 10)"); + $db->exec("INSERT INTO inventory (name, category, stock_level, reorder_level) VALUES ('Welding Rods', 'consumable', 100, 20)"); + + echo "Sample manufacturing data seeded.\n"; + } + +} catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; +} + diff --git a/inventory.php b/inventory.php new file mode 100644 index 0000000..60631fd --- /dev/null +++ b/inventory.php @@ -0,0 +1,264 @@ +beginTransaction(); + + // Record transaction + $stmt = $db->prepare("INSERT INTO inventory_transactions (inventory_id, type, quantity, user_id) VALUES (?, ?, ?, ?)"); + $stmt->execute([$itemId, $type, $qty, $userId]); + + // Update stock + $mod = ($type === 'in' ? '+' : '-'); + $db->exec("UPDATE inventory SET stock_level = stock_level $mod $qty WHERE id = $itemId"); + + $db->commit(); + header("Location: inventory.php?success=1"); + exit; + } catch (Exception $e) { + $db->rollBack(); + $error = "Error: " . $e->getMessage(); + } +} + +// Handle New Item +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['new_item'])) { + $name = $_POST['name']; + $cat = $_POST['category']; + $stock = $_POST['stock_level']; + $reorder = $_POST['reorder_level']; + $unit = $_POST['unit']; + + $stmt = $db->prepare("INSERT INTO inventory (name, category, stock_level, reorder_level, unit) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$name, $cat, $stock, $reorder, $unit]); + header("Location: inventory.php?item_added=1"); + exit; +} + +$inventory = $db->query("SELECT * FROM inventory ORDER BY name ASC")->fetchAll(); +$lowStock = array_filter($inventory, fn($i) => $i['stock_level'] <= $i['reorder_level']); + +?> + + + + + + M-TRACK | Inventory + + + + + + + + + +
+
+

Inventory Management

+ +
+ + +
+ +
Low Stock Warning: items are below reorder level.
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Item NameCategoryCurrent StockReorder LevelActions
No inventory items found.
+
+
+
+ + + + + +
+
+
+
+
+ + + + + + + + + + + diff --git a/jobs.php b/jobs.php new file mode 100644 index 0000000..a3fef6a --- /dev/null +++ b/jobs.php @@ -0,0 +1,394 @@ +prepare("INSERT INTO jobs (name, quantity, due_date, serial_number) VALUES (?, ?, ?, ?)"); + $stmt->execute([$name, $qty, $due, $sn]); + $jobId = $db->lastInsertId(); + header("Location: jobs.php?id=$jobId"); + exit; + } catch (Exception $e) { + $error = "Error: " . $e->getMessage(); + } +} + +// Handle Component Addition +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_component'])) { + $jobId = $_POST['job_id']; + $compName = $_POST['comp_name']; + $stmt = $db->prepare("INSERT INTO components (job_id, name) VALUES (?, ?)"); + $stmt->execute([$jobId, $compName]); + header("Location: jobs.php?id=$jobId"); + exit; +} + +// Handle Operation Addition +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_op'])) { + $jobId = $_POST['job_id']; + $compId = $_POST['comp_id']; + $opName = $_POST['op_name']; + $proc = $_POST['process_type']; + $prio = $_POST['priority']; + $stmt = $db->prepare("INSERT INTO operations (component_id, name, process_type, priority) VALUES (?, ?, ?, ?)"); + $stmt->execute([$compId, $opName, $proc, $prio]); + header("Location: jobs.php?id=$jobId"); + exit; +} + +$jobs = $db->query("SELECT * FROM jobs ORDER BY created_at DESC")->fetchAll(); + +$currentJobId = $_GET['id'] ?? null; +$currentJob = null; +$components = []; + +if ($currentJobId) { + $stmt = $db->prepare("SELECT * FROM jobs WHERE id = ?"); + $stmt->execute([$currentJobId]); + $currentJob = $stmt->fetch(); + + if ($currentJob) { + $stmt = $db->prepare("SELECT * FROM components WHERE job_id = ?"); + $stmt->execute([$currentJobId]); + $components = $stmt->fetchAll(); + + foreach ($components as &$comp) { + $stmt = $db->prepare("SELECT * FROM operations WHERE component_id = ?"); + $stmt->execute([$comp['id']]); + $comp['ops'] = $stmt->fetchAll(); + } + } +} + +?> + + + + + + M-TRACK | Job Management + + + + + + + + + +
+
+

Job Management

+ +
+ +
+
+
+
Existing Jobs
+
+ +
No jobs found.
+ + +
+
+
SN:
+
+ + +
+
+
+ +
+ +
+
+ Job Details: + +
+
+
+
Serial:
+
Quantity:
+
Due Date:
+
+
+ +
+ +
+
Components & Operations
+ +
+ + +
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
Op NameProcessPrioStatus
No operations defined.
+ + + +
+
+
+ +
+
+ +
+ +
Select a job from the list or create a new one.
+
+ +
+
+
+ + + + + + + + + + + + + + diff --git a/users.php b/users.php new file mode 100644 index 0000000..99fc92c --- /dev/null +++ b/users.php @@ -0,0 +1,255 @@ +prepare($sql)->execute($params); + + if ($role === 'admin' && !empty($pin)) { + $pinHash = password_hash($pin, PASSWORD_BCRYPT); + $db->prepare("UPDATE users SET pin_hash = ? WHERE id = ?")->execute([$pinHash, $id]); + } + } else { + // Create + $pinHash = ($role === 'admin' && !empty($pin)) ? password_hash($pin, PASSWORD_BCRYPT) : null; + $db->prepare("INSERT INTO users (name, role, assigned_processes, pin_hash) VALUES (?, ?, ?, ?)") + ->execute([$name, $role, $processes, $pinHash]); + } + header("Location: users.php?success=1"); + exit; +} + +$users = $db->query("SELECT * FROM users ORDER BY role ASC, name ASC")->fetchAll(); +$processTypes = ['cutting', 'welding', 'bending', 'assembly', 'inspection']; + +?> + + + + + + M-TRACK | User Management + + + + + + + + + +
+
+

User Management

+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + +
NameRoleAssigned ProcessesActions
+ + + + + + None assigned + + + + + + +
+
+
+
+
+ + + + + + + +