Autosave: 20260228-223337

This commit is contained in:
Flatlogic Bot 2026-02-28 22:33:39 +00:00
parent 951a96262b
commit e069921c28
42 changed files with 32510 additions and 144 deletions

244
api/import_bom.php Normal file
View File

@ -0,0 +1,244 @@
<?php
session_start();
require_once __DIR__ . '/../db/config.php';
header('Content-Type: application/json');
if (!isset($_SESSION['user_id']) || $_SESSION['role'] !== 'admin') {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_FILES['bomFile'])) {
http_response_code(400);
echo json_encode(['error' => 'No BOM file provided']);
exit;
}
$file = $_FILES['bomFile'];
if ($file['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
echo json_encode(['error' => 'File upload error']);
exit;
}
$jobName = $_POST['jobName'] ?? pathinfo($file['name'], PATHINFO_FILENAME);
$existingJobId = $_POST['jobId'] ?? null;
$zip = new ZipArchive();
if ($zip->open($file['tmp_name']) !== TRUE) {
http_response_code(400);
echo json_encode(['error' => 'Invalid XLSX file format']);
exit;
}
try {
$db = db();
$db->beginTransaction();
// 1. Create or Select Job
if ($existingJobId) {
$stmt = $db->prepare("SELECT id FROM jobs WHERE id = ?");
$stmt->execute([$existingJobId]);
if (!$stmt->fetch()) throw new Exception("Job not found");
$jobId = $existingJobId;
} else {
$stmt = $db->prepare("INSERT INTO jobs (name, status) VALUES (?, 'planned')");
$stmt->execute([$jobName]);
$jobId = $db->lastInsertId();
}
// 2. Read Shared Strings
$sharedStringsRaw = $zip->getFromName('xl/sharedStrings.xml');
$strings = [];
if ($sharedStringsRaw) {
$xml = @simplexml_load_string($sharedStringsRaw);
if ($xml && isset($xml->si)) {
foreach ($xml->si as $si) {
$t = '';
if (isset($si->t)) $t = (string)$si->t;
elseif (isset($si->r)) foreach ($si->r as $r) $t .= (string)$r->t;
$strings[] = $t;
}
}
}
// 3. Extract Image Anchors (Row -> Image Path)
$rowImages = [];
$drawingRelsRaw = $zip->getFromName('xl/drawings/_rels/drawing1.xml.rels');
$rels = [];
if ($drawingRelsRaw) {
$xml = @simplexml_load_string($drawingRelsRaw);
if ($xml) {
foreach ($xml->Relationship as $rel) {
$rels[(string)$rel['Id']] = (string)$rel['Target'];
}
}
}
$drawingRaw = $zip->getFromName('xl/drawings/drawing1.xml');
if ($drawingRaw) {
$xml = @simplexml_load_string($drawingRaw);
if ($xml) {
$xml->registerXPathNamespace('xdr', 'http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing');
$xml->registerXPathNamespace('a', 'http://schemas.openxmlformats.org/drawingml/2006/main');
$anchors = $xml->xpath('//xdr:twoCellAnchor | //xdr:oneCellAnchor');
if ($anchors) {
foreach ($anchors as $anchor) {
$from = $anchor->xpath('.//xdr:from/xdr:row');
$blip = $anchor->xpath('.//a:blip');
if (!$from || !$blip) continue;
$row = (int)$from[0];
$embedAttr = $blip[0]->attributes('r', true);
if (!$embedAttr) {
// try to get without true for prefix if namespaced correctly in some versions
$embedAttr = $blip[0]->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships');
}
if ($embedAttr && isset($embedAttr['embed'])) {
$rId = (string)$embedAttr['embed'];
if (isset($rels[$rId])) {
$target = str_replace('../', 'xl/', $rels[$rId]);
$rowImages[$row] = $target; // $row is 0-indexed in XML
}
}
}
}
}
}
// 4. Read Rows
$sheetRaw = $zip->getFromName('xl/worksheets/sheet1.xml');
if (!$sheetRaw) throw new Exception("Could not find sheet1.xml");
$xml = @simplexml_load_string($sheetRaw);
if (!$xml) throw new Exception("Failed to parse sheet1.xml");
$headerMap = [];
$componentsMap = []; // '1.6' => db id
$rowIndex = 0;
$stmtComp = $db->prepare("
INSERT INTO components (job_id, parent_id, name, type, quantity, thickness, material, notes, thumbnail_data, order_index)
VALUES (?, ?, ?, 'part', ?, ?, ?, ?, ?, ?)
");
if (isset($xml->sheetData->row)) {
foreach ($xml->sheetData->row as $row) {
$rowData = [];
$colIndex = 0;
// Handle sparse columns
foreach ($row->c as $c) {
// Excel cell reference like "A1", "C2"
$cellRef = (string)$c['r'];
$colLetter = preg_replace('/[0-9]/', '', $cellRef);
// Convert colLetter (A, B, C...) to index (0, 1, 2...)
$colNum = 0;
$len = strlen($colLetter);
for ($i = 0; $i < $len; $i++) {
$colNum = $colNum * 26 + (ord($colLetter[$i]) - 64);
}
$colIndex = $colNum - 1;
$val = (string)$c->v;
if (isset($c['t']) && $c['t'] == 's') {
$val = $strings[(int)$val] ?? $val;
}
$rowData[$colIndex] = $val;
}
// Ensure all indexes exist up to max column
if (!empty($rowData)) {
$maxKey = max(array_keys($rowData));
for ($i = 0; $i <= $maxKey; $i++) {
if (!isset($rowData[$i])) $rowData[$i] = '';
}
ksort($rowData);
}
if ($rowIndex === 0) {
foreach ($rowData as $idx => $val) {
$headerMap[trim(strtolower($val))] = $idx;
}
} else if (!empty($rowData)) {
// Data row
$itemIdx = $headerMap['item'] ?? 0;
$item = $rowData[$itemIdx] ?? '';
$partIdx = $headerMap['part number'] ?? ($headerMap['filename'] ?? -1);
$partNumber = $partIdx >= 0 ? ($rowData[$partIdx] ?? 'Unknown Part') : 'Unknown Part';
if (empty($item) && $partNumber === 'Unknown Part') {
$rowIndex++;
continue; // Skip empty
}
$qtyIdx = $headerMap['qty'] ?? 6;
$qty = $rowData[$qtyIdx] ?? 1;
$thickIdx = $headerMap['thickness'] ?? -1;
$thickness = $thickIdx >= 0 ? ($rowData[$thickIdx] ?? '') : '';
$thickness = str_replace(' in', '"', trim($thickness)); // convert inches
$matIdx = $headerMap['material'] ?? -1;
$material = $matIdx >= 0 ? ($rowData[$matIdx] ?? '') : '';
$descIdx = $headerMap['description'] ?? -1;
$notes = $descIdx >= 0 ? ($rowData[$descIdx] ?? '') : '';
// Thumbnail
$thumbnailData = null;
if (isset($rowImages[$rowIndex])) {
$imgData = $zip->getFromName($rowImages[$rowIndex]);
if ($imgData) {
// guess mime type based on extension
$ext = strtolower(pathinfo($rowImages[$rowIndex], PATHINFO_EXTENSION));
$mime = ($ext == 'png') ? 'image/png' : 'image/jpeg';
$thumbnailData = "data:$mime;base64," . base64_encode($imgData);
}
}
// Figure out parent
$parentId = null;
$parts = explode('.', (string)$item);
if (count($parts) > 1) {
array_pop($parts); // remove last segment
$parentItem = implode('.', $parts);
if (isset($componentsMap[$parentItem])) {
$parentId = $componentsMap[$parentItem];
}
}
$stmtComp->execute([
$jobId,
$parentId,
$partNumber,
(int)$qty,
$thickness,
$material,
$notes,
$thumbnailData,
$rowIndex // order index
]);
$compId = $db->lastInsertId();
$componentsMap[(string)$item] = $compId;
}
$rowIndex++;
}
}
$zip->close();
$db->commit();
echo json_encode(['success' => true, 'job_id' => $jobId, 'message' => "Imported successfully!"]);
} catch (Exception $e) {
if (isset($db)) $db->rollBack();
http_response_code(500);
echo json_encode(['error' => 'Import failed: ' . $e->getMessage()]);
}

49
api/test_bom_parser.php Normal file
View File

@ -0,0 +1,49 @@
<?php
$zip = new ZipArchive();
if ($zip->open(__DIR__ . "/../R6 BOM.xlsx") === TRUE) {
// 1. Get drawing relationships
$drawingRelsRaw = $zip->getFromName('xl/drawings/_rels/drawing1.xml.rels');
$rels = [];
if ($drawingRelsRaw) {
$xml = simplexml_load_string($drawingRelsRaw);
foreach ($xml->Relationship as $rel) {
$rels[(string)$rel['Id']] = (string)$rel['Target'];
}
}
// 2. Get drawing anchors to map row -> image path
$drawingRaw = $zip->getFromName('xl/drawings/drawing1.xml');
$rowImages = [];
if ($drawingRaw) {
$xml = simplexml_load_string($drawingRaw);
$xml->registerXPathNamespace('xdr', 'http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing');
$xml->registerXPathNamespace('a', 'http://schemas.openxmlformats.org/drawingml/2006/main');
$xml->registerXPathNamespace('r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships');
$anchors = $xml->xpath('//xdr:twoCellAnchor | //xdr:oneCellAnchor');
foreach ($anchors as $anchor) {
$from = $anchor->xpath('.//xdr:from/xdr:row');
if (!$from) continue;
$row = (int)$from[0];
$blip = $anchor->xpath('.//a:blip');
if (!$blip) continue;
$embedAttr = $blip[0]->attributes('r', true);
$rId = (string)$embedAttr['embed'];
if (isset($rels[$rId])) {
$target = $rels[$rId]; // e.g. ../media/image1.jpg
$target = str_replace('../', 'xl/', $target);
$rowImages[$row] = $target; // 0-indexed row
}
}
}
// Check first 5 row images
foreach (array_slice($rowImages, 0, 5, true) as $r => $img) {
echo "Row " . ($r + 1) . " has image $img\n";
}
$zip->close();
}

34
api/test_xlsx.php Normal file
View File

@ -0,0 +1,34 @@
<?php
$zip = new ZipArchive();
if ($zip->open(__DIR__ . "/../R6 BOM.xlsx") === TRUE) {
$sharedStringsRaw = $zip->getFromName('xl/sharedStrings.xml');
$strings = [];
if ($sharedStringsRaw) {
$xml = simplexml_load_string($sharedStringsRaw);
foreach ($xml->si as $si) {
$t = '';
if (isset($si->t)) $t = (string)$si->t;
elseif (isset($si->r)) foreach ($si->r as $r) $t .= (string)$r->t;
$strings[] = $t;
}
}
$sheetRaw = $zip->getFromName('xl/worksheets/sheet1.xml');
$xml = simplexml_load_string($sheetRaw);
$rows = 0;
foreach ($xml->sheetData->row as $row) {
$rowData = [];
foreach ($row->c as $c) {
$val = (string)$c->v;
if (isset($c['t']) && $c['t'] == 's') {
$val = $strings[(int)$val];
}
$rowData[] = $val;
}
echo implode(" | ", $rowData) . "\n";
$rows++;
if ($rows > 5) break;
}
$zip->close();
}

26
batch_task_schema.json Normal file
View File

@ -0,0 +1,26 @@
{
"groupKey": "rolling|MCR Steel|0.063 in|7",
"processType": "rolling",
"material": "MCR Steel",
"thickness": "0.063 in",
"jobId": 7,
"jobName": "R6",
"customerName": "Gillman",
"serialNumber": "9876",
"mainAssembly": "Chaff Collector",
"subAssembly": "Frame",
"operations": [
{
"operationId": 53,
"componentId": 1379,
"componentName": "Frame- Cylinder Upper",
"quantity": 1,
"status": "pending",
"stallReason": null,
"assignedTo": null
}
],
"totalCount": 1,
"pendingCount": 1,
"completedCount": 0
}

259
bom_import.php Normal file
View File

@ -0,0 +1,259 @@
<?php
session_start();
require 'db/config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['role'] !== 'admin') {
header('Location: index.php');
exit;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>M-TRACK | Import BOM</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.5/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;
left: 0;
height: 100vh;
background: var(--primary);
color: white;
padding: 2rem 1rem;
overflow-y: auto;
}
.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);
}
.drop-zone {
border: 2px dashed #3b82f6;
border-radius: 10px;
padding: 40px;
text-align: center;
background: white;
cursor: pointer;
transition: all 0.3s ease;
}
.drop-zone.dragover {
background: #eff6ff;
border-color: #2563eb;
}
.drop-zone i {
font-size: 3rem;
color: #3b82f6;
}
#file-input { display: none; }
.card { border: 1px solid var(--border); box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
</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="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>
<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>Import BOM</h1>
<a href="jobs.php" class="btn btn-outline-secondary"><i class="bi bi-arrow-left"></i> Back to Jobs</a>
</div>
<div class="row">
<div class="col-md-8 col-lg-6">
<div class="card bg-white p-4">
<?php $existingJobId = $_GET["job_id"] ?? ""; ?>
<form id="bomImportForm">
<?php if ($existingJobId): ?>
<input type="hidden" id="jobId" value="<?= htmlspecialchars($existingJobId) ?>">
<div class="alert alert-info">
<strong><i class="bi bi-info-circle"></i> Importing into existing job (ID: <?= htmlspecialchars($existingJobId) ?>)</strong>
</div>
<?php else: ?>
<div class="mb-3">
<label class="form-label fw-bold">Job Name (Optional)</label>
<input type="text" id="jobName" class="form-control" placeholder="Leave blank to use filename">
</div>
<?php endif; ?>
<div class="mb-4">
<label class="form-label fw-bold">BOM Excel File (.xlsx)</label>
<div class="drop-zone" id="dropZone" onclick="document.getElementById('file-input').click()">
<i class="bi bi-cloud-arrow-up mb-3 d-block"></i>
<h5>Click to select or drag and drop here</h5>
<p class="text-muted mb-0">Only .xlsx files are supported</p>
<input type="file" id="file-input" accept=".xlsx" required>
</div>
<div id="file-name-display" class="mt-2 text-primary fw-bold text-center" style="display:none;"></div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg" id="uploadBtn" disabled>
<i class="bi bi-upload"></i> Import BOM Now
</button>
</div>
</form>
<div id="uploadStatus" class="mt-4" style="display:none;">
<div class="alert alert-info d-flex align-items-center">
<div class="spinner-border spinner-border-sm me-3" role="status"></div>
<div>Parsing Excel file and extracting images natively... Please wait.</div>
</div>
</div>
<div id="uploadError" class="mt-4 alert alert-danger" style="display:none;"></div>
<div id="uploadSuccess" class="mt-4 alert alert-success" style="display:none;"></div>
</div>
<div class="card mt-4 p-3 bg-light text-muted small">
<h6 class="fw-bold mb-2"><i class="bi bi-info-circle text-info"></i> How it works</h6>
<ul class="mb-0 ps-3">
<li>The system parses the <strong>Item</strong> column (e.g. 1, 1.6, 1.6.1) to build the assembly tree perfectly matching the Replit behavior.</li>
<li>Extracts standard fields like Part Number, QTY, Thickness, Material, and Description.</li>
<li>Natively extracts embedded thumbnail images from the Excel archive and attaches them!</li>
</ul>
</div>
</div>
</div>
</div>
<script>
const fileInput = document.getElementById('file-input');
const dropZone = document.getElementById('dropZone');
const fileNameDisplay = document.getElementById('file-name-display');
const uploadBtn = document.getElementById('uploadBtn');
const form = document.getElementById('bomImportForm');
const uploadStatus = document.getElementById('uploadStatus');
const uploadError = document.getElementById('uploadError');
const uploadSuccess = document.getElementById('uploadSuccess');
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
if (e.dataTransfer.files.length) {
fileInput.files = e.dataTransfer.files;
handleFileSelect();
}
});
fileInput.addEventListener('change', handleFileSelect);
function handleFileSelect() {
if (fileInput.files.length > 0) {
fileNameDisplay.textContent = 'Selected: ' + fileInput.files[0].name;
fileNameDisplay.style.display = 'block';
uploadBtn.disabled = false;
}
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (fileInput.files.length === 0) return;
uploadBtn.disabled = true;
uploadStatus.style.display = 'block';
uploadError.style.display = 'none';
uploadSuccess.style.display = 'none';
const formData = new FormData();
formData.append('bomFile', fileInput.files[0]);
const jobIdEl = document.getElementById('jobId');
if (jobIdEl) {
formData.append('jobId', jobIdEl.value);
}
const jobNameEl = document.getElementById('jobName');
if (jobNameEl && jobNameEl.value.trim()) {
formData.append('jobName', jobNameEl.value.trim());
}
try {
const response = await fetch('api/import_bom.php', {
method: 'POST',
body: formData
});
const result = await response.json();
if (response.ok && result.success) {
uploadStatus.style.display = 'none';
uploadSuccess.textContent = result.message + ' Redirecting...';
uploadSuccess.style.display = 'block';
setTimeout(() => {
window.location.href = 'jobs.php?id=' + result.job_id;
}, 1500);
} else {
throw new Error(result.error || 'Upload failed');
}
} catch (err) {
uploadStatus.style.display = 'none';
uploadBtn.disabled = false;
uploadError.textContent = err.message;
uploadError.style.display = 'block';
}
});
</script>
</body>
</html>

