Autosave: 20260227-211046

This commit is contained in:
Flatlogic Bot 2026-02-27 21:10:46 +00:00
parent 6d06eea56a
commit 4349e548ed
6 changed files with 1401 additions and 27 deletions

115
api/ops.php Normal file
View File

@ -0,0 +1,115 @@
<?php
session_start();
require '../db/config.php';
$db = db();
if (!isset($_SESSION['user_id'])) {
header('Content-Type: application/json');
echo json_encode(['success' => 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()]);
}

View File

@ -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();
}
?>
<!doctype html>
<html lang="en">
@ -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;
}
</style>
</head>
<body>
@ -137,13 +224,13 @@ if ($role === 'worker') {
<div class="sidebar">
<h2>M-TRACK</h2>
<nav class="nav nav-pills flex-column">
<a class="nav-link active" href="#"><i class="bi bi-grid-fill me-2"></i> Dashboard</a>
<a class="nav-link active" href="dashboard.php"><i class="bi bi-grid-fill me-2"></i> Dashboard</a>
<?php if ($role === 'admin'): ?>
<a class="nav-link" href="#"><i class="bi bi-briefcase me-2"></i> Jobs</a>
<a class="nav-link" href="#"><i class="bi bi-boxes me-2"></i> Inventory</a>
<a class="nav-link" href="#"><i class="bi bi-people me-2"></i> Users</a>
<a class="nav-link" href="jobs.php"><i class="bi bi-briefcase me-2"></i> Jobs</a>
<a class="nav-link" href="inventory.php"><i class="bi bi-boxes me-2"></i> Inventory</a>
<a class="nav-link" href="users.php"><i class="bi bi-people me-2"></i> Users</a>
<?php else: ?>
<a class="nav-link" href="#"><i class="bi bi-list-task me-2"></i> My Queue</a>
<a class="nav-link" href="dashboard.php"><i class="bi bi-list-task me-2"></i> My Queue</a>
<?php endif; ?>
<hr class="my-4 border-secondary opacity-25">
<a class="nav-link text-danger" href="logout.php"><i class="bi bi-box-arrow-right me-2"></i> Logout</a>
@ -161,61 +248,212 @@ if ($role === 'worker') {
</div>
<?php if ($role === 'worker'): ?>
<div class="mb-4 d-flex align-items-center">
<span class="text-muted small fw-bold text-uppercase me-3">My Skills:</span>
<?php foreach ($assignedProcesses as $proc): ?>
<span class="badge-process"><?= strtoupper($proc) ?></span>
<?php endforeach; ?>
</div>
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
Active Processes:
<?php foreach ($assignedProcesses as $proc): ?>
<span class="badge-process"><?= strtoupper($proc) ?></span>
<?php endforeach; ?>
</div>
<div class="card-body py-5 text-center">
<?php if (empty($queue)): ?>
<div class="col-12">
<div class="card card-body py-5 text-center">
<div class="mb-3 text-muted" style="font-size: 2rem;"><i class="bi bi-clipboard-check"></i></div>
<h5 class="text-muted">No pending operations for your assigned processes.</h5>
<p class="text-muted small">New jobs will appear here when ready for production.</p>
</div>
</div>
</div>
<?php else: ?>
<?php foreach ($queue as $op): ?>
<div class="col-md-6 col-xl-4">
<div class="card op-card status-<?= $op['status'] ?>">
<div class="op-meta"><?= $op['process_type'] ?> • Priority <?= $op['priority'] ?></div>
<div class="op-title"><?= htmlspecialchars($op['name']) ?></div>
<div class="op-job mb-3">
<i class="bi bi-box-seam me-1"></i> <?= htmlspecialchars($op['job_name']) ?> / <?= htmlspecialchars($op['component_name']) ?>
</div>
<div class="d-flex gap-2">
<?php if ($op['status'] === 'pending' || $op['status'] === 'stalled'): ?>
<button class="btn btn-success btn-action w-100" onclick="handleOp(<?= $op['id'] ?>, 'start')">
<?= $op['status'] === 'stalled' ? 'Resume' : 'Start' ?>
</button>
<?php endif; ?>
<?php if ($op['status'] === 'in_progress'): ?>
<button class="btn btn-warning btn-action w-50" onclick="handleOp(<?= $op['id'] ?>, 'stall')">Stall</button>
<button class="btn btn-primary btn-action w-50" onclick="handleOp(<?= $op['id'] ?>, 'done')">Done</button>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<?php else: ?>
<!-- Admin View -->
<div class="row">
<div class="col-md-3">
<div class="card text-center p-3">
<div class="text-muted small fw-bold text-uppercase mb-1">Active Jobs</div>
<div class="h3 fw-bold mb-0">0</div>
<div class="h3 fw-bold mb-0"><?= $stats['active_jobs'] ?></div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center p-3">
<div class="text-muted small fw-bold text-uppercase mb-1">Pending Ops</div>
<div class="h3 fw-bold mb-0">0</div>
<div class="h3 fw-bold mb-0"><?= $stats['pending_ops'] ?></div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center p-3">
<div class="text-muted small fw-bold text-uppercase mb-1">Inventory Alerts</div>
<div class="h3 fw-bold mb-0">0</div>
<div class="h3 fw-bold mb-0 text-<?= $stats['inventory_alerts'] > 0 ? 'danger' : 'success' ?>">
<?= $stats['inventory_alerts'] ?>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center p-3">
<div class="text-muted small fw-bold text-uppercase mb-1">Active Workers</div>
<div class="h3 fw-bold mb-0">2</div>
<div class="h3 fw-bold mb-0"><?= $stats['active_workers'] ?></div>
</div>
</div>
</div>
<div class="card mt-4">
<div class="card-header">Production Status</div>
<div class="card-body py-5 text-center">
<h5 class="text-muted">No production data available yet.</h5>
<button class="btn btn-sm btn-primary mt-2">Create First Job</button>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
Live Production Status
<span class="badge bg-primary rounded-pill"><?= count($queue) ?> Active Ops</span>
</div>
<div class="card-body p-0">
<?php if (empty($queue)): ?>
<div class="p-5 text-center text-muted">No operations currently in progress.</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Operation</th>
<th>Worker</th>
<th>Status</th>
<th>Started</th>
</tr>
</thead>
<tbody>
<?php foreach ($queue as $op): ?>
<tr>
<td>
<div class="fw-bold"><?= htmlspecialchars($op['name']) ?></div>
<div class="small text-muted"><?= htmlspecialchars($op['job_name']) ?></div>
</td>
<td><?= htmlspecialchars($op['worker_name'] ?? 'Unassigned') ?></td>
<td>
<span class="badge bg-<?= $op['status'] === 'in_progress' ? 'success' : 'warning' ?>">
<?= strtoupper($op['status']) ?>
</span>
</td>
<td class="small"><?= date('H:i', strtotime($op['start_time'])) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header">Recent Jobs</div>
<div class="card-body p-0">
<div class="list-group list-group-flush">
<?php foreach ($recentJobs as $job): ?>
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="fw-bold"><?= htmlspecialchars($job['name']) ?></span>
<span class="badge bg-light text-dark border small"><?= $job['status'] ?></span>
</div>
<div class="small text-muted">Due: <?= $job['due_date'] ?> • Qty: <?= $job['quantity'] ?></div>
</div>
<?php endforeach; ?>
<?php if (empty($recentJobs)): ?>
<div class="p-4 text-center text-muted">No jobs created yet.</div>
<?php endif; ?>
</div>
</div>
<div class="card-footer bg-white">
<a href="jobs.php" class="btn btn-sm btn-outline-primary w-100">Manage Jobs</a>
</div>
</div>
</div>
</div>
<?php endif; ?>
</div>
<!-- Modal for Stall Reason -->
<div class="modal fade" id="stallModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Stall Reason</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<textarea id="stallReason" class="form-control" placeholder="Explain why work is stalling (e.g., missing hardware, machine down)..."></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-warning" onclick="confirmStall()">Confirm Stall</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
let currentOpId = null;
const stallModal = new bootstrap.Modal(document.getElementById('stallModal'));
function handleOp(opId, action) {
if (action === 'stall') {
currentOpId = opId;
stallModal.show();
return;
}
if (action === 'done' && !confirm('Mark this operation as complete?')) return;
performAction(opId, action);
}
function confirmStall() {
const reason = document.getElementById('stallReason').value.trim();
if (!reason) {
alert('Please provide a reason for stalling.');
return;
}
performAction(currentOpId, 'stall', reason);
stallModal.hide();
}
function performAction(opId, action, reason = '') {
fetch('api/ops.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ opId, action, reason })
})
.then(r => r.json())
.then(res => {
if (res.success) {
location.reload();
} else {
alert('Error: ' + res.error);
}
});
}
</script>
</body>
</html>
</html>

View File

@ -0,0 +1,108 @@
<?php
require 'db/config.php';
$db = db();
try {
// 1. Jobs
$db->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";
}

264
inventory.php Normal file
View File

@ -0,0 +1,264 @@
<?php
session_start();
require 'db/config.php';
$db = db();
if (!isset($_SESSION['user_id']) || $_SESSION['role'] !== 'admin') {
header('Location: index.php');
exit;
}
$userId = $_SESSION['user_id'];
// Handle Inventory Update
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_stock'])) {
$itemId = $_POST['item_id'];
$qty = $_POST['quantity'];
$type = $_POST['type']; // in or out
try {
$db->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']);
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>M-TRACK | Inventory</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--sidebar-width: 240px;
--bg: #f8fafc;
--primary: #1e293b;
--accent: #3b82f6;
--text: #334155;
--border: #e2e8f0;
--danger: #ef4444;
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background-color: var(--bg);
color: var(--text);
}
.sidebar {
width: var(--sidebar-width);
position: fixed;
top: 0;
bottom: 0;
left: 0;
background-color: var(--primary);
color: white;
z-index: 1000;
padding: 1.5rem 1rem;
}
.main-content {
margin-left: var(--sidebar-width);
padding: 2.5rem;
}
.card {
background: white;
border: 1px solid var(--border);
border-radius: 4px;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
margin-bottom: 1.5rem;
}
.alert-low {
border-left: 4px solid var(--danger);
}
</style>
</head>
<body>
<div class="sidebar">
<h2>M-TRACK</h2>
<nav class="nav nav-pills flex-column">
<a class="nav-link" href="dashboard.php"><i class="bi bi-grid-fill me-2"></i> Dashboard</a>
<a class="nav-link" href="jobs.php"><i class="bi bi-briefcase me-2"></i> Jobs</a>
<a class="nav-link active" href="inventory.php"><i class="bi bi-boxes me-2"></i> Inventory</a>
<a class="nav-link" href="users.php"><i class="bi bi-people me-2"></i> Users</a>
<hr class="my-4 border-secondary opacity-25">
<a class="nav-link text-danger" href="logout.php"><i class="bi bi-box-arrow-right me-2"></i> Logout</a>
</nav>
</div>
<div class="main-content">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Inventory Management</h1>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#newItemModal">
<i class="bi bi-plus-lg me-1"></i> New Item
</button>
</div>
<?php if (!empty($lowStock)): ?>
<div class="alert alert-danger d-flex align-items-center mb-4">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<div><strong>Low Stock Warning:</strong> <?= count($lowStock) ?> items are below reorder level.</div>
</div>
<?php endif; ?>
<div class="card">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead class="table-light">
<tr>
<th>Item Name</th>
<th>Category</th>
<th>Current Stock</th>
<th>Reorder Level</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($inventory)): ?>
<tr><td colspan="5" class="p-5 text-center text-muted">No inventory items found.</td></tr>
<?php else: ?>
<?php foreach ($inventory as $item): ?>
<tr class="<?= $item['stock_level'] <= $item['reorder_level'] ? 'table-danger alert-low' : '' ?>">
<td>
<div class="fw-bold"><?= htmlspecialchars($item['name']) ?></div>
<div class="small text-muted"><?= $item['unit'] ?></div>
</td>
<td><span class="badge bg-light text-dark border"><?= strtoupper($item['category']) ?></span></td>
<td>
<span class="fw-bold <?= $item['stock_level'] <= $item['reorder_level'] ? 'text-danger' : '' ?>">
<?= $item['stock_level'] ?>
</span>
</td>
<td><?= $item['reorder_level'] ?></td>
<td class="text-end">
<button class="btn btn-sm btn-outline-primary" onclick="setUpdateItem(<?= $item['id'] ?>, '<?= htmlspecialchars($item['name']) ?>')" data-bs-toggle="modal" data-bs-target="#updateModal">
Update Stock
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- New Item Modal -->
<div class="modal fade" id="newItemModal" tabindex="-1">
<div class="modal-dialog">
<form method="POST" class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Inventory Item</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Item Name</label>
<input type="text" name="name" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Category</label>
<select name="category" class="form-select">
<option value="material">Raw Material</option>
<option value="consumable">Consumable</option>
<option value="hardware">Hardware</option>
</select>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Initial Stock</label>
<input type="number" step="0.01" name="stock_level" class="form-control" value="0">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Reorder Level</label>
<input type="number" step="0.01" name="reorder_level" class="form-control" value="0">
</div>
</div>
<div class="mb-3">
<label class="form-label">Unit</label>
<input type="text" name="unit" class="form-control" placeholder="pcs, kg, meters, etc.">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" name="new_item" class="btn btn-primary">Add Item</button>
</div>
</form>
</div>
</div>
<!-- Update Stock Modal -->
<div class="modal fade" id="updateModal" tabindex="-1">
<div class="modal-dialog">
<form method="POST" class="modal-content">
<input type="hidden" name="item_id" id="update_item_id">
<div class="modal-header">
<h5 class="modal-title">Update Stock: <span id="update_item_name"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Transaction Type</label>
<select name="type" class="form-select">
<option value="in">Stock In (Receive)</option>
<option value="out">Stock Out (Consume)</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Quantity</label>
<input type="number" step="0.01" name="quantity" class="form-control" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" name="update_stock" class="btn btn-primary">Save Changes</button>
</div>
</form>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
function setUpdateItem(id, name) {
document.getElementById('update_item_id').value = id;
document.getElementById('update_item_name').innerText = name;
}
</script>
</body>
</html>

