Autosave: 20260228-223337
This commit is contained in:
parent
951a96262b
commit
e069921c28
244
api/import_bom.php
Normal file
244
api/import_bom.php
Normal 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
49
api/test_bom_parser.php
Normal 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
34
api/test_xlsx.php
Normal 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
26
batch_task_schema.json
Normal 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
259
bom_import.php
Normal 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
19
component_schema.json
Normal 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
1
components.json
Normal file
File diff suppressed because one or more lines are too long
1
consumables.json
Normal file
1
consumables.json
Normal file
@ -0,0 +1 @@
|
||||
[{"id":1,"name":"TIG Welding Rods","quantity":50,"reorderLevel":20,"unit":"kg"}]
|
||||
@ -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>
|
||||
|
||||
@ -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";
|
||||
}
|
||||
|
||||
@ -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";
|
||||
}
|
||||
|
||||
162
db/migrate_003_full_schema.php
Normal file
162
db/migrate_003_full_schema.php
Normal 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";
|
||||
}
|
||||
@ -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
18
job_schema.json
Normal file
File diff suppressed because one or more lines are too long
40
jobs.php
40
jobs.php
@ -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
21
operation_schema.json
Normal 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
1
operations.json
Normal file
File diff suppressed because one or more lines are too long
16
replit_app_summary.html
Normal file
16
replit_app_summary.html
Normal 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>
|
||||
808
replit_app_summary_full.html
Normal file
808
replit_app_summary_full.html
Normal 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 & 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 "Bootstrap" 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., ["cutting", "welding"]) 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' 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: "material" (raw part) or "assembly" (subassembly containing other parts)</li>
|
||||
<li>Processes: array of strings (e.g., ["cutting", "bending", "welding"])</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., '0.125"', '3mm')</li>
|
||||
<li>Material (e.g., "Mild Steel", "Aluminum 6061")</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., "Cut this part", "Weld this part").</p>
|
||||
<p>Fields:</p>
|
||||
<ul>
|
||||
<li>Component ID (belongs to a component)</li>
|
||||
<li>Name (descriptive task name)</li>
|
||||
<li>Process type (e.g., "cutting", "Sheetmetal cutting", "bending", "welding", "machining", "assembly")</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., ["cutting", "bending"])</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., "cutting", "Sheetmetal cutting", "bending", "welding", "machining", "assembly")</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 -> in_progress -> 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., "UpperCylinder" -> "Upper Cylinder")</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 "1", "2") as type "assembly"</li>
|
||||
<li>Second pass: Creates parts (items with decimal IDs like "1.1", "1.2") as type "material", 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 "MCR" 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 & 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 & 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 "blocked" 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 (<=2 days), urgent (<=5 days), normal (<=14 days), backlog (>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 "cutting" or "cut" (case-insensitive), or equals "material"</li>
|
||||
<li>Examples that batch: "cutting", "Sheetmetal cutting", "Steel Tube cutting", "material"</li>
|
||||
<li>Examples that do NOT batch: "bending", "welding", "machining", "assembly"</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., "12 parts")</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: "Cut" for cutting/material operations, otherwise the capitalized process name</li>
|
||||
<li>Product: The job'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 " - " dash separators</li>
|
||||
<li>Empty parts are omitted</li>
|
||||
<li>Thickness: Displayed exactly as it comes from the BOM, with " in" suffix converted to double-quote (") for inches</li>
|
||||
</ul>
|
||||
<h3>7. Worker Queue / "Next Work" 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'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 "start" event</li>
|
||||
<li><strong>Stalled</strong>: Pauses work; worker MUST provide a reason (e.g., "missing hardware", "machine down"). Records a time study "stalled" event</li>
|
||||
<li><strong>Done (Complete)</strong>: Marks operation(s) as completed with timestamp. Records a time study "completed" 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 "Next Work" 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 -> approved -> ordered -> 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 "quantity made"</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 "fulfill from inventory" 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'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 ("You're Ready!")</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'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 "cutting" and "bending" won'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'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 & 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's assigned work</li>
|
||||
<li><code>GET /api/work/all-workers</code> - Get all workers' 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 & 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 & 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 +------------------+
|
||||
| | <===============> | | <==========> | |
|
||||
| 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 "isCuttingProcess" helper</strong> must use case-insensitive matching. Check if processType.toLowerCase() includes "cutting" or "cut", OR equals "material". This is critical for batch grouping to work with process types like "Sheetmetal cutting" or "Steel Tube cutting".</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 "Next Work" endpoint.</p>
|
||||
</li>
|
||||
<li><p><strong>Operation status transitions</strong>: pending -> in_progress -> completed (or stalled from in_progress, then resumed back to in_progress). The "skipped" status exists for operations bypassed via inventory fulfillment.</p>
|
||||
</li>
|
||||
<li><p><strong>The task naming two-line format</strong> must use " (double quote character) instead of "in" 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 " - " (space dash space)</strong> as separators. Empty parts are omitted entirely.</p>
|
||||
</li>
|
||||
<li><p><strong>Assembly blocking</strong>: An operation on an "assembly" type component is blocked until ALL child components (and their children, recursively) have status "completed".</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's priorityScore and priorityTier based on current date vs. due date.</p>
|
||||
</li>
|
||||
</ol>
|
||||
</body></html>
|
||||
1
replit_data/batch-tasks.json
Normal file
1
replit_data/batch-tasks.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
29095
replit_data/components.json
Normal file
29095
replit_data/components.json
Normal file
File diff suppressed because one or more lines are too long
12
replit_data/consumables.json
Normal file
12
replit_data/consumables.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"count": 1,
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "TIG Welding Rods",
|
||||
"quantity": 50,
|
||||
"reorderLevel": 20,
|
||||
"unit": "kg"
|
||||
}
|
||||
]
|
||||
}
|
||||
16
replit_data/dashboard.json
Normal file
16
replit_data/dashboard.json
Normal 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>
|
||||
27
replit_data/inventory.json
Normal file
27
replit_data/inventory.json
Normal 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
41
replit_data/jobs.json
Normal file
File diff suppressed because one or more lines are too long
4
replit_data/machined-parts.json
Normal file
4
replit_data/machined-parts.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"count": 0,
|
||||
"data": []
|
||||
}
|
||||
1013
replit_data/operations.json
Normal file
1013
replit_data/operations.json
Normal file
File diff suppressed because it is too large
Load Diff
65
replit_data/process-types.json
Normal file
65
replit_data/process-types.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
4
replit_data/reorder-alerts.json
Normal file
4
replit_data/reorder-alerts.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"count": 0,
|
||||
"data": []
|
||||
}
|
||||
1
replit_data/subtasks.json
Normal file
1
replit_data/subtasks.json
Normal 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}]
|
||||
1
replit_data/time-study.json
Normal file
1
replit_data/time-study.json
Normal file
@ -0,0 +1 @@
|
||||
{"message":"Unauthorized"}
|
||||
72
replit_data/users.json
Normal file
72
replit_data/users.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
16
replit_data/worker-batch-queue.json
Normal file
16
replit_data/worker-batch-queue.json
Normal 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
16
replit_db_export.json
Normal 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>
|
||||
1
replit_db_export_full.json
Normal file
1
replit_db_export_full.json
Normal file
File diff suppressed because one or more lines are too long
82
scan.php
Normal file
82
scan.php
Normal 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
185
shop_floor.php
Normal 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
1
subtasks.json
Normal 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
114
time_study.php
Normal 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
40
ui_review.md
Normal 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
1
users.json
Normal 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"}]
|
||||
@ -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">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user