464 lines
17 KiB
PHP
464 lines
17 KiB
PHP
<?php
|
|
session_start();
|
|
require 'db/config.php';
|
|
$db = db();
|
|
|
|
if (!isset($_SESSION['user_id'])) {
|
|
header('Location: index.php');
|
|
exit;
|
|
}
|
|
|
|
$role = $_SESSION['role'];
|
|
$userName = $_SESSION['user_name'];
|
|
$userId = $_SESSION['user_id'];
|
|
|
|
// Get assigned processes for worker
|
|
$assignedProcesses = [];
|
|
if ($role === 'worker') {
|
|
$stmt = $db->prepare("SELECT assigned_processes FROM users WHERE id = ?");
|
|
$stmt->execute([$userId]);
|
|
$res = $stmt->fetch();
|
|
$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">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>M-TRACK | Dashboard</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;
|
|
--success: #10b981;
|
|
--warning: #f59e0b;
|
|
--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;
|
|
}
|
|
.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);
|
|
}
|
|
.main-content {
|
|
margin-left: var(--sidebar-width);
|
|
padding: 2.5rem;
|
|
}
|
|
.top-bar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 2rem;
|
|
}
|
|
.top-bar h1 {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
color: var(--primary);
|
|
margin: 0;
|
|
}
|
|
.user-pill {
|
|
background: white;
|
|
border: 1px solid var(--border);
|
|
padding: 0.375rem 1rem;
|
|
border-radius: 9999px;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.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;
|
|
}
|
|
.card-header {
|
|
background: white;
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 1rem 1.25rem;
|
|
font-weight: 600;
|
|
font-size: 0.875rem;
|
|
color: var(--primary);
|
|
}
|
|
.badge-process {
|
|
background: #e2e8f0;
|
|
color: #475569;
|
|
font-weight: 600;
|
|
font-size: 0.75rem;
|
|
padding: 0.25rem 0.625rem;
|
|
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>
|
|
|
|
<div class="sidebar">
|
|
<h2>M-TRACK</h2>
|
|
<nav class="nav nav-pills flex-column">
|
|
<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="jobs.php"><i class="bi bi-briefcase me-2"></i> Jobs</a>
|
|
<a class="nav-link" href="shop_floor.php"><i class="bi bi-kanban me-2"></i> Shop Floor</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>
|
|
<a class="nav-link" href="time_study.php"><i class="bi bi-clock-history me-2"></i> Time Study</a>
|
|
<a class="nav-link" href="scan.php"><i class="bi bi-upc-scan me-2"></i> Scan</a>
|
|
<?php else: ?>
|
|
<a class="nav-link" href="dashboard.php"><i class="bi bi-list-task me-2"></i> My Queue</a>
|
|
<a class="nav-link" href="shop_floor.php"><i class="bi bi-kanban me-2"></i> Board View</a>
|
|
<a class="nav-link" href="scan.php"><i class="bi bi-upc-scan me-2"></i> Scan</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>
|
|
</nav>
|
|
</div>
|
|
|
|
<div class="main-content">
|
|
<div class="top-bar">
|
|
<h1><?= $role === 'admin' ? 'Operations Overview' : 'Work Queue' ?></h1>
|
|
<div class="user-pill">
|
|
<i class="bi bi-person-circle text-muted"></i>
|
|
<?= htmlspecialchars($userName) ?>
|
|
<span class="text-muted text-uppercase" style="font-size: 0.625rem;"><?= $role ?></span>
|
|
</div>
|
|
</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">
|
|
<?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>
|
|
</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"><?= $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"><?= $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 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"><?= $stats['active_workers'] ?></div>
|
|
</div>
|
|
</div>
|
|
</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>
|