394
jobs.php Normal file
View File

@ -0,0 +1,394 @@
<?php
session_start();
require 'db/config.php';
$db = db();
if (!isset($_SESSION['user_id']) || $_SESSION['role'] !== 'admin') {
header('Location: index.php');
exit;
}
$userName = $_SESSION['user_name'];
$userId = $_SESSION['user_id'];
// Handle Job Creation
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_job'])) {
$name = $_POST['name'];
$qty = $_POST['quantity'];
$due = $_POST['due_date'];
$sn = $_POST['serial_number'];
try {
$stmt = $db->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();
}
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>M-TRACK | Job Management</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--sidebar-width: 240px;
--bg: #f8fafc;
--primary: #1e293b;
--accent: #3b82f6;
--text: #334155;
--border: #e2e8f0;
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background-color: var(--bg);
color: var(--text);
}
.sidebar {
width: var(--sidebar-width);
position: fixed;
top: 0;
bottom: 0;
left: 0;
background-color: var(--primary);
color: white;
z-index: 1000;
padding: 1.5rem 1rem;
}
.main-content {
margin-left: var(--sidebar-width);
padding: 2.5rem;
}
.sidebar h2 {
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 2rem;
padding: 0 0.5rem;
letter-spacing: -0.025em;
}
.nav-pills .nav-link {
color: #94a3b8;
font-weight: 500;
font-size: 0.875rem;
padding: 0.625rem 0.75rem;
margin-bottom: 0.25rem;
border-radius: 4px;
}
.nav-pills .nav-link:hover {
color: white;
background-color: rgba(255,255,255,0.05);
}
.nav-pills .nav-link.active {
color: white;
background-color: var(--accent);
}
.card {
background: white;
border: 1px solid var(--border);
border-radius: 4px;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
margin-bottom: 1.5rem;
}
.card-header {
background: white;
border-bottom: 1px solid var(--border);
padding: 1rem 1.25rem;
font-weight: 600;
font-size: 0.875rem;
color: var(--primary);
}
.job-item {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border);
cursor: pointer;
}
.job-item:hover { background: #f1f5f9; }
.job-item.active { background: #eff6ff; border-left: 3px solid var(--accent); }
.component-box {
background: #f8fafc;
border: 1px solid var(--border);
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
}
</style>
</head>
<body>
<div class="sidebar">
<h2>M-TRACK</h2>
<nav class="nav nav-pills flex-column">
<a class="nav-link" href="dashboard.php"><i class="bi bi-grid-fill me-2"></i> Dashboard</a>
<a class="nav-link active" href="jobs.php"><i class="bi bi-briefcase me-2"></i> Jobs</a>
<a class="nav-link" href="inventory.php"><i class="bi bi-boxes me-2"></i> Inventory</a>
<a class="nav-link" href="users.php"><i class="bi bi-people me-2"></i> Users</a>
<hr class="my-4 border-secondary opacity-25">
<a class="nav-link text-danger" href="logout.php"><i class="bi bi-box-arrow-right me-2"></i> Logout</a>
</nav>
</div>
<div class="main-content">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Job Management</h1>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#newJobModal">
<i class="bi bi-plus-lg me-1"></i> New Job
</button>
</div>
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-header">Existing Jobs</div>
<div class="card-body p-0" style="max-height: 70vh; overflow-y: auto;">
<?php if (empty($jobs)): ?>
<div class="p-4 text-center text-muted">No jobs found.</div>
<?php else: ?>
<?php foreach ($jobs as $job): ?>
<div class="job-item <?= $currentJobId == $job['id'] ? 'active' : '' ?>" onclick="location.href='jobs.php?id=<?= $job['id'] ?>'">
<div class="fw-bold"><?= htmlspecialchars($job['name']) ?></div>
<div class="small text-muted">SN: <?= htmlspecialchars($job['serial_number'] ?: 'N/A') ?> • <?= $job['status'] ?></div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-md-8">
<?php if ($currentJob): ?>
<div class="card mb-4">
<div class="card-header d-flex justify-content-between">
Job Details: <?= htmlspecialchars($currentJob['name']) ?>
<span class="badge bg-light text-dark border"><?= strtoupper($currentJob['status']) ?></span>
</div>
<div class="card-body">
<div class="row small mb-3">
<div class="col-md-3"><strong>Serial:</strong> <?= htmlspecialchars($currentJob['serial_number']) ?></div>
<div class="col-md-3"><strong>Quantity:</strong> <?= $currentJob['quantity'] ?></div>
<div class="col-md-3"><strong>Due Date:</strong> <?= $currentJob['due_date'] ?></div>
<div class="col-md-3 text-end"><button class="btn btn-sm btn-outline-danger">Delete Job</button></div>
</div>
<hr>
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0 fw-bold">Components & Operations</h6>
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#newCompModal">
<i class="bi bi-plus me-1"></i> Add Component
</button>
</div>
<?php foreach ($components as $comp): ?>
<div class="component-box">
<div class="d-flex justify-content-between mb-3">
<h6 class="mb-0 fw-bold text-primary"><?= htmlspecialchars($comp['name']) ?></h6>
<button class="btn btn-sm btn-link text-primary p-0" onclick="setCompId(<?= $comp['id'] ?>)" data-bs-toggle="modal" data-bs-target="#newOpModal">
<i class="bi bi-plus-circle me-1"></i> Add Op
</button>
</div>
<div class="table-responsive">
<table class="table table-sm table-bordered bg-white mb-0">
<thead class="table-light">
<tr class="small">
<th>Op Name</th>
<th>Process</th>
<th>Prio</th>
<th>Status</th>
</tr>
</thead>
<tbody class="small">
<?php if (empty($comp['ops'])): ?>
<tr><td colspan="4" class="text-center text-muted">No operations defined.</td></tr>
<?php else: ?>
<?php foreach ($comp['ops'] as $op): ?>
<tr>
<td><?= htmlspecialchars($op['name']) ?></td>
<td><?= strtoupper($op['process_type']) ?></td>
<td><?= $op['priority'] ?></td>
<td>
<span class="badge bg-<?= $op['status'] === 'completed' ? 'success' : ($op['status'] === 'in_progress' ? 'info' : 'secondary') ?>">
<?= $op['status'] ?>
</span>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php else: ?>
<div class="card py-5 text-center text-muted">
<i class="bi bi-arrow-left h1 mb-3"></i>
<h5>Select a job from the list or create a new one.</h5>
</div>
<?php endif; ?>
</div>
</div>
</div>
<!-- New Job Modal -->
<div class="modal fade" id="newJobModal" tabindex="-1">
<div class="modal-dialog">
<form method="POST" class="modal-content">
<div class="modal-header">
<h5 class="modal-title">New Job</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Job Name</label>
<input type="text" name="name" class="form-control" required placeholder="e.g. Project X">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Quantity</label>
<input type="number" name="quantity" class="form-control" value="1">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Due Date</label>
<input type="date" name="due_date" class="form-control">
</div>
</div>
<div class="mb-3">
<label class="form-label">Serial Number</label>
<input type="text" name="serial_number" class="form-control" placeholder="SN-XXXX">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" name="create_job" class="btn btn-primary">Create Job</button>
</div>
</form>
</div>
</div>
<!-- New Component Modal -->
<div class="modal fade" id="newCompModal" tabindex="-1">
<div class="modal-dialog">
<form method="POST" class="modal-content">
<input type="hidden" name="job_id" value="<?= $currentJobId ?>">
<div class="modal-header">
<h5 class="modal-title">Add Component</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Component Name</label>
<input type="text" name="comp_name" class="form-control" required placeholder="e.g. Chassis, Motor Mount">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" name="add_component" class="btn btn-primary">Add Component</button>
</div>
</form>
</div>
</div>
<!-- New Operation Modal -->
<div class="modal fade" id="newOpModal" tabindex="-1">
<div class="modal-dialog">
<form method="POST" class="modal-content">
<input type="hidden" name="job_id" value="<?= $currentJobId ?>">
<input type="hidden" name="comp_id" id="modal_comp_id">
<div class="modal-header">
<h5 class="modal-title">Add Operation</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Operation Name</label>
<input type="text" name="op_name" class="form-control" required placeholder="e.g. Laser Cut, MIG Weld">
</div>
<div class="mb-3">
<label class="form-label">Process Type</label>
<select name="process_type" class="form-select">
<option value="cutting">Cutting</option>
<option value="welding">Welding</option>
<option value="bending">Bending</option>
<option value="assembly">Assembly</option>
<option value="inspection">Inspection</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Priority (higher first)</label>
<input type="number" name="priority" class="form-control" value="0">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" name="add_op" class="btn btn-primary">Add Operation</button>
</div>
</form>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
function setCompId(id) {
document.getElementById('modal_comp_id').value = id;
}
</script>
</body>
</html>

255
users.php Normal file
View File

@ -0,0 +1,255 @@
<?php
session_start();
require 'db/config.php';
$db = db();
if (!isset($_SESSION['user_id']) || $_SESSION['role'] !== 'admin') {
header('Location: index.php');
exit;
}
// Handle User Creation/Update
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_user'])) {
$id = $_POST['user_id'] ?? null;
$name = $_POST['name'];
$role = $_POST['role'];
$processes = json_encode($_POST['processes'] ?? []);
$pin = $_POST['pin'] ?? '';
if ($id) {
// Update
$sql = "UPDATE users SET name = ?, role = ?, assigned_processes = ? WHERE id = ?";
$params = [$name, $role, $processes, $id];
$db->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'];
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>M-TRACK | User Management</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--sidebar-width: 240px;
--bg: #f8fafc;
--primary: #1e293b;
--accent: #3b82f6;
--text: #334155;
--border: #e2e8f0;
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background-color: var(--bg);
color: var(--text);
}
.sidebar {
width: var(--sidebar-width);
position: fixed;
top: 0;
bottom: 0;
left: 0;
background-color: var(--primary);
color: white;
z-index: 1000;
padding: 1.5rem 1rem;
}
.main-content {
margin-left: var(--sidebar-width);
padding: 2.5rem;
}
.card {
background: white;
border: 1px solid var(--border);
border-radius: 4px;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
margin-bottom: 1.5rem;
}
.badge-process {
background: #e2e8f0;
color: #475569;
font-weight: 600;
font-size: 0.75rem;
padding: 0.25rem 0.625rem;
border-radius: 4px;
margin-right: 0.25rem;
margin-bottom: 0.25rem;
display: inline-block;
}
</style>
</head>
<body>
<div class="sidebar">
<h2>M-TRACK</h2>
<nav class="nav nav-pills flex-column">
<a class="nav-link" href="dashboard.php"><i class="bi bi-grid-fill me-2"></i> Dashboard</a>
<a class="nav-link" href="jobs.php"><i class="bi bi-briefcase me-2"></i> Jobs</a>
<a class="nav-link" href="inventory.php"><i class="bi bi-boxes me-2"></i> Inventory</a>
<a class="nav-link active" href="users.php"><i class="bi bi-people me-2"></i> Users</a>
<hr class="my-4 border-secondary opacity-25">
<a class="nav-link text-danger" href="logout.php"><i class="bi bi-box-arrow-right me-2"></i> Logout</a>
</nav>
</div>
<div class="main-content">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>User Management</h1>
<button class="btn btn-primary" onclick="resetUserModal()" data-bs-toggle="modal" data-bs-target="#userModal">
<i class="bi bi-person-plus-fill me-1"></i> New User
</button>
</div>
<div class="card">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead class="table-light">
<tr>
<th>Name</th>
<th>Role</th>
<th>Assigned Processes</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $user): ?>
<?php $procs = json_decode($user['assigned_processes'] ?? '[]', true); ?>
<tr>
<td><div class="fw-bold"><?= htmlspecialchars($user['name']) ?></div></td>
<td><span class="badge bg-<?= $user['role'] === 'admin' ? 'dark' : 'light text-dark border' ?> text-uppercase" style="font-size: 0.7rem;"><?= $user['role'] ?></span></td>
<td>
<?php if ($user['role'] === 'worker'): ?>
<?php foreach ($procs as $p): ?>
<span class="badge-process"><?= strtoupper($p) ?></span>
<?php endforeach; ?>
<?php if (empty($procs)): ?>
<span class="text-muted small">None assigned</span>
<?php endif; ?>
<?php else: ?>
<span class="text-muted small"></span>
<?php endif; ?>
</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-primary" onclick='editUser(<?= json_encode($user) ?>)' data-bs-toggle="modal" data-bs-target="#userModal">Edit</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- User Modal -->
<div class="modal fade" id="userModal" tabindex="-1">
<div class="modal-dialog">
<form method="POST" class="modal-content" id="userForm">
<input type="hidden" name="user_id" id="user_id">
<div class="modal-header">
<h5 class="modal-title" id="modalTitle">New User</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Full Name</label>
<input type="text" name="name" id="user_name" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Role</label>
<select name="role" id="user_role" class="form-select" onchange="toggleRoleFields()">
<option value="worker">Worker</option>
<option value="admin">Admin</option>
</select>
</div>
<div id="worker_fields">
<label class="form-label d-block">Assigned Processes</label>
<div class="row">
<?php foreach ($processTypes as $pt): ?>
<div class="col-6 mb-2">
<div class="form-check">
<input class="form-check-input proc-checkbox" type="checkbox" name="processes[]" value="<?= $pt ?>" id="proc_<?= $pt ?>">
<label class="form-check-label text-capitalize" for="proc_<?= $pt ?>">
<?= $pt ?>
</label>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<div id="admin_fields" style="display:none;">
<div class="mb-3">
<label class="form-label">PIN (4-6 digits)</label>
<input type="password" name="pin" id="user_pin" class="form-control" placeholder="Leave blank to keep current PIN if editing">
<small class="text-muted">Admins must enter this PIN to log in.</small>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" name="save_user" class="btn btn-primary">Save User</button>
</div>
</form>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
function toggleRoleFields() {
const role = document.getElementById('user_role').value;
document.getElementById('worker_fields').style.display = role === 'worker' ? 'block' : 'none';
document.getElementById('admin_fields').style.display = role === 'admin' ? 'block' : 'none';
}
function resetUserModal() {
document.getElementById('userForm').reset();
document.getElementById('user_id').value = '';
document.getElementById('modalTitle').innerText = 'New User';
toggleRoleFields();
}
function editUser(user) {
document.getElementById('user_id').value = user.id;
document.getElementById('user_name').value = user.name;
document.getElementById('user_role').value = user.role;
document.getElementById('modalTitle').innerText = 'Edit User: ' + user.name;
// Reset checkboxes
document.querySelectorAll('.proc-checkbox').forEach(cb => cb.checked = false);
if (user.assigned_processes) {
const procs = JSON.parse(user.assigned_processes);
procs.forEach(p => {
const cb = document.getElementById('proc_' + p);
if (cb) cb.checked = true;
});
}
toggleRoleFields();
}
</script>
</body>
</html>