19
component_schema.json Normal file
View File

@ -0,0 +1,19 @@
{
"id": 1,
"name": "Steel Tube cutting",
"type": "machining",
"processes": [],
"status": "completed",
"quantity": 1,
"jobId": 1,
"parentId": null,
"assignedTo": "1571ac82-0762-4763-83e7-a216a6f3ed82",
"notes": "Cut to 1500mm length",
"thickness": null,
"material": null,
"thumbnailData": null,
"blueprintUrl": null,
"orderIndex": 0,
"priorityWeight": 50,
"readyForAssembly": false
}

1
components.json Normal file

File diff suppressed because one or more lines are too long

1
consumables.json Normal file
View File

@ -0,0 +1 @@
[{"id":1,"name":"TIG Welding Rods","quantity":50,"reorderLevel":20,"unit":"kg"}]

View File

@ -227,10 +227,15 @@ if ($role === 'worker') {
<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>

View File

@ -1,31 +0,0 @@
<?php
require 'db/config.php';
$db = db();
try {
$db->exec("CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
role ENUM('worker', 'admin') NOT NULL,
pin_hash VARCHAR(255) DEFAULT NULL,
assigned_processes JSON DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");
$stmt = $db->query("SELECT COUNT(*) FROM users");
if ($stmt->fetchColumn() == 0) {
$db->prepare("INSERT INTO users (name, role, assigned_processes) VALUES (?, ?, ?)")
->execute(['John Operator', 'worker', json_encode(['cutting', 'welding'])]);
$db->prepare("INSERT INTO users (name, role, assigned_processes) VALUES (?, ?, ?)")
->execute(['Sarah Smith', 'worker', json_encode(['bending', 'assembly'])]);
// Admin user
$db->prepare("INSERT INTO users (name, role) VALUES (?, ?)")
->execute(['Mike Manager', 'admin']);
echo "Users table created and seeded.\n";
} else {
echo "Users table already exists and is not empty.\n";
}
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}

View File

@ -1,108 +0,0 @@
<?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";
}

View File

@ -0,0 +1,162 @@
<?php
require_once __DIR__ . '/config.php';
$db = db();
try {
// Disable foreign key checks for dropping tables
$db->exec("SET FOREIGN_KEY_CHECKS=0");
// Drop existing tables if we want a clean slate
$tables = ['time_study_events', 'inventory_transactions', 'inventory', 'operations', 'components', 'jobs', 'users', 'process_types', 'subtasks', 'reorder_alerts', 'machined_parts'];
foreach ($tables as $table) {
$db->exec("DROP TABLE IF EXISTS `$table`");
}
// 1. Users (Admins and Workers)
$db->exec("CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
uuid VARCHAR(36) UNIQUE,
name VARCHAR(255) NOT NULL,
role ENUM('worker', 'admin') NOT NULL,
pin_hash VARCHAR(255) DEFAULT NULL,
assigned_processes JSON DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");
// 2. Process Types (System defaults)
$db->exec("CREATE TABLE process_types (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE,
description TEXT DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");
// 3. Jobs (Projects/Orders)
$db->exec("CREATE TABLE jobs (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
customer VARCHAR(255) DEFAULT NULL,
quantity INT DEFAULT 1,
due_date DATE DEFAULT NULL,
serial_number VARCHAR(100) UNIQUE,
priority ENUM('low', 'normal', 'high', 'urgent') DEFAULT 'normal',
status ENUM('planned', 'in_progress', 'completed', 'archived', 'template') DEFAULT 'planned',
description TEXT DEFAULT NULL,
is_template BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at DATETIME DEFAULT NULL
)");
// 4. Components (BOM Tree)
$db->exec("CREATE TABLE components (
id INT AUTO_INCREMENT PRIMARY KEY,
job_id INT NOT NULL,
parent_id INT DEFAULT NULL,
name VARCHAR(255) NOT NULL,
type VARCHAR(100) DEFAULT 'part',
status ENUM('pending', 'in_progress', 'completed') DEFAULT 'pending',
quantity INT DEFAULT 1,
thickness VARCHAR(50) DEFAULT NULL,
material VARCHAR(100) DEFAULT NULL,
notes TEXT DEFAULT NULL,
thumbnail_data LONGTEXT DEFAULT NULL,
blueprint_url VARCHAR(255) DEFAULT NULL,
order_index INT DEFAULT 0,
priority_weight INT DEFAULT 50,
assigned_to INT DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at DATETIME DEFAULT NULL,
FOREIGN KEY (job_id) REFERENCES jobs(id) ON DELETE CASCADE,
FOREIGN KEY (parent_id) REFERENCES components(id) ON DELETE CASCADE,
FOREIGN KEY (assigned_to) REFERENCES users(id) ON DELETE SET NULL
)");
// 5. Operations (Tasks within a Component)
$db->exec("CREATE TABLE operations (
id INT AUTO_INCREMENT PRIMARY KEY,
component_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
process_type VARCHAR(100) NOT NULL,
status ENUM('pending', 'in_progress', 'stalled', 'completed') DEFAULT 'pending',
order_index INT DEFAULT 0,
estimated_minutes INT DEFAULT NULL,
instructions TEXT DEFAULT NULL,
assigned_worker_id INT DEFAULT NULL,
quantity_made INT DEFAULT 0,
fulfilled_from_inventory BOOLEAN DEFAULT FALSE,
priority_tier ENUM('low', 'normal', 'high', 'urgent') DEFAULT 'normal',
blocked_reason TEXT DEFAULT NULL,
start_time DATETIME DEFAULT NULL,
completed_at DATETIME 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
)");
// 6. Subtasks (Checklists for Operations/Components)
$db->exec("CREATE TABLE subtasks (
id INT AUTO_INCREMENT PRIMARY KEY,
component_id INT NOT NULL,
operation_id INT DEFAULT NULL,
name VARCHAR(255) NOT NULL,
status ENUM('pending', 'completed') DEFAULT 'pending',
FOREIGN KEY (component_id) REFERENCES components(id) ON DELETE CASCADE,
FOREIGN KEY (operation_id) REFERENCES operations(id) ON DELETE CASCADE
)");
// 7. Inventory (Materials & Consumables)
$db->exec("CREATE TABLE inventory (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
category ENUM('material', 'consumable', 'hardware', 'machined_part') 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
)");
// 8. Reorder Alerts
$db->exec("CREATE TABLE reorder_alerts (
id INT AUTO_INCREMENT PRIMARY KEY,
inventory_id INT NOT NULL,
status ENUM('active', 'ordered', 'dismissed', 'fulfilled') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (inventory_id) REFERENCES inventory(id) ON DELETE CASCADE
)");
// 9. Time Study Events (Analytics)
$db->exec("CREATE TABLE 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)
)");
$db->exec("SET FOREIGN_KEY_CHECKS=1");
// SEED ONLY SYSTEM DATA (No user-added records)
// Default Admin User (Pin: 1234 hash)
$defaultPinHash = password_hash('1234', PASSWORD_DEFAULT);
$db->prepare("INSERT INTO users (name, role, pin_hash) VALUES (?, ?, ?)")
->execute(['System Admin', 'admin', $defaultPinHash]);
// Default Process Types
$processes = [
'Cutting', 'Welding', 'Bending', 'Assembly', 'Painting', 'Quality Control', 'Packaging', 'Machining'
];
$stmt = $db->prepare("INSERT INTO process_types (name) VALUES (?)");
foreach ($processes as $p) {
$stmt->execute([$p]);
}
echo "Full schema migrated successfully without user data. Admin PIN is 1234.\n";
} catch (Exception $e) {
$db->rollBack();
echo "Error: " . $e->getMessage() . "\n";
}

View File

@ -114,6 +114,8 @@ $lowStock = array_filter($inventory, fn($i) => $i['stock_level'] <= $i['reorder_
<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>
<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>
<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>

18
job_schema.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -20,7 +20,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_job'])) {
try {
$stmt = $db->prepare("INSERT INTO jobs (name, quantity, due_date, serial_number) VALUES (?, ?, ?, ?)");
$stmt->execute([$name, $qty, $due, $sn]);
$stmt->execute([$name, $qty, empty($due) ? null : $due, empty($sn) ? null : $sn]);
$jobId = $db->lastInsertId();
header("Location: jobs.php?id=$jobId");
exit;
@ -176,19 +176,31 @@ if ($currentJobId) {
<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="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>
<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">
<?php if (!empty($error)): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?= htmlspecialchars($error) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<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>
<a href="bom_import.php" class="btn btn-outline-success ms-2">
<i class="bi bi-file-earmark-excel me-1"></i> Import BOM
</a>
</div>
<div class="row">
@ -229,15 +241,32 @@ if ($currentJobId) {
<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>
<a href="bom_import.php?job_id=<?= $currentJobId ?>" class="btn btn-sm btn-outline-success me-2">
<i class="bi bi-file-earmark-excel me-1"></i> Upload BOM
</a>
<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>
</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>
<div class="d-flex align-items-center">
<?php if (!empty($comp["thumbnail_data"])): ?>
<img src="<?= $comp["thumbnail_data"] ?>" style="height:32px; width:32px; object-fit:cover; border-radius:4px; margin-right:10px; border:1px solid #dee2e6;">
<?php else: ?>
<div style="height:32px; width:32px; border-radius:4px; margin-right:10px; background:#e2e8f0; display:flex; align-items:center; justify-content:center; border:1px solid #cbd5e1;"><i class="bi bi-box" style="color:#94a3b8;"></i></div>
<?php endif; ?>
<div>
<h6 class="mb-0 fw-bold text-primary"><?= htmlspecialchars($comp["name"]) ?></h6>
<?php if(!empty($comp["material"]) || !empty($comp["thickness"])): ?>
<small class="text-muted"><?= htmlspecialchars($comp["material"]) ?> <?= htmlspecialchars($comp["thickness"]) ?></small>
<?php endif; ?>
</div>
</div>
<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>
@ -317,6 +346,7 @@ if ($currentJobId) {
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<a href="bom_import.php" class="btn btn-outline-success me-auto">Import BOM Instead</a>
<button type="submit" name="create_job" class="btn btn-primary">Create Job</button>
</div>
</form>

21
operation_schema.json Normal file
View File

@ -0,0 +1,21 @@
{
"id": 1,
"componentId": 28,
"name": "sheetmetal cutting",
"processType": "sheetmetal cutting",
"orderIndex": 0,
"estimatedMinutes": null,
"instructions": null,
"status": "pending",
"assignedTo": null,
"completedAt": null,
"quantityMade": null,
"fulfilledFromInventory": false,
"priorityScore": null,
"priorityTier": "normal",
"readyAt": null,
"blockedReason": null,
"overriddenByUserId": null,
"overrideReason": null,
"overrideExpiresAt": null
}

1
operations.json Normal file

File diff suppressed because one or more lines are too long

16
replit_app_summary.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
<link rel="icon" type="image/png" href="/favicon.png" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Architects+Daughter&family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Fira+Code:wght@300..700&family=Geist+Mono:wght@100..900&family=Geist:wght@100..900&family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Lora:ital,wght@0,400..700;1,400..700&family=Merriweather:ital,opsz,wght@0,18..144,300..900;1,18..144,300..900&family=Montserrat:ital,wght@0,100..900;1,100..900&family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Outfit:wght@100..900&family=Oxanium:wght@200..800&family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100..900;1,100..900&family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&family=Space+Grotesk:wght@300..700&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
<script type="module" crossorigin src="/assets/index-fCLaIajI.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-aOXWrruY.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -0,0 +1,808 @@
<!DOCTYPE html>
<html><head><meta charset='utf-8'><title>M-TRACK App Summary</title>
<style>
@media print { @page { margin: 0.75in; } }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; font-size: 11pt; line-height: 1.6; color: #1a1a1a; max-width: 900px; margin: 0 auto; padding: 20px; }
h1 { font-size: 22pt; border-bottom: 3px solid #2563eb; padding-bottom: 8px; color: #111; }
h2 { font-size: 16pt; border-bottom: 1px solid #ddd; padding-bottom: 4px; margin-top: 28px; color: #1e40af; }
h3 { font-size: 13pt; margin-top: 20px; color: #333; }
code { background: #f3f4f6; padding: 1px 4px; border-radius: 3px; font-size: 10pt; }
pre { background: #f3f4f6; padding: 12px; border-radius: 6px; overflow-x: auto; font-size: 9pt; border: 1px solid #e5e7eb; }
pre code { background: none; padding: 0; }
ul, ol { padding-left: 24px; }
li { margin-bottom: 4px; }
hr { border: none; border-top: 2px solid #e5e7eb; margin: 24px 0; }
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 10pt; }
th, td { border: 1px solid #d1d5db; padding: 6px 10px; text-align: left; }
th { background: #f3f4f6; font-weight: 600; }
.print-btn { position: fixed; top: 20px; right: 20px; background: #2563eb; color: white; border: none; padding: 12px 24px; font-size: 14pt; border-radius: 8px; cursor: pointer; z-index: 1000; box-shadow: 0 2px 8px rgba(0,0,0,0.2); }
.print-btn:hover { background: #1d4ed8; }
@media print { .print-btn { display: none; } }
</style></head><body>
<button class="print-btn" onclick="window.print()">Save as PDF</button><h1>M-TRACK Manufacturing Control System - Complete Application Summary</h1>
<h2>Purpose</h2>
<p>M-TRACK is an internal manufacturing control system for a shop floor environment. It manages jobs, components (parts), operations (work steps), inventory, and worker task assignments. The app is designed for two user types: <strong>Admins</strong> (managers who create jobs, assign work, and monitor production) and <strong>Workers</strong> (shop floor operators who execute tasks).</p>
<p>The system is self-hosted on a Windows machine and accessed remotely via web browser (including mobile devices on the shop floor).</p>
<hr>
<h2>Deployment</h2>
<ul>
<li>Self-hosted on a Windows machine</li>
<li>Remote web access via browser (desktop and mobile)</li>
<li>PostgreSQL database for data persistence</li>
<li>Single server process serves both the API and the frontend</li>
<li>Environment variable <code>DATABASE_URL</code> for database connection</li>
<li>Environment variable <code>SESSION_SECRET</code> for session encryption</li>
</ul>
<hr>
<h2>Authentication &amp; Access Control</h2>
<h3>Two-Tier Login System</h3>
<ul>
<li><strong>Workers</strong>: Select their name from a dropdown on the landing page. No password required. Instant login for minimal friction on the shop floor.</li>
<li><strong>Admins</strong>: Select their name, then must enter a 4-6 digit numeric PIN.</li>
</ul>
<h3>PIN Management</h3>
<ul>
<li>PINs are stored as bcrypt-hashed values in the database</li>
<li>Admins can set, reset, and initialize PINs for other admin users</li>
<li>A &quot;Bootstrap&quot; mode exists for first-time setup: if no admin has a PIN configured, the landing page shows a setup dialog to create/initialize the first admin account</li>
</ul>
<h3>Sessions</h3>
<ul>
<li>Express-session based with 24-hour expiration</li>
<li>Session data stored server-side (MemoryStore for development, database-backed sessions table available for production)</li>
</ul>
<h3>Role-Based Access</h3>
<ul>
<li>Two roles: <code>admin</code> and <code>worker</code></li>
<li>Workers can only use the simplified login flow (no PIN)</li>
<li>Workers have an <code>assignedProcesses</code> array (e.g., [&quot;cutting&quot;, &quot;welding&quot;]) that filters which operations they see in their work queue</li>
<li>Admin-only features: User management, inventory deletion, PIN management, time study reports, process type configuration, viewing all workers&#39; queues</li>
</ul>
<hr>
<h2>Core Data Model</h2>
<h3>Jobs</h3>
<p>A job represents a manufacturing order (e.g., building a specific machine for a customer).</p>
<p>Fields:</p>
<ul>
<li>Product name (the thing being built)</li>
<li>Customer name</li>
<li>Order number</li>
<li>Machine serial number</li>
<li>Description and details (free text)</li>
<li>Status: planned, in_progress, completed</li>
<li>Quantity (how many units to build, default 1)</li>
<li>Priority: integer 1-5 (1 = highest)</li>
<li>Due date</li>
<li>Is template flag (templates are reusable job blueprints)</li>
<li>Archived at timestamp (soft delete)</li>
<li>BOM file name and BOM file data (stored for re-download)</li>
</ul>
<h3>Components</h3>
<p>A component is a part or subassembly within a job. Components form a tree hierarchy (parent-child relationships).</p>
<p>Fields:</p>
<ul>
<li>Name</li>
<li>Type: &quot;material&quot; (raw part) or &quot;assembly&quot; (subassembly containing other parts)</li>
<li>Processes: array of strings (e.g., [&quot;cutting&quot;, &quot;bending&quot;, &quot;welding&quot;])</li>
<li>Status: pending, in_progress, completed</li>
<li>Quantity (default 1)</li>
<li>Job ID (belongs to a job)</li>
<li>Parent ID (self-referencing for hierarchy: assemblies contain sub-components)</li>
<li>Assigned to (user ID)</li>
<li>Notes (free text)</li>
<li>Thickness (e.g., &#39;0.125&quot;&#39;, &#39;3mm&#39;)</li>
<li>Material (e.g., &quot;Mild Steel&quot;, &quot;Aluminum 6061&quot;)</li>
<li>Thumbnail data (base64 image extracted from BOM file)</li>
<li>Blueprint URL</li>
<li>Order index (display ordering)</li>
<li>Priority weight (1-100, default 50, higher = more important within the job)</li>
<li>Ready for assembly flag</li>
</ul>
<h3>Operations</h3>
<p>An operation is a specific work step on a component (e.g., &quot;Cut this part&quot;, &quot;Weld this part&quot;).</p>
<p>Fields:</p>
<ul>
<li>Component ID (belongs to a component)</li>
<li>Name (descriptive task name)</li>
<li>Process type (e.g., &quot;cutting&quot;, &quot;Sheetmetal cutting&quot;, &quot;bending&quot;, &quot;welding&quot;, &quot;machining&quot;, &quot;assembly&quot;)</li>
<li>Order index (sequence within the component)</li>
<li>Estimated minutes</li>
<li>Instructions (free text)</li>
<li>Status: pending, in_progress, stalled, completed, skipped</li>
<li>Assigned to (user ID)</li>
<li>Completed at timestamp</li>
<li>Quantity made (for machining operations that produce countable output)</li>
<li>Fulfilled from inventory flag (machining operation fulfilled from existing stock)</li>
<li>Priority score (calculated integer, lower = higher priority)</li>
<li>Priority tier: hot, urgent, normal, backlog (calculated from days until due date)</li>
<li>Ready at timestamp</li>
<li>Blocked reason (text explaining why the operation cannot proceed)</li>
<li>Override fields: overriddenByUserId, overrideReason, overrideExpiresAt (admin can bypass blocking)</li>
</ul>
<h3>Operation Dependencies</h3>
<p>Explicit cross-component blocking relationships.</p>
<p>Fields:</p>
<ul>
<li>Operation ID (the operation that is blocked)</li>
<li>Depends on operation ID (must complete before this operation can start)</li>
<li>Depends on component ID (entire component must complete before this operation can start)</li>
</ul>
<h3>Inventory Items</h3>
<p>Unified inventory system for all materials, manufactured parts, and consumables.</p>
<p>Fields:</p>
<ul>
<li>Item number (auto-assigned by category: consumables 1000+, materials 5000+, manufactured 6000+)</li>
<li>Name, description</li>
<li>Category: material, manufactured, consumable</li>
<li>SKU</li>
<li>Unit: pcs, kg, meters, liters, etc.</li>
<li>Quantity (current stock level)</li>
<li>Restock point (yellow warning threshold)</li>
<li>Reorder point (red critical threshold, triggers automatic alerts)</li>
<li>Reorder quantity (suggested order amount)</li>
<li>Storage location</li>
<li>Supplier name, contact, email</li>
<li>Unit cost</li>
<li>Minimum order quantity</li>
<li>Lead time in days</li>
<li>Last restock date</li>
</ul>
<h3>Inventory Transactions</h3>
<p>Audit trail for every stock change.</p>
<p>Fields:</p>
<ul>
<li>Inventory item ID</li>
<li>Transaction type: restock, consume, adjust, manufacture</li>
<li>Quantity change</li>
<li>Previous quantity and new quantity</li>
<li>Note (reason for change)</li>
<li>User ID (who performed it)</li>
</ul>
<h3>Reorder Alerts</h3>
<p>Tracks inventory replenishment requests.</p>
<p>Fields:</p>
<ul>
<li>Inventory item ID</li>
<li>Status: pending, approved, ordered, received, dismissed</li>
<li>Triggered by: scan (manual QR scan) or auto (hit reorder point)</li>
<li>Scanned by user ID</li>
<li>Approved by user ID, approved at timestamp</li>
<li>Order quantity</li>
<li>Supplier email sent flag and timestamp</li>
<li>Note</li>
</ul>
<h3>Time Study Events</h3>
<p>Performance tracking log.</p>
<p>Fields:</p>
<ul>
<li>Operation ID, component ID, job ID</li>
<li>Worker ID (who performed the work)</li>
<li>Recorded by (who logged the event)</li>
<li>Event type: start, stalled, completed</li>
<li>Note</li>
<li>Occurred at timestamp</li>
</ul>
<h3>Users</h3>
<p>Fields:</p>
<ul>
<li>ID (UUID)</li>
<li>Username (unique, required)</li>
<li>Email, first name, last name, profile image URL (optional)</li>
<li>Role: admin or worker</li>
<li>Assigned processes: array of strings (e.g., [&quot;cutting&quot;, &quot;bending&quot;])</li>
<li>Has completed tutorial: boolean</li>
<li>PIN hash (bcrypt, for admin login)</li>
</ul>
<h3>Process Types</h3>
<p>Configurable list of available manufacturing processes (admin-managed).</p>
<p>Fields:</p>
<ul>
<li>Name (unique, e.g., &quot;cutting&quot;, &quot;Sheetmetal cutting&quot;, &quot;bending&quot;, &quot;welding&quot;, &quot;machining&quot;, &quot;assembly&quot;)</li>
<li>Order index (display ordering)</li>
</ul>
<h3>Other Tables</h3>
<ul>
<li><strong>Subtasks</strong>: Granular checklist items within a component (id, name, status, componentId)</li>
<li><strong>Consumables</strong>: Legacy table for small shop items</li>
<li><strong>Kanban Cards</strong>: Legacy cards for consumable tracking</li>
<li><strong>Machined Parts Inventory</strong>: Tracks stock of machined parts (partName, quantity)</li>
<li><strong>Sessions</strong>: Server-side session storage (sid, sess JSON, expire timestamp)</li>
<li><strong>Operation Assignment Log</strong>: Audit trail for operation assignments (operationId, userId, action, performedByUserId, reason)</li>
</ul>
<hr>
<h2>Key Features</h2>
<h3>1. Job Management</h3>
<p><strong>Creating Jobs:</strong></p>
<ul>
<li>Manually create jobs with product name, customer, serial number, priority, due date, description</li>
<li>Import from BOM (Bill of Materials) Excel files</li>
<li>Instantiate from saved templates</li>
</ul>
<p><strong>Job Templates:</strong></p>
<ul>
<li>Any job can be saved as a template</li>
<li>Templates preserve the full component tree and operation structure</li>
<li>Instantiating a template creates a new active job with all components and operations copied</li>
</ul>
<p><strong>Job Lifecycle:</strong></p>
<ul>
<li>Status progression: planned -&gt; in_progress -&gt; completed</li>
<li>Jobs can be archived (soft delete) and unarchived</li>
<li>Archived jobs are hidden from main views but preserved in the database</li>
</ul>
<p><strong>Job Priority:</strong></p>
<ul>
<li>Integer 1-5 (1 = Critical/highest, 5 = Lowest)</li>
<li>Priority drives the ordering of worker task queues</li>
<li>Combined with due date for automatic priority score calculation</li>
</ul>
<h3>2. BOM Import</h3>
<p><strong>File Format:</strong></p>
<ul>
<li>Accepts Excel files (.xlsx, .xls) and CSV</li>
<li>Uses the <code>xlsx</code> library to read data and <code>exceljs</code> to extract embedded thumbnail images</li>
</ul>
<p><strong>Column Detection:</strong></p>
<ul>
<li>Auto-detects columns by header name using exact and partial matching</li>
<li>Looks for: Item Number, Part Name, Quantity, Material, Thickness</li>
</ul>
<p><strong>Inventor File Name Parsing:</strong></p>
<ul>
<li>Designed to parse Autodesk Inventor file naming conventions</li>
<li>Format: <code>Model_MajorComponent_PartName.ipt</code> or <code>Model_MajorComponent_PartName.iam</code></li>
<li>File extensions (.iam, .ipt) are stripped</li>
<li>CamelCase is split into separate words (e.g., &quot;UpperCylinder&quot; -&gt; &quot;Upper Cylinder&quot;)</li>
<li>Underscores become spaces</li>
<li>Hyphens are preserved with spaces</li>
</ul>
<p><strong>Major Component Codes:</strong></p>
<ul>
<li>Recognizes codes like RB (Roaster Body), CC (Chaff Can), CT (Cooling Tray)</li>
<li>These codes determine which subassembly a part belongs to</li>
</ul>
<p><strong>Two-Pass Component Creation:</strong></p>
<ol>
<li>First pass: Creates subassemblies (items with whole-number IDs like &quot;1&quot;, &quot;2&quot;) as type &quot;assembly&quot;</li>
<li>Second pass: Creates parts (items with decimal IDs like &quot;1.1&quot;, &quot;1.2&quot;) as type &quot;material&quot;, assigned to their parent subassembly</li>
</ol>
<p><strong>Filtering Logic:</strong></p>
<ul>
<li>Parts must have a valid major component prefix (RB, CC, CT) in their file name</li>
<li>Parts must have material containing &quot;MCR&quot; to be imported</li>
<li>This filters out hardware, fasteners, and purchased parts</li>
</ul>
<p><strong>BOM Update:</strong></p>
<ul>
<li>Existing jobs can have their BOM re-imported</li>
<li>The update process preserves existing operations and work progress</li>
<li>New components are added, removed components are deleted, existing ones are updated</li>
<li>Returns counts of updated, added, and removed components</li>
</ul>
<p><strong>Thumbnail Extraction:</strong></p>
<ul>
<li>Embedded images in Excel files are extracted as base64 data</li>
<li>Matched to components by row position</li>
<li>Displayed as component thumbnails in the UI</li>
</ul>
<h3>3. Component Tree &amp; Hierarchy</h3>
<ul>
<li>Components form a tree: Jobs contain top-level assemblies, which contain sub-components</li>
<li>Parent-child relationships via parentId field</li>
<li>Components can be moved between parents via drag or API</li>
<li>Order index controls display ordering within a parent</li>
<li>Assembly components act as containers; their operations are blocked until all child components are completed</li>
</ul>
<h3>4. Operations &amp; Dependencies</h3>
<p><strong>Operation Creation:</strong></p>
<ul>
<li>Operations are created on individual components</li>
<li>Each operation has a process type (cutting, bending, welding, machining, assembly, etc.)</li>
<li>Operations have an order index that determines their sequence within a component</li>
</ul>
<p><strong>Bulk Operations:</strong></p>
<ul>
<li>Admin can assign operations in bulk across multiple components</li>
<li>Select multiple components and multiple process types to create operations for all combinations</li>
<li>Bulk delete also supported</li>
</ul>
<p><strong>Operation Reordering:</strong></p>
<ul>
<li>Operations within a component can be reordered by dragging or via API</li>
<li>Order determines sequential dependency (operation at index 0 must complete before index 1 can start)</li>
</ul>
<p><strong>Dependency System (Three Layers):</strong></p>
<ol>
<li><strong>Sequential</strong>: Within a component, operations must complete in order index sequence</li>
<li><strong>Hierarchical</strong>: Assembly operations are blocked until all child components are completed</li>
<li><strong>Explicit</strong>: Cross-component dependencies defined in the operation_dependencies table</li>
</ol>
<p><strong>Blocking Logic:</strong></p>
<ul>
<li>An operation is &quot;blocked&quot; if any of its dependencies (sequential, hierarchical, or explicit) are not yet completed</li>
<li>Blocked operations appear at the bottom of worker queues with a reason explaining the block</li>
<li>Admins can override blocks by providing an override reason (with optional expiration)</li>
</ul>
<p><strong>Priority Calculation:</strong></p>
<ul>
<li>Priority score = basePriority * (100 - componentWeight) / 100</li>
<li>basePriority = daysUntilDue * 100</li>
<li>Lower score = higher priority</li>
<li>Priority tiers assigned by days until due: hot (&lt;=2 days), urgent (&lt;=5 days), normal (&lt;=14 days), backlog (&gt;14 days)</li>
<li>Recalculation can be triggered manually via API</li>
</ul>
<h3>5. Batch Task Grouping (Cutting Operations Only)</h3>
<p><strong>What Gets Batched:</strong></p>
<ul>
<li>ONLY operations where the process type contains &quot;cutting&quot; or &quot;cut&quot; (case-insensitive), or equals &quot;material&quot;</li>
<li>Examples that batch: &quot;cutting&quot;, &quot;Sheetmetal cutting&quot;, &quot;Steel Tube cutting&quot;, &quot;material&quot;</li>
<li>Examples that do NOT batch: &quot;bending&quot;, &quot;welding&quot;, &quot;machining&quot;, &quot;assembly&quot;</li>
</ul>
<p><strong>Grouping Criteria:</strong></p>
<ul>
<li>Operations are grouped by: Process Type + Material + Thickness + Job ID</li>
<li>All operations sharing these attributes become a single batch that workers can start/complete together</li>
</ul>
<p><strong>Why Batching Exists:</strong></p>
<ul>
<li>In manufacturing, cutting is often done on a single machine for all parts of the same material and thickness</li>
<li>A worker sets up the machine once and cuts all parts, rather than switching materials repeatedly</li>
</ul>
<p><strong>Batch Display:</strong></p>
<ul>
<li>Batches show a count of operations (e.g., &quot;12 parts&quot;)</li>
<li>Individual parts within a batch are listed for reference</li>
</ul>
<h3>6. Task Naming Format (Two-Line Display)</h3>
<p>All work items (batches and individual operations) are displayed in a consistent two-line format:</p>
<p><strong>Line 1 (Title):</strong> <code>[Process] [Product] [Main Assembly] [Sub-Assembly]</code></p>
<ul>
<li>Process: &quot;Cut&quot; for cutting/material operations, otherwise the capitalized process name</li>
<li>Product: The job&#39;s product name</li>
<li>Main Assembly: The top-level assembly name (if the component has a parent hierarchy)</li>
<li>Sub-Assembly: The intermediate assembly name (if applicable)</li>
</ul>
<p><strong>Line 2 (Subtitle):</strong> <code>[Thickness] [Material] - [Customer] - [Serial Number]</code></p>
<ul>
<li>Parts are separated by &quot; - &quot; dash separators</li>
<li>Empty parts are omitted</li>
<li>Thickness: Displayed exactly as it comes from the BOM, with &quot; in&quot; suffix converted to double-quote (&quot;) for inches</li>
</ul>
<h3>7. Worker Queue / &quot;Next Work&quot; View</h3>
<p><strong>Purpose:</strong></p>
<ul>
<li>Shows each worker a prioritized list of tasks they should work on next</li>
<li>Filtered to only show operations matching the worker&#39;s assigned processes</li>
</ul>
<p><strong>Three Sections:</strong></p>
<ol>
<li><strong>In Progress</strong>: Tasks the worker has already started</li>
<li><strong>Stalled</strong>: Tasks that are paused with a reason</li>
<li><strong>Pending</strong>: Tasks available to start, ordered by priority</li>
</ol>
<p><strong>Mixed Content:</strong></p>
<ul>
<li>Cutting batches (grouped by material + thickness + job) appear as single items</li>
<li>All other operations (welding, bending, machining, assembly) appear as individual items</li>
<li>Both types are interleaved in a single prioritized list</li>
</ul>
<p><strong>Priority Ordering:</strong></p>
<ol>
<li>In-progress items appear at the top</li>
<li>Higher job priority (lower number) comes first</li>
<li>Earlier due dates come first (no due date goes to bottom)</li>
<li>Blocked items are pushed to the bottom with a reason displayed</li>
</ol>
<p><strong>Worker Actions:</strong></p>
<ul>
<li><strong>Start</strong>: Moves operation(s) from pending to in_progress, assigns to the current worker, records a time study &quot;start&quot; event</li>
<li><strong>Stalled</strong>: Pauses work; worker MUST provide a reason (e.g., &quot;missing hardware&quot;, &quot;machine down&quot;). Records a time study &quot;stalled&quot; event</li>
<li><strong>Done (Complete)</strong>: Marks operation(s) as completed with timestamp. Records a time study &quot;completed&quot; event. For batches, all operations in the batch are completed simultaneously</li>
<li><strong>Resume</strong>: Moves stalled tasks back to in_progress</li>
</ul>
<p><strong>Admin View:</strong></p>
<ul>
<li>Admins can select any worker from a dropdown to view their queue</li>
<li>Admins can reassign batches to different workers</li>
</ul>
<h3>8. Shop Floor View</h3>
<p><strong>Two Modes:</strong></p>
<p><strong>Batch View (Task-Based):</strong></p>
<ul>
<li>Shows all cutting batches grouped by material + thickness</li>
<li>Filter by process type</li>
<li>Mark batches as complete or stalled</li>
<li>Shows in-progress work at the top</li>
</ul>
<p><strong>Kanban Board (Component-Based):</strong></p>
<ul>
<li>Three columns: Pending, In Progress, Completed</li>
<li>Drag-and-drop components between columns to update status</li>
<li>Uses @hello-pangea/dnd library</li>
<li>Filter by process type</li>
<li>Expand components to view and manage subtasks</li>
</ul>
<h3>9. Dashboard</h3>
<p><strong>Stat Cards (Clickable):</strong></p>
<ul>
<li>Active Jobs: Count of in_progress jobs (click to see list)</li>
<li>Completed Units: Count of completed jobs (click to see list)</li>
<li>Low Stock Alert: Count of consumables below reorder level (click to see list)</li>
<li>Total Jobs: Total job count (click to see list)</li>
</ul>
<p><strong>Charts:</strong></p>
<ul>
<li>Job Status Bar Chart: Planned vs In Progress vs Completed job counts (using Recharts)</li>
<li>Recent Jobs List: 5 most recent jobs with status badges</li>
</ul>
<p><strong>Work Management:</strong></p>
<ul>
<li>Workers see their assigned &quot;Next Work&quot; items</li>
<li>Admins see a global view of all in-progress and stalled work</li>
<li>Admins can select specific workers to view their assigned work</li>
<li>Start/Stall/Done actions available directly from the dashboard</li>
<li>In-progress work section highlighted at the top</li>
</ul>
<p><strong>Job Detail Dialog:</strong></p>
<ul>
<li>Click a job to see metadata (quantity, due date, serial number)</li>
<li>Breakdown of incomplete vs completed tasks</li>
</ul>
<h3>10. Inventory Management</h3>
<p><strong>Three Categories (Tabs):</strong></p>
<ol>
<li><strong>Materials</strong>: Raw materials (steel, aluminum, etc.)</li>
<li><strong>Manufactured Parts</strong>: Parts produced in-house (machined parts with stock tracking)</li>
<li><strong>Consumables</strong>: Shop supplies (welding wire, grinding discs, etc.)</li>
</ol>
<p><strong>Item Number Auto-Assignment:</strong></p>
<ul>
<li>Consumables: 1000+</li>
<li>Materials: 5000+</li>
<li>Manufactured parts: 6000+</li>
</ul>
<p><strong>CRUD Operations:</strong></p>
<ul>
<li>Add, edit, delete inventory items</li>
<li>Quantity adjustment with transaction type (restock, consume, adjust, manufacture)</li>
<li>Every adjustment creates a transaction log entry</li>
</ul>
<p><strong>Low-Stock Alerting:</strong></p>
<ul>
<li>Two thresholds per item: restock point (warning) and reorder point (critical)</li>
<li>When quantity falls to or below reorder point, an automatic reorder alert is created</li>
<li>Manual alerts via QR code scanning</li>
</ul>
<p><strong>Reorder Alert Workflow:</strong></p>
<ul>
<li>Status progression: pending -&gt; approved -&gt; ordered -&gt; received (or dismissed)</li>
<li>Admins approve/dismiss alerts</li>
<li>System can generate pre-filled supplier email for ordering</li>
<li>Notification badge in sidebar shows pending alert count</li>
</ul>
<p><strong>QR Code / Kanban Cards:</strong></p>
<ul>
<li>Generate printable kanban cards with QR codes for physical inventory items</li>
<li>Workers scan QR codes to trigger reorder alerts</li>
<li>Camera-based QR scanning (requires HTTPS) or manual item ID entry</li>
</ul>
<p><strong>Machining Integration:</strong></p>
<ul>
<li>When a machining operation is completed, workers enter &quot;quantity made&quot;</li>
<li>If quantity exceeds what the job needs, excess is automatically added to machined parts inventory</li>
<li>Before starting a machining operation, the system checks if the part already exists in inventory</li>
<li>Workers can &quot;fulfill from inventory&quot; to skip machining and use existing stock</li>
</ul>
<h3>11. Time Study / Performance Reports</h3>
<p><strong>Event Tracking:</strong></p>
<ul>
<li>Every Start, Stalled, and Completed action on an operation creates a time study event</li>
<li>Events record: operation, component, job, worker, event type, timestamp, optional note</li>
</ul>
<p><strong>Reports (Admin Only):</strong></p>
<ul>
<li>Filter by worker and/or job</li>
<li>Shows total active time (time between start and completed events)</li>
<li>Shows total stalled time (time between stalled and resumed events)</li>
<li>Event log with timestamps for detailed analysis</li>
</ul>
<h3>12. Tutorial / Onboarding System</h3>
<p><strong>Automatic Trigger:</strong></p>
<ul>
<li>Tutorial appears automatically for any user who hasn&#39;t completed it (hasCompletedTutorial = false)</li>
</ul>
<p><strong>8 Steps:</strong></p>
<ol>
<li>Welcome to M-TRACK</li>
<li>Dashboard overview</li>
<li>Jobs Management</li>
<li>Shop Floor</li>
<li>Inventory</li>
<li>Your Work Queue</li>
<li>Settings</li>
<li>Completion (&quot;You&#39;re Ready!&quot;)</li>
</ol>
<p><strong>Controls:</strong></p>
<ul>
<li>Navigate with Next/Previous buttons or Arrow keys</li>
<li>Skip with Escape key or Skip button</li>
<li>Restart from Settings page</li>
<li>Completion status saved to database so it doesn&#39;t reappear</li>
</ul>
<p><strong>Implementation:</strong></p>
<ul>
<li>React Context (TutorialContext) manages state</li>
<li>Overlay component with Framer Motion animations</li>
<li>Persisted via API calls to mark completion/reset</li>
</ul>
<h3>13. Settings Page</h3>
<p><strong>For All Users:</strong></p>
<ul>
<li>View account info</li>
<li>Restart tutorial</li>
<li>View system architecture diagram</li>
</ul>
<p><strong>For Admins:</strong></p>
<ul>
<li>User Management: Create users, edit roles/usernames, assign work processes, manage PINs</li>
<li>Process Type Configuration: Add or remove manufacturing process categories</li>
</ul>
<h3>14. Architecture Diagram</h3>
<ul>
<li>Interactive Mermaid.js diagrams</li>
<li>Two views: System Architecture and User Workflow</li>
<li>Zoom in/out capability</li>
<li>Download as SVG</li>
<li>Shows how components relate (frontend, backend, database, user flows)</li>
</ul>
<hr>
<h2>UI Navigation</h2>
<h3>Sidebar</h3>
<ul>
<li>Dashboard</li>
<li>Jobs</li>
<li>Shop Floor</li>
<li>Worker Queue</li>
<li>Inventory</li>
<li>Scan (QR scanning)</li>
<li>Reorder Alerts (with notification badge showing pending count)</li>
<li>Time Study (admin only)</li>
<li>Settings</li>
<li>Architecture Diagram</li>
<li>Logout</li>
</ul>
<h3>Responsive Design</h3>
<ul>
<li>Works on desktop and mobile browsers</li>
<li>Sidebar collapses on mobile</li>
<li>Touch-friendly interactions for shop floor use</li>
</ul>
<hr>
<h2>Key Business Rules</h2>
<ol>
<li><p><strong>Cutting operations batch together</strong> by material + thickness + job. All other processes are individual operations.</p>
</li>
<li><p><strong>Operation blocking</strong> prevents workers from starting work out of sequence. Sequential order within a component, assembly hierarchy, and explicit cross-component dependencies all contribute to blocking.</p>
</li>
<li><p><strong>BOM updates preserve existing work.</strong> Re-importing a BOM updates metadata but does not delete operations that have already been started or completed.</p>
</li>
<li><p><strong>Machined parts can be fulfilled from inventory</strong> instead of re-machining, saving production time.</p>
</li>
<li><p><strong>Reorder alerts fire automatically</strong> when stock hits the reorder point, but can also be triggered manually via QR scan.</p>
</li>
<li><p><strong>Priority scoring is automatic</strong> based on due date and component weight, but jobs also have a manual priority override (1-5).</p>
</li>
<li><p><strong>Workers only see operations matching their assigned processes.</strong> A worker assigned to &quot;cutting&quot; and &quot;bending&quot; won&#39;t see welding tasks.</p>
</li>
<li><p><strong>Admins can override operation blocking</strong> with a reason and optional expiration time.</p>
</li>
<li><p><strong>Time study events are created automatically</strong> whenever a worker starts, stalls, or completes an operation.</p>
</li>
<li><p><strong>Process types are configurable</strong> - admins can add new process types without code changes.</p>
</li>
</ol>
<hr>
<h2>API Overview</h2>
<h3>Authentication</h3>
<ul>
<li><code>POST /api/login</code> - Worker login (select user, no PIN)</li>
<li><code>POST /api/auth/login-with-pin</code> - Admin login with PIN</li>
<li><code>GET /api/auth/user</code> - Get current session user</li>
<li><code>POST /api/logout</code> - End session</li>
<li><code>GET /api/auth/bootstrap-status</code> - Check if first-time setup needed</li>
<li><code>POST /api/auth/bootstrap-initialize</code> - Create/initialize first admin</li>
<li><code>GET /api/auth/check-pin/:userId</code> - Check if user requires PIN</li>
<li><code>POST /api/auth/set-pin</code> - Set own PIN</li>
<li><code>POST /api/auth/reset-pin/:userId</code> - Admin reset another user&#39;s PIN</li>
<li><code>POST /api/auth/initialize-pin/:userId</code> - Admin set PIN for another user</li>
<li><code>POST /api/auth/complete-tutorial</code> - Mark tutorial complete</li>
<li><code>POST /api/auth/reset-tutorial</code> - Reset tutorial for current user</li>
</ul>
<h3>Users</h3>
<ul>
<li><code>GET /api/users</code> - List all users</li>
<li><code>POST /api/users</code> - Create user (username, role, assignedProcesses)</li>
<li><code>PATCH /api/users/:id</code> - Update user</li>
</ul>
<h3>Jobs</h3>
<ul>
<li><code>GET /api/jobs</code> - List jobs (query: isTemplate=true/false)</li>
<li><code>GET /api/jobs/:id</code> - Get single job</li>
<li><code>POST /api/jobs</code> - Create job</li>
<li><code>PATCH /api/jobs/:id</code> - Update job</li>
<li><code>DELETE /api/jobs/:id</code> - Delete job</li>
<li><code>POST /api/jobs/:id/archive</code> - Soft delete</li>
<li><code>POST /api/jobs/:id/unarchive</code> - Restore archived job</li>
<li><code>GET /api/jobs/archived</code> - List archived jobs</li>
<li><code>POST /api/jobs/:id/instantiate</code> - Create job from template</li>
<li><code>POST /api/jobs/:id/save-template</code> - Save job as template</li>
<li><code>POST /api/jobs/import-bom</code> - Import BOM file (multipart/form-data)</li>
<li><code>GET /api/jobs/:id/bom</code> - Download original BOM file</li>
<li><code>POST /api/jobs/:id/bom/update</code> - Re-import BOM preserving existing work</li>
</ul>
<h3>Components</h3>
<ul>
<li><code>GET /api/components</code> - List (query: jobId, assignedTo)</li>
<li><code>POST /api/components</code> - Create component</li>
<li><code>PATCH /api/components/:id</code> - Update component</li>
<li><code>DELETE /api/components/:id</code> - Delete component</li>
<li><code>POST /api/components/move</code> - Move component to new parent</li>
</ul>
<h3>Operations</h3>
<ul>
<li><code>GET /api/operations</code> - List (query: componentId)</li>
<li><code>GET /api/operations/:id</code> - Get single operation</li>
<li><code>POST /api/operations</code> - Create operation</li>
<li><code>PATCH /api/operations/:id</code> - Update operation</li>
<li><code>DELETE /api/operations/:id</code> - Delete operation</li>
<li><code>POST /api/operations/reorder</code> - Reorder operations within a component</li>
<li><code>POST /api/operations/bulk</code> - Bulk create operations across components</li>
<li><code>DELETE /api/operations/bulk</code> - Bulk delete operations</li>
</ul>
<h3>Batch Tasks &amp; Worker Queue</h3>
<ul>
<li><code>GET /api/batch-tasks</code> - Get batch groups (query: processType, jobId, status)</li>
<li><code>POST /api/batch-tasks/start</code> - Start a batch (operationIds + userId)</li>
<li><code>POST /api/batch-tasks/complete</code> - Complete a batch</li>
<li><code>POST /api/batch-tasks/stall</code> - Stall a batch (with reason)</li>
<li><code>POST /api/batch-tasks/dismiss-stall</code> - Resume stalled batch</li>
<li><code>GET /api/batch-tasks/worker/:userId</code> - Get batches for a worker</li>
<li><code>GET /api/work/next/:userId</code> - Get unified next work (batches + individual ops)</li>
<li><code>GET /api/work/assigned</code> - Get current user&#39;s assigned work</li>
<li><code>GET /api/work/all-workers</code> - Get all workers&#39; current work (admin)</li>
</ul>
<h3>Inventory</h3>
<ul>
<li><code>GET /api/inventory</code> - List items (query: category)</li>
<li><code>POST /api/inventory</code> - Create item</li>
<li><code>PATCH /api/inventory/:id</code> - Update item</li>
<li><code>DELETE /api/inventory/:id</code> - Delete item (admin only)</li>
<li><code>POST /api/inventory/:id/adjust</code> - Adjust quantity with transaction log</li>
<li><code>GET /api/inventory/:id/transactions</code> - Get transaction history</li>
<li><code>GET /api/inventory-transactions</code> - Get all transactions</li>
</ul>
<h3>Machined Parts</h3>
<ul>
<li><code>GET /api/machined-parts</code> - List machined parts inventory</li>
<li><code>POST /api/operations/:id/complete-machining</code> - Complete machining with quantity tracking</li>
<li><code>POST /api/operations/:id/fulfill-from-inventory</code> - Use existing stock instead of machining</li>
<li><code>GET /api/machined-parts/check/:partName</code> - Check if part exists in inventory</li>
</ul>
<h3>Consumables (Legacy)</h3>
<ul>
<li><code>GET /api/consumables</code> - List consumables</li>
<li><code>POST /api/consumables</code> - Create consumable</li>
<li><code>PATCH /api/consumables/:id</code> - Update consumable</li>
</ul>
<h3>Alerts &amp; Scanning</h3>
<ul>
<li><code>GET /api/reorder-alerts</code> - List alerts (query: status)</li>
<li><code>GET /api/reorder-alerts/count</code> - Get pending alert count</li>
<li><code>POST /api/reorder-alerts</code> - Create manual alert</li>
<li><code>PATCH /api/reorder-alerts/:id</code> - Update alert status</li>
<li><code>POST /api/scan/:itemId</code> - Scan item to trigger reorder alert</li>
</ul>
<h3>Reports &amp; Config</h3>
<ul>
<li><code>GET /api/time-study/report</code> - Get time study data (query: workerId, jobId, componentId)</li>
<li><code>POST /api/time-study</code> - Record time study event</li>
<li><code>GET /api/process-types</code> - List configured process types</li>
<li><code>POST /api/process-types</code> - Add process type</li>
<li><code>DELETE /api/process-types/:id</code> - Remove process type</li>
<li><code>POST /api/recalculate-priorities</code> - Recalculate all operation priorities</li>
</ul>
<h3>Subtasks</h3>
<ul>
<li><code>GET /api/subtasks</code> - List (query: componentId)</li>
<li><code>POST /api/subtasks</code> - Create subtask</li>
<li><code>PATCH /api/subtasks/:id</code> - Update subtask</li>
<li><code>DELETE /api/subtasks/:id</code> - Delete subtask</li>
</ul>
<hr>
<h2>Architecture Diagram (Text)</h2>
<pre><code>+------------------+ HTTP/REST +------------------+ SQL +------------------+
| | &lt;===============&gt; | | &lt;==========&gt; | |
| React SPA | | Express.js | | PostgreSQL |
| (Frontend) | | (Backend) | | (Database) |
| | | | | |
| - Dashboard | | - REST API | | - Jobs |
| - Jobs | | - Session Auth | | - Components |
| - Shop Floor | | - BOM Parser | | - Operations |
| - Worker Queue | | - Batch Logic | | - Dependencies |
| - Inventory | | - Priority Calc | | - Inventory |
| - Settings | | - File Upload | | - Time Study |
| - Time Study | | | | - Users |
| - Scan (QR) | | | | - Sessions |
| | | | | |
+------------------+ +------------------+ +------------------+
| |
| Single server process |
| serves both frontend |
| static files and API |
+--------------------------------------+
</code></pre>
<h2>User Flow Diagram (Text)</h2>
<pre><code> +------------------+
| Landing Page |
| (User Select) |
+--------+---------+
|
+--------+---------+
| Worker or Admin? |
+--------+---------+
|
+--------------+--------------+
| |
+--------v---------+ +---------v--------+
| Worker Login | | Admin PIN Entry |
| (No PIN needed) | | (4-6 digits) |
+--------+----------+ +---------+--------+
| |
+-------------+---------------+
|
+--------v---------+
| Dashboard |
| (Role-specific) |
+--------+---------+
|
+-------------------+-------------------+
| | | |
+----v----+ +---v----+ +----v-----+ +------v------+
| Jobs | | Shop | | Worker | | Inventory |
| | | Floor | | Queue | | |
+---------+ +--------+ +----+-----+ +------+------+
| |
+------v------+ +-----v-------+
| Start/Stall | | Reorder |
| Done Actions| | Alerts |
+-------------+ +-------------+
</code></pre>
<hr>
<h2>Important Implementation Notes for Recreation</h2>
<ol>
<li><p><strong>The &quot;isCuttingProcess&quot; helper</strong> must use case-insensitive matching. Check if processType.toLowerCase() includes &quot;cutting&quot; or &quot;cut&quot;, OR equals &quot;material&quot;. This is critical for batch grouping to work with process types like &quot;Sheetmetal cutting&quot; or &quot;Steel Tube cutting&quot;.</p>
</li>
<li><p><strong>BOM import uses two libraries</strong>: <code>xlsx</code> for reading spreadsheet data, and <code>exceljs</code> for extracting embedded images (thumbnails). They serve different purposes and both are needed.</p>
</li>
<li><p><strong>The worker queue mixes batched and individual items</strong> in a single prioritized list. Cutting operations are grouped into batches; everything else is individual. This is the &quot;Next Work&quot; endpoint.</p>
</li>
<li><p><strong>Operation status transitions</strong>: pending -&gt; in_progress -&gt; completed (or stalled from in_progress, then resumed back to in_progress). The &quot;skipped&quot; status exists for operations bypassed via inventory fulfillment.</p>
</li>
<li><p><strong>The task naming two-line format</strong> must use &quot; (double quote character) instead of &quot;in&quot; for inches in thickness. The thickness value should be displayed as it comes from the BOM with this one normalization.</p>
</li>
<li><p><strong>Subtitle parts are joined with &quot; - &quot; (space dash space)</strong> as separators. Empty parts are omitted entirely.</p>
</li>
<li><p><strong>Assembly blocking</strong>: An operation on an &quot;assembly&quot; type component is blocked until ALL child components (and their children, recursively) have status &quot;completed&quot;.</p>
</li>
<li><p><strong>Session-based auth</strong> means the server must support sessions. The worker login flow explicitly prevents admin role users from using it (they must use PIN).</p>
</li>
<li><p><strong>Inventory auto-numbering</strong> assigns the next available number in the category range when creating new items.</p>
</li>
<li><p><strong>Reorder alerts deduplicate</strong> - the system should check for existing pending alerts before creating duplicates.</p>
</li>
<li><p><strong>BOM update (re-import) preserves work</strong> - when updating a BOM, existing operations with status in_progress or completed must not be deleted. Only add new components, remove components that no longer exist in the BOM, and update metadata on existing ones.</p>
</li>
<li><p><strong>The priority recalculation</strong> processes all active (non-template, non-archived) jobs and updates every operation&#39;s priorityScore and priorityTier based on current date vs. due date.</p>
</li>
</ol>
</body></html>

View File

@ -0,0 +1 @@
{}

29095
replit_data/components.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,12 @@
{
"count": 1,
"data": [
{
"id": 1,
"name": "TIG Welding Rods",
"quantity": 50,
"reorderLevel": 20,
"unit": "kg"
}
]
}

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
<link rel="icon" type="image/png" href="/favicon.png" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Architects+Daughter&family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Fira+Code:wght@300..700&family=Geist+Mono:wght@100..900&family=Geist:wght@100..900&family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Lora:ital,wght@0,400..700;1,400..700&family=Merriweather:ital,opsz,wght@0,18..144,300..900;1,18..144,300..900&family=Montserrat:ital,wght@0,100..900;1,100..900&family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Outfit:wght@100..900&family=Oxanium:wght@200..800&family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100..900;1,100..900&family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&family=Space+Grotesk:wght@300..700&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
<script type="module" crossorigin src="/assets/index-fCLaIajI.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-aOXWrruY.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -0,0 +1,27 @@
{
"count": 1,
"data": [
{
"id": 1,
"itemNumber": 5000,
"name": "ghghgh",
"description": "",
"category": "material",
"sku": "",
"unit": "pcs",
"quantity": 0,
"restockPoint": 10,
"reorderPoint": 5,
"reorderQuantity": 20,
"storageLocation": "",
"supplierName": "",
"supplierContact": "",
"supplierEmail": "",
"unitCost": 0,
"minimumOrderQuantity": 0,
"leadTimeDays": 0,
"lastRestockDate": null,
"createdAt": "2026-01-12T14:12:25.972Z"
}
]
}

41
replit_data/jobs.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,4 @@
{
"count": 0,
"data": []
}

1013
replit_data/operations.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,65 @@
{
"count": 10,
"data": [
{
"id": 1,
"name": "sheetmetal cutting",
"orderIndex": 1,
"createdAt": "2026-01-11T16:30:06.420Z"
},
{
"id": 2,
"name": "bar/tube cutting",
"orderIndex": 2,
"createdAt": "2026-01-11T16:30:14.370Z"
},
{
"id": 3,
"name": "bending",
"orderIndex": 3,
"createdAt": "2026-01-11T16:30:26.992Z"
},
{
"id": 4,
"name": "fabrication",
"orderIndex": 4,
"createdAt": "2026-01-11T16:31:39.741Z"
},
{
"id": 5,
"name": "welding",
"orderIndex": 5,
"createdAt": "2026-01-11T16:31:46.010Z"
},
{
"id": 6,
"name": "electrical",
"orderIndex": 6,
"createdAt": "2026-01-11T16:31:52.729Z"
},
{
"id": 7,
"name": "assembly",
"orderIndex": 7,
"createdAt": "2026-01-11T16:31:58.973Z"
},
{
"id": 8,
"name": "rolling",
"orderIndex": 8,
"createdAt": "2026-01-11T16:39:05.843Z"
},
{
"id": 9,
"name": "finishing",
"orderIndex": 9,
"createdAt": "2026-01-11T16:40:58.715Z"
},
{
"id": 10,
"name": "machining",
"orderIndex": 10,
"createdAt": "2026-01-11T16:43:02.837Z"
}
]
}

View File

@ -0,0 +1,4 @@
{
"count": 0,
"data": []
}

View File

@ -0,0 +1 @@
[{"id":1,"name":"Measure stock","status":"completed","componentId":1},{"id":2,"name":"Set saw angle","status":"completed","componentId":1},{"id":3,"name":"Fixture setup","status":"completed","componentId":2},{"id":4,"name":"Root pass","status":"pending","componentId":2},{"id":5,"name":"Check DXF file","status":"pending","componentId":3}]

View File

@ -0,0 +1 @@
{"message":"Unauthorized"}

72
replit_data/users.json Normal file
View File

@ -0,0 +1,72 @@
{
"count": 4,
"data": [
{
"id": "1571ac82-0762-4763-83e7-a216a6f3ed82",
"email": null,
"firstName": null,
"lastName": null,
"profileImageUrl": null,
"username": "Sam",
"role": "worker",
"assignedProcesses": [
"fabrication",
"welding",
"rolling"
],
"hasCompletedTutorial": false,
"pinHash": null,
"createdAt": "2026-01-11T16:27:30.536Z",
"updatedAt": "2026-01-11T16:42:29.719Z"
},
{
"id": "f36f5e69-d8f2-4416-adcf-306b049b3d2b",
"email": null,
"firstName": null,
"lastName": null,
"profileImageUrl": null,
"username": "Derik",
"role": "worker",
"assignedProcesses": [
"sheetmetal cutting",
"bar/tube cutting",
"bending",
"finishing"
],
"hasCompletedTutorial": false,
"pinHash": null,
"createdAt": "2026-01-11T16:42:42.127Z",
"updatedAt": "2026-01-11T16:42:42.127Z"
},
{
"id": "b901b15b-0ee6-493f-859c-1e8fd004f575",
"email": null,
"firstName": null,
"lastName": null,
"profileImageUrl": null,
"username": "Daniel",
"role": "worker",
"assignedProcesses": [
"machining"
],
"hasCompletedTutorial": false,
"pinHash": null,
"createdAt": "2026-01-11T16:43:11.659Z",
"updatedAt": "2026-01-11T16:43:11.659Z"
},
{
"id": "9b906950-cb85-437f-b9e3-2a253ffd5ddf",
"email": null,
"firstName": null,
"lastName": null,
"profileImageUrl": null,
"username": "Admin User",
"role": "admin",
"assignedProcesses": [],
"hasCompletedTutorial": true,
"pinHash": "[REDACTED]",
"createdAt": "2026-01-11T16:27:30.489Z",
"updatedAt": "2026-01-12T13:16:58.877Z"
}
]
}

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
<link rel="icon" type="image/png" href="/favicon.png" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Architects+Daughter&family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Fira+Code:wght@300..700&family=Geist+Mono:wght@100..900&family=Geist:wght@100..900&family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Lora:ital,wght@0,400..700;1,400..700&family=Merriweather:ital,opsz,wght@0,18..144,300..900;1,18..144,300..900&family=Montserrat:ital,wght@0,100..900;1,100..900&family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Outfit:wght@100..900&family=Oxanium:wght@200..800&family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100..900;1,100..900&family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&family=Space+Grotesk:wght@300..700&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
<script type="module" crossorigin src="/assets/index-fCLaIajI.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-aOXWrruY.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

16
replit_db_export.json Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
<link rel="icon" type="image/png" href="/favicon.png" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Architects+Daughter&family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Fira+Code:wght@300..700&family=Geist+Mono:wght@100..900&family=Geist:wght@100..900&family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Lora:ital,wght@0,400..700;1,400..700&family=Merriweather:ital,opsz,wght@0,18..144,300..900;1,18..144,300..900&family=Montserrat:ital,wght@0,100..900;1,100..900&family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Outfit:wght@100..900&family=Oxanium:wght@200..800&family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100..900;1,100..900&family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&family=Space+Grotesk:wght@300..700&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
<script type="module" crossorigin src="/assets/index-fCLaIajI.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-aOXWrruY.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

File diff suppressed because one or more lines are too long

82
scan.php Normal file
View File

@ -0,0 +1,82 @@
<?php
session_start();
require 'db/config.php';
if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
$role = $_SESSION['role'];
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>M-TRACK | Scan QR/Barcode</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 {
--bg: #f8fafc;
--card-bg: #ffffff;
--primary: #1e293b;
--accent: #3b82f6;
--text: #334155;
--sidebar-w: 260px;
}
body { font-family: 'Inter', sans-serif; background-color: var(--bg); color: var(--text); }
.sidebar { width: var(--sidebar-w); position: fixed; top: 0; bottom: 0; left: 0; background: var(--card-bg); border-right: 1px solid #e2e8f0; padding: 2rem 1.5rem; }
.main-content { margin-left: var(--sidebar-w); padding: 2rem; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; }
.scanner-box { width: 100%; max-width: 500px; background: white; border-radius: 16px; padding: 2rem; text-align: center; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1); }
.camera-placeholder { width: 100%; height: 300px; background: #0f172a; border-radius: 12px; position: relative; overflow: hidden; display: flex; align-items: center; justify-content: center; margin-bottom: 1.5rem; }
.scan-line { width: 100%; height: 2px; background: #22c55e; position: absolute; top: 50%; box-shadow: 0 0 10px 2px #22c55e; animation: scan 2s infinite linear; }
@keyframes scan { 0% { top: 0; } 50% { top: 100%; } 100% { top: 0; } }
</style>
</head>
<body>
<div class="sidebar">
<h4 class="fw-bold mb-5" style="color: var(--primary);"><i class="bi bi-cpu me-2"></i>M-TRACK</h4>
<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>
<?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>
<?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>
<?php endif; ?>
<a class="nav-link active" href="scan.php"><i class="bi bi-upc-scan me-2"></i> Scan</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="scanner-box">
<h3 class="fw-bold mb-3">Scan Kanban Card</h3>
<p class="text-muted mb-4">Position the QR or Barcode within the frame to trigger a reorder or update job status.</p>
<div class="camera-placeholder">
<span class="text-white-50"><i class="bi bi-camera-video me-2"></i> Camera Feed Active</span>
<div class="scan-line"></div>
</div>
<button class="btn btn-primary w-100 btn-lg rounded-pill" onclick="simulateScan()">
<i class="bi bi-upc-scan me-2"></i> Simulate Scan
</button>
</div>
</div>
<script>
function simulateScan() {
alert("Scanned! (This is a simulation). In a real environment, this would parse the QR code payload and trigger an API call to update inventory or job status.");
}
</script>
</body>
</html>

185
shop_floor.php Normal file
View File

@ -0,0 +1,185 @@
<?php
session_start();
require 'db/config.php';
$db = db();
if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
$role = $_SESSION['role'];
$userId = $_SESSION['user_id'];
// Fetch all operations with context
$sql = "
SELECT o.*, c.name as component_name, j.name as job_name, j.serial_number, 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 != 'completed'
ORDER BY o.priority_tier DESC, o.order_index ASC, o.created_at ASC
";
$operations = $db->query($sql)->fetchAll();
// Group by status for Kanban
$kanban = [
'pending' => [],
'in_progress' => [],
'stalled' => []
];
foreach ($operations as $op) {
if (isset($kanban[$op['status']])) {
$kanban[$op['status']][] = $op;
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>M-TRACK | Shop Floor</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 {
--bg: #f8fafc;
--card-bg: #ffffff;
--primary: #1e293b;
--accent: #3b82f6;
--text: #334155;
--sidebar-w: 260px;
}
body { font-family: 'Inter', sans-serif; background-color: var(--bg); color: var(--text); overflow-x: hidden; }
.sidebar { width: var(--sidebar-w); position: fixed; top: 0; bottom: 0; left: 0; background: var(--card-bg); border-right: 1px solid #e2e8f0; padding: 2rem 1.5rem; z-index: 10; }
.main-content { margin-left: var(--sidebar-w); padding: 2rem; min-height: 100vh; }
.top-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; }
.kanban-board { display: flex; gap: 1.5rem; align-items: flex-start; overflow-x: auto; padding-bottom: 1rem; }
.kanban-col { background: #e2e8f0; border-radius: 12px; min-width: 320px; padding: 1rem; flex-shrink: 0; }
.kanban-card { background: white; border-radius: 8px; padding: 1rem; margin-bottom: 1rem; box-shadow: 0 2px 4px rgba(0,0,0,0.05); cursor: grab; border-left: 4px solid var(--accent); transition: transform 0.2s; }
.kanban-card:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
.kanban-card.stalled { border-left-color: #ef4444; }
.kanban-card.in_progress { border-left-color: #10b981; }
.kanban-title { font-weight: 600; font-size: 0.95rem; margin-bottom: 0.25rem; color: var(--primary); }
.kanban-meta { font-size: 0.8rem; color: #64748b; }
.badge-tier { font-size: 0.7rem; padding: 0.25em 0.5em; border-radius: 4px; font-weight: 600; text-transform: uppercase; }
.tier-urgent { background: #fee2e2; color: #b91c1c; }
.tier-high { background: #fef3c7; color: #c2410c; }
.tier-normal { background: #f1f5f9; color: #475569; }
.tier-low { background: #f8fafc; color: #94a3b8; border: 1px solid #e2e8f0; }
</style>
</head>
<body>
<div class="sidebar">
<h4 class="fw-bold mb-5" style="color: var(--primary);"><i class="bi bi-cpu me-2"></i>M-TRACK</h4>
<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>
<?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 active" 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>
<?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 active" href="shop_floor.php"><i class="bi bi-kanban me-2"></i> Board View</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">
<div>
<h2 class="h3 fw-bold mb-1">Shop Floor</h2>
<p class="text-muted mb-0">Drag and drop operations to update status</p>
</div>
</div>
<div class="kanban-board">
<!-- Pending Column -->
<div class="kanban-col">
<h5 class="fw-bold mb-3 px-2">Pending <span class="badge bg-secondary rounded-pill float-end"><?= count($kanban['pending']) ?></span></h5>
<?php foreach ($kanban['pending'] as $op): ?>
<div class="kanban-card">
<div class="d-flex justify-content-between mb-1">
<span class="badge-tier tier-<?= $op['priority_tier'] ?>"><?= $op['priority_tier'] ?></span>
<span class="badge bg-light text-dark border"><i class="bi bi-tag"></i> <?= htmlspecialchars($op['process_type']) ?></span>
</div>
<div class="kanban-title"><?= htmlspecialchars($op['name']) ?></div>
<div class="kanban-meta mb-2">
<div><i class="bi bi-box"></i> <?= htmlspecialchars($op['component_name']) ?></div>
<div><i class="bi bi-briefcase"></i> <?= htmlspecialchars($op['job_name']) ?> (<?= htmlspecialchars($op['serial_number']) ?>)</div>
</div>
<div class="mt-2 text-end">
<button class="btn btn-sm btn-outline-primary rounded-pill px-3">Start</button>
</div>
</div>
<?php endforeach; ?>
<?php if (empty($kanban['pending'])): ?>
<div class="text-muted text-center p-3 small">No pending operations</div>
<?php endif; ?>
</div>
<!-- In Progress Column -->
<div class="kanban-col">
<h5 class="fw-bold mb-3 px-2">In Progress <span class="badge bg-success rounded-pill float-end"><?= count($kanban['in_progress']) ?></span></h5>
<?php foreach ($kanban['in_progress'] as $op): ?>
<div class="kanban-card in_progress">
<div class="d-flex justify-content-between mb-1">
<span class="badge-tier tier-<?= $op['priority_tier'] ?>"><?= $op['priority_tier'] ?></span>
<span class="badge bg-light text-dark border"><i class="bi bi-person"></i> <?= htmlspecialchars($op['worker_name'] ?? 'Unknown') ?></span>
</div>
<div class="kanban-title"><?= htmlspecialchars($op['name']) ?></div>
<div class="kanban-meta mb-2">
<div><i class="bi bi-box"></i> <?= htmlspecialchars($op['component_name']) ?></div>
<div><i class="bi bi-briefcase"></i> <?= htmlspecialchars($op['job_name']) ?> (<?= htmlspecialchars($op['serial_number']) ?>)</div>
</div>
<div class="mt-2 d-flex justify-content-between">
<button class="btn btn-sm btn-outline-danger rounded-pill px-3">Stall</button>
<button class="btn btn-sm btn-success rounded-pill px-3">Done</button>
</div>
</div>
<?php endforeach; ?>
<?php if (empty($kanban['in_progress'])): ?>
<div class="text-muted text-center p-3 small">No operations in progress</div>
<?php endif; ?>
</div>
<!-- Stalled Column -->
<div class="kanban-col">
<h5 class="fw-bold mb-3 px-2">Stalled <span class="badge bg-danger rounded-pill float-end"><?= count($kanban['stalled']) ?></span></h5>
<?php foreach ($kanban['stalled'] as $op): ?>
<div class="kanban-card stalled">
<div class="d-flex justify-content-between mb-1">
<span class="badge-tier tier-<?= $op['priority_tier'] ?>"><?= $op['priority_tier'] ?></span>
<span class="badge bg-light text-dark border"><i class="bi bi-exclamation-triangle"></i> Issue</span>
</div>
<div class="kanban-title"><?= htmlspecialchars($op['name']) ?></div>
<div class="kanban-meta mb-2">
<div><i class="bi bi-box"></i> <?= htmlspecialchars($op['component_name']) ?></div>
<div class="text-danger mt-1"><i class="bi bi-chat-dots"></i> <?= htmlspecialchars($op['blocked_reason'] ?? 'No reason provided') ?></div>
</div>
<div class="mt-2 text-end">
<button class="btn btn-sm btn-outline-secondary rounded-pill px-3">Resume</button>
</div>
</div>
<?php endforeach; ?>
<?php if (empty($kanban['stalled'])): ?>
<div class="text-muted text-center p-3 small">No stalled operations</div>
<?php endif; ?>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

1
subtasks.json Normal file
View File

@ -0,0 +1 @@
[{"id":1,"name":"Measure stock","status":"completed","componentId":1},{"id":2,"name":"Set saw angle","status":"completed","componentId":1},{"id":3,"name":"Fixture setup","status":"completed","componentId":2},{"id":4,"name":"Root pass","status":"pending","componentId":2},{"id":5,"name":"Check DXF file","status":"pending","componentId":3}]

114
time_study.php Normal file
View File

@ -0,0 +1,114 @@
<?php
session_start();
require 'db/config.php';
$db = db();
if (!isset($_SESSION['user_id']) || $_SESSION['role'] !== 'admin') {
header('Location: index.php');
exit;
}
$role = $_SESSION['role'];
// Fetch Time Study events
$sql = "
SELECT t.*, o.name as op_name, u.name as worker_name, j.name as job_name
FROM time_study_events t
JOIN operations o ON t.operation_id = o.id
JOIN components c ON o.component_id = c.id
JOIN jobs j ON c.job_id = j.id
JOIN users u ON t.user_id = u.id
ORDER BY t.timestamp DESC
LIMIT 100
";
$events = $db->query($sql)->fetchAll();
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>M-TRACK | Time Study</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 {
--bg: #f8fafc;
--card-bg: #ffffff;
--primary: #1e293b;
--accent: #3b82f6;
--text: #334155;
--sidebar-w: 260px;
}
body { font-family: 'Inter', sans-serif; background-color: var(--bg); color: var(--text); }
.sidebar { width: var(--sidebar-w); position: fixed; top: 0; bottom: 0; left: 0; background: var(--card-bg); border-right: 1px solid #e2e8f0; padding: 2rem 1.5rem; }
.main-content { margin-left: var(--sidebar-w); padding: 2rem; }
.card { border: none; border-radius: 12px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05); }
.event-start { color: #3b82f6; background: #eff6ff; padding: 2px 6px; border-radius: 4px; }
.event-completed { color: #10b981; background: #ecfdf5; padding: 2px 6px; border-radius: 4px; }
.event-stalled { color: #ef4444; background: #fef2f2; padding: 2px 6px; border-radius: 4px; }
.event-resume { color: #f59e0b; background: #fffbeb; padding: 2px 6px; border-radius: 4px; }
</style>
</head>
<body>
<div class="sidebar">
<h4 class="fw-bold mb-5" style="color: var(--primary);"><i class="bi bi-cpu me-2"></i>M-TRACK</h4>
<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="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 active" href="time_study.php"><i class="bi bi-clock-history me-2"></i> Time Study</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">
<h2 class="h3 fw-bold mb-0">Time Study Analytics</h2>
<button class="btn btn-outline-primary"><i class="bi bi-download"></i> Export CSV</button>
</div>
<div class="card p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>Timestamp</th>
<th>Worker</th>
<th>Job</th>
<th>Operation</th>
<th>Event</th>
<th>Reason / Note</th>
</tr>
</thead>
<tbody>
<?php foreach ($events as $e): ?>
<tr>
<td><?= date('Y-m-d H:i:s', strtotime($e['timestamp'])) ?></td>
<td class="fw-medium"><?= htmlspecialchars($e['worker_name']) ?></td>
<td><?= htmlspecialchars($e['job_name']) ?></td>
<td><?= htmlspecialchars($e['op_name']) ?></td>
<td>
<span class="event-<?= $e['event_type'] ?> small fw-bold text-uppercase">
<?= htmlspecialchars($e['event_type']) ?>
</span>
</td>
<td class="text-muted small"><?= htmlspecialchars($e['reason'] ?? '-') ?></td>
</tr>
<?php endforeach; ?>
<?php if(empty($events)): ?>
<tr><td colspan="6" class="text-center py-4 text-muted">No time study events recorded yet.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>

40
ui_review.md Normal file
View File

@ -0,0 +1,40 @@
# M-TRACK UI & Frontend Architecture Review
Based on the Replit App Summary, here is a structured breakdown of the UI components we need to build for the Flatlogic version of M-TRACK:
## 1. Authentication & Onboarding Flow
- **Landing Page:** A unified entry point displaying a user selection list.
- **Two-Tier Login:** Workers get frictionless 1-click access; Admins are prompted for a 4-6 digit hashed PIN.
- **First-Time Setup:** A "Bootstrap" mode if no Admin exists.
- **Tutorial Overlay:** An 8-step interactive onboarding flow (using context/state) for first-time logins.
## 2. Dashboard Interface
- **Stat Cards:** 4 clickable metric widgets (Active Jobs, Completed Units, Low Stock Alert, Total Jobs).
- **Charts:** A bar chart visualizing Job Statuses (Planned vs. In Progress vs. Completed).
- **Recent Activity:** A list of the 5 most recent jobs with status badges.
- **Work Management Panel:** Highlights in-progress work. Workers see their own queue; Admins can filter by any worker.
## 3. Jobs & BOM Management
- **Job Lists:** Filterable views for Active Jobs, Archived Jobs, and Job Templates.
- **Job Creation Modal:** Options to create manually or instantiate from a template.
- **BOM Import Workflow:** A critical UI for uploading Excel/CSV files, displaying extracted thumbnail images, and showing an import summary.
- **Component Tree View:** A hierarchical UI (Assemblies -> Sub-assemblies -> Parts) showing order indexes, quantities, and attached operations.
## 4. Shop Floor & Worker Queue (The Core Engine)
- **"Next Work" Unified Queue:** A prioritized, interleaved list of batched tasks (e.g., grouped cutting operations) and individual tasks.
- **Standardized Task Cards:**
- Line 1: `[Process] [Product] [Main Assembly] [Sub-Assembly]`
- Line 2: `[Thickness] [Material] - [Customer] - [Serial Number]`
- **Task Controls:** Start, Stall (with mandatory reason prompt), Resume, and Done buttons.
- **Kanban Board:** A drag-and-drop interface mapping components across Pending, In Progress, and Completed columns.
## 5. Inventory Management
- **Tabbed Data Tables:** Separated views for Materials, Manufactured Parts, and Consumables.
- **Stock Adjustment Modal:** Interface to update quantities with transaction reasons (restock, consume, etc.).
- **Reorder Alerts:** A dedicated view for Admins to approve/dismiss alerts, with a notification badge in the sidebar.
- **QR / Barcode Integration:** UI to generate printable Kanban cards and a camera scanner view to trigger reorders.
## 6. Admin Settings & Analytics
- **User Management:** Assign roles, reset PINs, and map specific process types (e.g., cutting, welding) to individual workers.
- **Process Types Config:** A simple list manager to add/remove manufacturing process categories.
- **Time Study Reports:** Analytical tables filtering work events (start, stall, complete) by user/job to calculate active vs. stalled time.

1
users.json Normal file
View File

@ -0,0 +1 @@
[{"id":"1571ac82-0762-4763-83e7-a216a6f3ed82","email":null,"firstName":null,"lastName":null,"profileImageUrl":null,"username":"Sam","role":"worker","assignedProcesses":["fabrication","welding","rolling"],"hasCompletedTutorial":false,"pinHash":null,"createdAt":"2026-01-11T16:27:30.536Z","updatedAt":"2026-01-11T16:42:29.719Z"},{"id":"f36f5e69-d8f2-4416-adcf-306b049b3d2b","email":null,"firstName":null,"lastName":null,"profileImageUrl":null,"username":"Derik","role":"worker","assignedProcesses":["sheetmetal cutting","bar/tube cutting","bending","finishing"],"hasCompletedTutorial":false,"pinHash":null,"createdAt":"2026-01-11T16:42:42.127Z","updatedAt":"2026-01-11T16:42:42.127Z"},{"id":"b901b15b-0ee6-493f-859c-1e8fd004f575","email":null,"firstName":null,"lastName":null,"profileImageUrl":null,"username":"Daniel","role":"worker","assignedProcesses":["machining"],"hasCompletedTutorial":false,"pinHash":null,"createdAt":"2026-01-11T16:43:11.659Z","updatedAt":"2026-01-11T16:43:11.659Z"},{"id":"9b906950-cb85-437f-b9e3-2a253ffd5ddf","email":null,"firstName":null,"lastName":null,"profileImageUrl":null,"username":"Admin User","role":"admin","assignedProcesses":[],"hasCompletedTutorial":true,"pinHash":"$2b$10$wPkYDWj/APEJGfi2lW3HbOAryaFXki5f1D9UrFyMUhaDSJP057Ge6","createdAt":"2026-01-11T16:27:30.489Z","updatedAt":"2026-01-12T13:16:58.877Z"}]

View File

@ -105,6 +105,7 @@ $processTypes = ['cutting', 'welding', 'bending', 'assembly', 'inspection'];
<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="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 active" href="users.php"><i class="bi bi-people me-2"></i> Users</a>
<hr class="my-4 border-secondary opacity-25">