Autosave: 20260227-211046
This commit is contained in:
parent
6d06eea56a
commit
4349e548ed
115
api/ops.php
Normal file
115
api/ops.php
Normal 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()]);
|
||||
}
|
||||
292
dashboard.php
292
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();
|
||||
}
|
||||
|
||||
?>
|
||||
<!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>
|
||||
108
db/migrate_002_manufacturing_core.php
Normal file
108
db/migrate_002_manufacturing_core.php
Normal 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
264
inventory.php
Normal 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
394
jobs.php
Normal 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
255
users.php
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user