Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e069921c28 | ||
|
|
951a96262b | ||
|
|
4349e548ed | ||
|
|
6d06eea56a |
BIN
R6 BOM.xlsx
Normal file
BIN
R6 BOM.xlsx
Normal file
Binary file not shown.
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()]);
|
||||
}
|
||||
115
api/ops.php
Normal file
115
api/ops.php
Normal file
@ -0,0 +1,115 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../db/config.php';
|
||||
$db = db();
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$userId = $_SESSION['user_id'];
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!$data || !isset($data['opId']) || !isset($data['action'])) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid data']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$opId = $data['opId'];
|
||||
$action = $data['action'];
|
||||
$reason = $data['reason'] ?? null;
|
||||
|
||||
try {
|
||||
$db->beginTransaction();
|
||||
|
||||
// Fetch current op state
|
||||
$stmt = $db->prepare("SELECT * FROM operations WHERE id = ?");
|
||||
$stmt->execute([$opId]);
|
||||
$op = $stmt->fetch();
|
||||
|
||||
if (!$op) {
|
||||
throw new Exception("Operation not found");
|
||||
}
|
||||
|
||||
$eventType = '';
|
||||
$statusUpdate = '';
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
switch ($action) {
|
||||
case 'start':
|
||||
$statusUpdate = "status = 'in_progress', assigned_worker_id = $userId, start_time = IFNULL(start_time, '$now')";
|
||||
$eventType = 'start';
|
||||
// If it was stalled, event type is 'resume'
|
||||
if ($op['status'] === 'stalled') {
|
||||
$eventType = 'resume';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'stall':
|
||||
$statusUpdate = "status = 'stalled', stall_reason = " . $db->quote($reason);
|
||||
$eventType = 'stalled';
|
||||
break;
|
||||
|
||||
case 'done':
|
||||
$statusUpdate = "status = 'completed', end_time = '$now'";
|
||||
$eventType = 'completed';
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception("Invalid action");
|
||||
}
|
||||
|
||||
// Update operation
|
||||
$db->exec("UPDATE operations SET $statusUpdate WHERE id = $opId");
|
||||
|
||||
// Log time study event
|
||||
$stmt = $db->prepare("INSERT INTO time_study_events (operation_id, user_id, event_type, reason) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$opId, $userId, $eventType, $reason]);
|
||||
|
||||
// Post-completion logic (if done)
|
||||
if ($action === 'done') {
|
||||
// Check if all operations for this component are done
|
||||
$compId = $op['component_id'];
|
||||
$pendingOps = $db->prepare("SELECT COUNT(*) FROM operations WHERE component_id = ? AND status != 'completed'");
|
||||
$pendingOps->execute([$compId]);
|
||||
if ($pendingOps->fetchColumn() == 0) {
|
||||
// Component is done
|
||||
$db->exec("UPDATE components SET status = 'completed' WHERE id = $compId");
|
||||
|
||||
// Check if all components for this job are done
|
||||
$stmt = $db->prepare("SELECT job_id FROM components WHERE id = ?");
|
||||
$stmt->execute([$compId]);
|
||||
$jobId = $stmt->fetchColumn();
|
||||
|
||||
$pendingComps = $db->prepare("SELECT COUNT(*) FROM components WHERE job_id = ? AND status != 'completed'");
|
||||
$pendingComps->execute([$jobId]);
|
||||
if ($pendingComps->fetchColumn() == 0) {
|
||||
// Job is done
|
||||
$db->exec("UPDATE jobs SET status = 'completed' WHERE id = $jobId");
|
||||
} else {
|
||||
// Job is in_progress if not already
|
||||
$db->exec("UPDATE jobs SET status = 'in_progress' WHERE id = $jobId");
|
||||
}
|
||||
} else {
|
||||
// Component is in_progress if not already
|
||||
$db->exec("UPDATE components SET status = 'in_progress' WHERE id = $compId");
|
||||
|
||||
// Job is in_progress
|
||||
$stmt = $db->prepare("SELECT job_id FROM components WHERE id = ?");
|
||||
$stmt->execute([$compId]);
|
||||
$jobId = $stmt->fetchColumn();
|
||||
$db->exec("UPDATE jobs SET status = 'in_progress' WHERE id = $jobId");
|
||||
}
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
echo json_encode(['success' => true]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$db->rollBack();
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
90
auth.php
Normal file
90
auth.php
Normal file
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
session_start();
|
||||
require 'db/config.php';
|
||||
|
||||
function loginWorker($userId) {
|
||||
$db = db();
|
||||
$stmt = $db->prepare("SELECT * FROM users WHERE id = ? AND role = 'worker'");
|
||||
$stmt->execute([$userId]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($user) {
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['user_name'] = $user['name'];
|
||||
$_SESSION['role'] = 'worker';
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function loginAdmin($userId, $pin) {
|
||||
$db = db();
|
||||
$stmt = $db->prepare("SELECT * FROM users WHERE id = ? AND role = 'admin'");
|
||||
$stmt->execute([$userId]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($user) {
|
||||
// If no PIN set, allow setup (bootstrap mode)
|
||||
if ($user['pin_hash'] === null) {
|
||||
// This is special case, first time login
|
||||
$_SESSION['pending_setup_user_id'] = $user['id'];
|
||||
return 'setup';
|
||||
}
|
||||
|
||||
if (password_verify($pin, $user['pin_hash'])) {
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['user_name'] = $user['name'];
|
||||
$_SESSION['role'] = 'admin';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function setupAdminPin($userId, $pin) {
|
||||
$db = db();
|
||||
$hash = password_hash($pin, PASSWORD_BCRYPT);
|
||||
$stmt = $db->prepare("UPDATE users SET pin_hash = ? WHERE id = ? AND role = 'admin'");
|
||||
return $stmt->execute([$hash, $userId]);
|
||||
}
|
||||
|
||||
// Check POST requests
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'login_worker') {
|
||||
if (loginWorker($_POST['user_id'])) {
|
||||
header('Location: dashboard.php');
|
||||
exit;
|
||||
}
|
||||
} elseif ($action === 'login_admin') {
|
||||
$res = loginAdmin($_POST['user_id'], $_POST['pin']);
|
||||
if ($res === true) {
|
||||
header('Location: dashboard.php');
|
||||
exit;
|
||||
} elseif ($res === 'setup') {
|
||||
header('Location: index.php?setup=1');
|
||||
exit;
|
||||
}
|
||||
} elseif ($action === 'setup_pin') {
|
||||
$userId = $_SESSION['pending_setup_user_id'] ?? null;
|
||||
if ($userId && !empty($_POST['pin'])) {
|
||||
if (setupAdminPin($userId, $_POST['pin'])) {
|
||||
unset($_SESSION['pending_setup_user_id']);
|
||||
// Auto login after setup
|
||||
$db = db();
|
||||
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
|
||||
$stmt->execute([$userId]);
|
||||
$user = $stmt->fetch();
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['user_name'] = $user['name'];
|
||||
$_SESSION['role'] = 'admin';
|
||||
header('Location: dashboard.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: index.php?error=1');
|
||||
exit;
|
||||
}
|
||||
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"}]
|
||||
5
cookies.txt
Normal file
5
cookies.txt
Normal file
@ -0,0 +1,5 @@
|
||||
# Netscape HTTP Cookie File
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
assembly-flow--alexmillcity.replit.app FALSE / FALSE 1774821327 GAESA Cp4BMDBkYTZjZDJjNDcxZmM2Y2MwMWJiZWUzMGRjNDQwM2E5MGIzMmM2MGJmZGFlZGY2NmNjM2ExMzE0NTJlNjgyYmJhOWU3YTY3Y2U3YTRkOWU3ZTA4ZGM4N2M3ZTYyNmU1NmNlNGM3MjkzZDI1ZGFhMjcwZjlkOGVjOWVmZTEwOGMyMTRhNmRlMzZjYzQ3OWVlNGVhZGUxNDE0ZTI3ZDgQo_jliMoz
|
||||
464
dashboard.php
Normal file
464
dashboard.php
Normal file
@ -0,0 +1,464 @@
|
||||
<?php
|
||||
session_start();
|
||||
require 'db/config.php';
|
||||
$db = db();
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$role = $_SESSION['role'];
|
||||
$userName = $_SESSION['user_name'];
|
||||
$userId = $_SESSION['user_id'];
|
||||
|
||||
// Get assigned processes for worker
|
||||
$assignedProcesses = [];
|
||||
if ($role === 'worker') {
|
||||
$stmt = $db->prepare("SELECT assigned_processes FROM users WHERE id = ?");
|
||||
$stmt->execute([$userId]);
|
||||
$res = $stmt->fetch();
|
||||
$assignedProcesses = json_decode($res['assigned_processes'] ?? '[]', true);
|
||||
}
|
||||
|
||||
// Stats for Admin
|
||||
$stats = [
|
||||
'active_jobs' => 0,
|
||||
'pending_ops' => 0,
|
||||
'inventory_alerts' => 0,
|
||||
'active_workers' => 0
|
||||
];
|
||||
|
||||
if ($role === 'admin') {
|
||||
$stats['active_jobs'] = $db->query("SELECT COUNT(*) FROM jobs WHERE status = 'in_progress'")->fetchColumn();
|
||||
$stats['pending_ops'] = $db->query("SELECT COUNT(*) FROM operations WHERE status = 'pending'")->fetchColumn();
|
||||
$stats['inventory_alerts'] = $db->query("SELECT COUNT(*) FROM inventory WHERE stock_level <= reorder_level")->fetchColumn();
|
||||
$stats['active_workers'] = $db->query("SELECT COUNT(DISTINCT assigned_worker_id) FROM operations WHERE status = 'in_progress'")->fetchColumn();
|
||||
|
||||
// Fetch recent jobs
|
||||
$recentJobs = $db->query("SELECT * FROM jobs ORDER BY created_at DESC LIMIT 5")->fetchAll();
|
||||
}
|
||||
|
||||
// Queue for Worker or Admin View
|
||||
$queue = [];
|
||||
if ($role === 'worker') {
|
||||
if (!empty($assignedProcesses)) {
|
||||
$placeholders = implode(',', array_fill(0, count($assignedProcesses), '?'));
|
||||
$stmt = $db->prepare("
|
||||
SELECT o.*, c.name as component_name, j.name as job_name
|
||||
FROM operations o
|
||||
JOIN components c ON o.component_id = c.id
|
||||
JOIN jobs j ON c.job_id = j.id
|
||||
WHERE o.status IN ('pending', 'in_progress', 'stalled')
|
||||
AND o.process_type IN ($placeholders)
|
||||
ORDER BY o.priority DESC, o.created_at ASC
|
||||
");
|
||||
$stmt->execute($assignedProcesses);
|
||||
$queue = $stmt->fetchAll();
|
||||
}
|
||||
} else {
|
||||
// Admin sees all in-progress or stalled ops
|
||||
$queue = $db->query("
|
||||
SELECT o.*, c.name as component_name, j.name as job_name, u.name as worker_name
|
||||
FROM operations o
|
||||
JOIN components c ON o.component_id = c.id
|
||||
JOIN jobs j ON c.job_id = j.id
|
||||
LEFT JOIN users u ON o.assigned_worker_id = u.id
|
||||
WHERE o.status IN ('in_progress', 'stalled')
|
||||
ORDER BY o.created_at ASC
|
||||
")->fetchAll();
|
||||
}
|
||||
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>M-TRACK | Dashboard</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--sidebar-width: 240px;
|
||||
--bg: #f8fafc;
|
||||
--primary: #1e293b;
|
||||
--accent: #3b82f6;
|
||||
--text: #334155;
|
||||
--border: #e2e8f0;
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
--danger: #ef4444;
|
||||
}
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
z-index: 1000;
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
.sidebar h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 2rem;
|
||||
padding: 0 0.5rem;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
.nav-pills .nav-link {
|
||||
color: #94a3b8;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
padding: 0.625rem 0.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.nav-pills .nav-link:hover {
|
||||
color: white;
|
||||
background-color: rgba(255,255,255,0.05);
|
||||
}
|
||||
.nav-pills .nav-link.active {
|
||||
color: white;
|
||||
background-color: var(--accent);
|
||||
}
|
||||
.main-content {
|
||||
margin-left: var(--sidebar-width);
|
||||
padding: 2.5rem;
|
||||
}
|
||||
.top-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.top-bar h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
margin: 0;
|
||||
}
|
||||
.user-pill {
|
||||
background: white;
|
||||
border: 1px solid var(--border);
|
||||
padding: 0.375rem 1rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.card {
|
||||
background: white;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.card-header {
|
||||
background: white;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 1rem 1.25rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: var(--primary);
|
||||
}
|
||||
.badge-process {
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.625rem;
|
||||
border-radius: 4px;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.op-card {
|
||||
padding: 1.25rem;
|
||||
border-left: 4px solid #cbd5e1;
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
.op-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.op-card.status-in_progress { border-left-color: var(--success); }
|
||||
.op-card.status-stalled { border-left-color: var(--warning); }
|
||||
.op-card.status-pending { border-left-color: #cbd5e1; }
|
||||
|
||||
.op-meta {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
margin-bottom: 0.25rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.op-title {
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
color: var(--primary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.op-job {
|
||||
font-size: 0.875rem;
|
||||
color: #64748b;
|
||||
}
|
||||
.btn-action {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="sidebar">
|
||||
<h2>M-TRACK</h2>
|
||||
<nav class="nav nav-pills flex-column">
|
||||
<a class="nav-link active" href="dashboard.php"><i class="bi bi-grid-fill me-2"></i> Dashboard</a>
|
||||
<?php if ($role === 'admin'): ?>
|
||||
<a class="nav-link" href="jobs.php"><i class="bi bi-briefcase me-2"></i> Jobs</a>
|
||||
<a class="nav-link" href="shop_floor.php"><i class="bi bi-kanban me-2"></i> Shop Floor</a>
|
||||
<a class="nav-link" href="inventory.php"><i class="bi bi-boxes me-2"></i> Inventory</a>
|
||||
<a class="nav-link" href="users.php"><i class="bi bi-people me-2"></i> Users</a>
|
||||
<a class="nav-link" href="time_study.php"><i class="bi bi-clock-history me-2"></i> Time Study</a>
|
||||
<a class="nav-link" href="scan.php"><i class="bi bi-upc-scan me-2"></i> Scan</a>
|
||||
<?php else: ?>
|
||||
<a class="nav-link" href="dashboard.php"><i class="bi bi-list-task me-2"></i> My Queue</a>
|
||||
<a class="nav-link" href="shop_floor.php"><i class="bi bi-kanban me-2"></i> Board View</a>
|
||||
<a class="nav-link" href="scan.php"><i class="bi bi-upc-scan me-2"></i> Scan</a>
|
||||
<?php endif; ?>
|
||||
<hr class="my-4 border-secondary opacity-25">
|
||||
<a class="nav-link text-danger" href="logout.php"><i class="bi bi-box-arrow-right me-2"></i> Logout</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="top-bar">
|
||||
<h1><?= $role === 'admin' ? 'Operations Overview' : 'Work Queue' ?></h1>
|
||||
<div class="user-pill">
|
||||
<i class="bi bi-person-circle text-muted"></i>
|
||||
<?= htmlspecialchars($userName) ?>
|
||||
<span class="text-muted text-uppercase" style="font-size: 0.625rem;"><?= $role ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($role === 'worker'): ?>
|
||||
<div class="mb-4 d-flex align-items-center">
|
||||
<span class="text-muted small fw-bold text-uppercase me-3">My Skills:</span>
|
||||
<?php foreach ($assignedProcesses as $proc): ?>
|
||||
<span class="badge-process"><?= strtoupper($proc) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<?php if (empty($queue)): ?>
|
||||
<div class="col-12">
|
||||
<div class="card card-body py-5 text-center">
|
||||
<div class="mb-3 text-muted" style="font-size: 2rem;"><i class="bi bi-clipboard-check"></i></div>
|
||||
<h5 class="text-muted">No pending operations for your assigned processes.</h5>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($queue as $op): ?>
|
||||
<div class="col-md-6 col-xl-4">
|
||||
<div class="card op-card status-<?= $op['status'] ?>">
|
||||
<div class="op-meta"><?= $op['process_type'] ?> • Priority <?= $op['priority'] ?></div>
|
||||
<div class="op-title"><?= htmlspecialchars($op['name']) ?></div>
|
||||
<div class="op-job mb-3">
|
||||
<i class="bi bi-box-seam me-1"></i> <?= htmlspecialchars($op['job_name']) ?> / <?= htmlspecialchars($op['component_name']) ?>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<?php if ($op['status'] === 'pending' || $op['status'] === 'stalled'): ?>
|
||||
<button class="btn btn-success btn-action w-100" onclick="handleOp(<?= $op['id'] ?>, 'start')">
|
||||
<?= $op['status'] === 'stalled' ? 'Resume' : 'Start' ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($op['status'] === 'in_progress'): ?>
|
||||
<button class="btn btn-warning btn-action w-50" onclick="handleOp(<?= $op['id'] ?>, 'stall')">Stall</button>
|
||||
<button class="btn btn-primary btn-action w-50" onclick="handleOp(<?= $op['id'] ?>, 'done')">Done</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<!-- Admin View -->
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center p-3">
|
||||
<div class="text-muted small fw-bold text-uppercase mb-1">Active Jobs</div>
|
||||
<div class="h3 fw-bold mb-0"><?= $stats['active_jobs'] ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center p-3">
|
||||
<div class="text-muted small fw-bold text-uppercase mb-1">Pending Ops</div>
|
||||
<div class="h3 fw-bold mb-0"><?= $stats['pending_ops'] ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center p-3">
|
||||
<div class="text-muted small fw-bold text-uppercase mb-1">Inventory Alerts</div>
|
||||
<div class="h3 fw-bold mb-0 text-<?= $stats['inventory_alerts'] > 0 ? 'danger' : 'success' ?>">
|
||||
<?= $stats['inventory_alerts'] ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center p-3">
|
||||
<div class="text-muted small fw-bold text-uppercase mb-1">Active Workers</div>
|
||||
<div class="h3 fw-bold mb-0"><?= $stats['active_workers'] ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
Live Production Status
|
||||
<span class="badge bg-primary rounded-pill"><?= count($queue) ?> Active Ops</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<?php if (empty($queue)): ?>
|
||||
<div class="p-5 text-center text-muted">No operations currently in progress.</div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Operation</th>
|
||||
<th>Worker</th>
|
||||
<th>Status</th>
|
||||
<th>Started</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($queue as $op): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-bold"><?= htmlspecialchars($op['name']) ?></div>
|
||||
<div class="small text-muted"><?= htmlspecialchars($op['job_name']) ?></div>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($op['worker_name'] ?? 'Unassigned') ?></td>
|
||||
<td>
|
||||
<span class="badge bg-<?= $op['status'] === 'in_progress' ? 'success' : 'warning' ?>">
|
||||
<?= strtoupper($op['status']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="small"><?= date('H:i', strtotime($op['start_time'])) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header">Recent Jobs</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="list-group list-group-flush">
|
||||
<?php foreach ($recentJobs as $job): ?>
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<span class="fw-bold"><?= htmlspecialchars($job['name']) ?></span>
|
||||
<span class="badge bg-light text-dark border small"><?= $job['status'] ?></span>
|
||||
</div>
|
||||
<div class="small text-muted">Due: <?= $job['due_date'] ?> • Qty: <?= $job['quantity'] ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($recentJobs)): ?>
|
||||
<div class="p-4 text-center text-muted">No jobs created yet.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-white">
|
||||
<a href="jobs.php" class="btn btn-sm btn-outline-primary w-100">Manage Jobs</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Modal for Stall Reason -->
|
||||
<div class="modal fade" id="stallModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Stall Reason</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<textarea id="stallReason" class="form-control" placeholder="Explain why work is stalling (e.g., missing hardware, machine down)..."></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-warning" onclick="confirmStall()">Confirm Stall</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
let currentOpId = null;
|
||||
const stallModal = new bootstrap.Modal(document.getElementById('stallModal'));
|
||||
|
||||
function handleOp(opId, action) {
|
||||
if (action === 'stall') {
|
||||
currentOpId = opId;
|
||||
stallModal.show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'done' && !confirm('Mark this operation as complete?')) return;
|
||||
|
||||
performAction(opId, action);
|
||||
}
|
||||
|
||||
function confirmStall() {
|
||||
const reason = document.getElementById('stallReason').value.trim();
|
||||
if (!reason) {
|
||||
alert('Please provide a reason for stalling.');
|
||||
return;
|
||||
}
|
||||
performAction(currentOpId, 'stall', reason);
|
||||
stallModal.hide();
|
||||
}
|
||||
|
||||
function performAction(opId, action, reason = '') {
|
||||
fetch('api/ops.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ opId, action, reason })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error: ' + res.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
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";
|
||||
}
|
||||
293
index.php
293
index.php
@ -1,150 +1,207 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
session_start();
|
||||
require 'db/config.php';
|
||||
$db = db();
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
header('Location: dashboard.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$workers = $db->query("SELECT * FROM users WHERE role = 'worker'")->fetchAll();
|
||||
$admins = $db->query("SELECT * FROM users WHERE role = 'admin'")->fetchAll();
|
||||
$isSetup = isset($_GET['setup']) && isset($_SESSION['pending_setup_user_id']);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>New Style</title>
|
||||
<?php
|
||||
// Read project preview data from environment
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<?php if ($projectDescription): ?>
|
||||
<!-- Meta description -->
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<!-- Open Graph meta tags -->
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<!-- Twitter meta tags -->
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<!-- Open Graph image -->
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<!-- Twitter image -->
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<?php endif; ?>
|
||||
<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=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<title>M-TRACK | Login</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-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
--bg: #f8fafc;
|
||||
--card-bg: #ffffff;
|
||||
--primary: #1e293b;
|
||||
--accent: #3b82f6;
|
||||
--text: #334155;
|
||||
--border: #e2e8f0;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 2.5rem;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
.login-header {
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.login-header h1 {
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
color: var(--primary);
|
||||
letter-spacing: -0.025em;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.login-header p {
|
||||
font-size: 0.875rem;
|
||||
color: #64748b;
|
||||
}
|
||||
.nav-tabs {
|
||||
border-bottom: 2px solid var(--border);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.nav-link {
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
border: none !important;
|
||||
padding: 0.75rem 0;
|
||||
margin-right: 1.5rem;
|
||||
position: relative;
|
||||
}
|
||||
body::before {
|
||||
.nav-link.active {
|
||||
color: var(--accent) !important;
|
||||
background: none !important;
|
||||
}
|
||||
.nav-link.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
height: 2px;
|
||||
background-color: var(--accent);
|
||||
}
|
||||
@keyframes bg-pan {
|
||||
0% { background-position: 0% 0%; }
|
||||
100% { background-position: 100% 100%; }
|
||||
.form-label {
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
color: var(--primary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.loader {
|
||||
margin: 1.25rem auto 1.25rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.hint {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap; border: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
code {
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 2px 6px;
|
||||
.form-select, .form-control {
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
border-color: var(--border);
|
||||
padding: 0.625rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
.form-select:focus, .form-control:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
.btn-primary {
|
||||
background-color: var(--primary);
|
||||
border: none;
|
||||
padding: 0.625rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background-color: #0f172a;
|
||||
}
|
||||
.alert {
|
||||
font-size: 0.8125rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
</div>
|
||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||
|
||||
<div class="login-card">
|
||||
<div class="login-header">
|
||||
<h1>M-TRACK</h1>
|
||||
<p>Manufacturing Control System</p>
|
||||
</div>
|
||||
|
||||
<?php if (isset($_GET['error'])): ?>
|
||||
<div class="alert alert-danger">
|
||||
<i class="bi bi-exclamation-circle me-2"></i> Invalid selection or PIN.
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($isSetup): ?>
|
||||
<form action="auth.php" method="POST">
|
||||
<input type="hidden" name="action" value="setup_pin">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Create Admin PIN (4-6 digits)</label>
|
||||
<input type="password" name="pin" class="form-control" required pattern="\d{4,6}" inputmode="numeric">
|
||||
<div class="form-text mt-2">Initial setup for <?= htmlspecialchars($_SESSION['pending_setup_user_id']) ?></div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Set PIN & Login</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<ul class="nav nav-tabs" id="loginTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="worker-tab" data-bs-toggle="tab" data-bs-target="#worker" type="button" role="tab">Worker</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="admin-tab" data-bs-toggle="tab" data-bs-target="#admin" type="button" role="tab">Admin</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="loginTabsContent">
|
||||
<!-- Worker Tab -->
|
||||
<div class="tab-pane fade show active" id="worker" role="tabpanel">
|
||||
<form action="auth.php" method="POST">
|
||||
<input type="hidden" name="action" value="login_worker">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Select Name</label>
|
||||
<select name="user_id" class="form-select" required>
|
||||
<option value="">Select...</option>
|
||||
<?php foreach ($workers as $w): ?>
|
||||
<option value="<?= $w['id'] ?>"><?= htmlspecialchars($w['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Admin Tab -->
|
||||
<div class="tab-pane fade" id="admin" role="tabpanel">
|
||||
<form action="auth.php" method="POST">
|
||||
<input type="hidden" name="action" value="login_admin">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Select Name</label>
|
||||
<select name="user_id" class="form-select" required>
|
||||
<option value="">Select...</option>
|
||||
<?php foreach ($admins as $a): ?>
|
||||
<option value="<?= $a['id'] ?>"><?= htmlspecialchars($a['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Enter PIN</label>
|
||||
<input type="password" name="pin" class="form-control" placeholder="••••" inputmode="numeric">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
266
inventory.php
Normal file
266
inventory.php
Normal file
@ -0,0 +1,266 @@
|
||||
<?php
|
||||
session_start();
|
||||
require 'db/config.php';
|
||||
$db = db();
|
||||
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$userId = $_SESSION['user_id'];
|
||||
|
||||
// Handle Inventory Update
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_stock'])) {
|
||||
$itemId = $_POST['item_id'];
|
||||
$qty = $_POST['quantity'];
|
||||
$type = $_POST['type']; // in or out
|
||||
|
||||
try {
|
||||
$db->beginTransaction();
|
||||
|
||||
// Record transaction
|
||||
$stmt = $db->prepare("INSERT INTO inventory_transactions (inventory_id, type, quantity, user_id) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$itemId, $type, $qty, $userId]);
|
||||
|
||||
// Update stock
|
||||
$mod = ($type === 'in' ? '+' : '-');
|
||||
$db->exec("UPDATE inventory SET stock_level = stock_level $mod $qty WHERE id = $itemId");
|
||||
|
||||
$db->commit();
|
||||
header("Location: inventory.php?success=1");
|
||||
exit;
|
||||
} catch (Exception $e) {
|
||||
$db->rollBack();
|
||||
$error = "Error: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle New Item
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['new_item'])) {
|
||||
$name = $_POST['name'];
|
||||
$cat = $_POST['category'];
|
||||
$stock = $_POST['stock_level'];
|
||||
$reorder = $_POST['reorder_level'];
|
||||
$unit = $_POST['unit'];
|
||||
|
||||
$stmt = $db->prepare("INSERT INTO inventory (name, category, stock_level, reorder_level, unit) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$name, $cat, $stock, $reorder, $unit]);
|
||||
header("Location: inventory.php?item_added=1");
|
||||
exit;
|
||||
}
|
||||
|
||||
$inventory = $db->query("SELECT * FROM inventory ORDER BY name ASC")->fetchAll();
|
||||
$lowStock = array_filter($inventory, fn($i) => $i['stock_level'] <= $i['reorder_level']);
|
||||
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>M-TRACK | Inventory</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--sidebar-width: 240px;
|
||||
--bg: #f8fafc;
|
||||
--primary: #1e293b;
|
||||
--accent: #3b82f6;
|
||||
--text: #334155;
|
||||
--border: #e2e8f0;
|
||||
--danger: #ef4444;
|
||||
}
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
z-index: 1000;
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
.main-content {
|
||||
margin-left: var(--sidebar-width);
|
||||
padding: 2.5rem;
|
||||
}
|
||||
.card {
|
||||
background: white;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.alert-low {
|
||||
border-left: 4px solid var(--danger);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="sidebar">
|
||||
<h2>M-TRACK</h2>
|
||||
<nav class="nav nav-pills flex-column">
|
||||
<a class="nav-link" href="dashboard.php"><i class="bi bi-grid-fill me-2"></i> Dashboard</a>
|
||||
<a class="nav-link" href="jobs.php"><i class="bi bi-briefcase me-2"></i> Jobs</a>
|
||||
<a class="nav-link active" href="inventory.php"><i class="bi bi-boxes me-2"></i> Inventory</a>
|
||||
<a class="nav-link" href="users.php"><i class="bi bi-people me-2"></i> Users</a>
|
||||
<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>Inventory Management</h1>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#newItemModal">
|
||||
<i class="bi bi-plus-lg me-1"></i> New Item
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($lowStock)): ?>
|
||||
<div class="alert alert-danger d-flex align-items-center mb-4">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
<div><strong>Low Stock Warning:</strong> <?= count($lowStock) ?> items are below reorder level.</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0 align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Item Name</th>
|
||||
<th>Category</th>
|
||||
<th>Current Stock</th>
|
||||
<th>Reorder Level</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($inventory)): ?>
|
||||
<tr><td colspan="5" class="p-5 text-center text-muted">No inventory items found.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($inventory as $item): ?>
|
||||
<tr class="<?= $item['stock_level'] <= $item['reorder_level'] ? 'table-danger alert-low' : '' ?>">
|
||||
<td>
|
||||
<div class="fw-bold"><?= htmlspecialchars($item['name']) ?></div>
|
||||
<div class="small text-muted"><?= $item['unit'] ?></div>
|
||||
</td>
|
||||
<td><span class="badge bg-light text-dark border"><?= strtoupper($item['category']) ?></span></td>
|
||||
<td>
|
||||
<span class="fw-bold <?= $item['stock_level'] <= $item['reorder_level'] ? 'text-danger' : '' ?>">
|
||||
<?= $item['stock_level'] ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?= $item['reorder_level'] ?></td>
|
||||
<td class="text-end">
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="setUpdateItem(<?= $item['id'] ?>, '<?= htmlspecialchars($item['name']) ?>')" data-bs-toggle="modal" data-bs-target="#updateModal">
|
||||
Update Stock
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Item Modal -->
|
||||
<div class="modal fade" id="newItemModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<form method="POST" class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Add Inventory Item</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Item Name</label>
|
||||
<input type="text" name="name" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Category</label>
|
||||
<select name="category" class="form-select">
|
||||
<option value="material">Raw Material</option>
|
||||
<option value="consumable">Consumable</option>
|
||||
<option value="hardware">Hardware</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Initial Stock</label>
|
||||
<input type="number" step="0.01" name="stock_level" class="form-control" value="0">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Reorder Level</label>
|
||||
<input type="number" step="0.01" name="reorder_level" class="form-control" value="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Unit</label>
|
||||
<input type="text" name="unit" class="form-control" placeholder="pcs, kg, meters, etc.">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" name="new_item" class="btn btn-primary">Add Item</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Update Stock Modal -->
|
||||
<div class="modal fade" id="updateModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<form method="POST" class="modal-content">
|
||||
<input type="hidden" name="item_id" id="update_item_id">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Update Stock: <span id="update_item_name"></span></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Transaction Type</label>
|
||||
<select name="type" class="form-select">
|
||||
<option value="in">Stock In (Receive)</option>
|
||||
<option value="out">Stock Out (Consume)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Quantity</label>
|
||||
<input type="number" step="0.01" name="quantity" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" name="update_stock" class="btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
function setUpdateItem(id, name) {
|
||||
document.getElementById('update_item_id').value = id;
|
||||
document.getElementById('update_item_name').innerText = name;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
18
job_schema.json
Normal file
18
job_schema.json
Normal file
File diff suppressed because one or more lines are too long
424
jobs.php
Normal file
424
jobs.php
Normal file
@ -0,0 +1,424 @@
|
||||
<?php
|
||||
session_start();
|
||||
require 'db/config.php';
|
||||
$db = db();
|
||||
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$userName = $_SESSION['user_name'];
|
||||
$userId = $_SESSION['user_id'];
|
||||
|
||||
// Handle Job Creation
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_job'])) {
|
||||
$name = $_POST['name'];
|
||||
$qty = $_POST['quantity'];
|
||||
$due = $_POST['due_date'];
|
||||
$sn = $_POST['serial_number'];
|
||||
|
||||
try {
|
||||
$stmt = $db->prepare("INSERT INTO jobs (name, quantity, due_date, serial_number) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$name, $qty, empty($due) ? null : $due, empty($sn) ? null : $sn]);
|
||||
$jobId = $db->lastInsertId();
|
||||
header("Location: jobs.php?id=$jobId");
|
||||
exit;
|
||||
} catch (Exception $e) {
|
||||
$error = "Error: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Component Addition
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_component'])) {
|
||||
$jobId = $_POST['job_id'];
|
||||
$compName = $_POST['comp_name'];
|
||||
$stmt = $db->prepare("INSERT INTO components (job_id, name) VALUES (?, ?)");
|
||||
$stmt->execute([$jobId, $compName]);
|
||||
header("Location: jobs.php?id=$jobId");
|
||||
exit;
|
||||
}
|
||||
|
||||
// Handle Operation Addition
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_op'])) {
|
||||
$jobId = $_POST['job_id'];
|
||||
$compId = $_POST['comp_id'];
|
||||
$opName = $_POST['op_name'];
|
||||
$proc = $_POST['process_type'];
|
||||
$prio = $_POST['priority'];
|
||||
$stmt = $db->prepare("INSERT INTO operations (component_id, name, process_type, priority) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$compId, $opName, $proc, $prio]);
|
||||
header("Location: jobs.php?id=$jobId");
|
||||
exit;
|
||||
}
|
||||
|
||||
$jobs = $db->query("SELECT * FROM jobs ORDER BY created_at DESC")->fetchAll();
|
||||
|
||||
$currentJobId = $_GET['id'] ?? null;
|
||||
$currentJob = null;
|
||||
$components = [];
|
||||
|
||||
if ($currentJobId) {
|
||||
$stmt = $db->prepare("SELECT * FROM jobs WHERE id = ?");
|
||||
$stmt->execute([$currentJobId]);
|
||||
$currentJob = $stmt->fetch();
|
||||
|
||||
if ($currentJob) {
|
||||
$stmt = $db->prepare("SELECT * FROM components WHERE job_id = ?");
|
||||
$stmt->execute([$currentJobId]);
|
||||
$components = $stmt->fetchAll();
|
||||
|
||||
foreach ($components as &$comp) {
|
||||
$stmt = $db->prepare("SELECT * FROM operations WHERE component_id = ?");
|
||||
$stmt->execute([$comp['id']]);
|
||||
$comp['ops'] = $stmt->fetchAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>M-TRACK | Job Management</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--sidebar-width: 240px;
|
||||
--bg: #f8fafc;
|
||||
--primary: #1e293b;
|
||||
--accent: #3b82f6;
|
||||
--text: #334155;
|
||||
--border: #e2e8f0;
|
||||
}
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
z-index: 1000;
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
.main-content {
|
||||
margin-left: var(--sidebar-width);
|
||||
padding: 2.5rem;
|
||||
}
|
||||
.sidebar h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 2rem;
|
||||
padding: 0 0.5rem;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
.nav-pills .nav-link {
|
||||
color: #94a3b8;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
padding: 0.625rem 0.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.nav-pills .nav-link:hover {
|
||||
color: white;
|
||||
background-color: rgba(255,255,255,0.05);
|
||||
}
|
||||
.nav-pills .nav-link.active {
|
||||
color: white;
|
||||
background-color: var(--accent);
|
||||
}
|
||||
.card {
|
||||
background: white;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.card-header {
|
||||
background: white;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 1rem 1.25rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: var(--primary);
|
||||
}
|
||||
.job-item {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
}
|
||||
.job-item:hover { background: #f1f5f9; }
|
||||
.job-item.active { background: #eff6ff; border-left: 3px solid var(--accent); }
|
||||
.component-box {
|
||||
background: #f8fafc;
|
||||
border: 1px solid var(--border);
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="sidebar">
|
||||
<h2>M-TRACK</h2>
|
||||
<nav class="nav nav-pills flex-column">
|
||||
<a class="nav-link" href="dashboard.php"><i class="bi bi-grid-fill me-2"></i> Dashboard</a>
|
||||
<a class="nav-link active" href="jobs.php"><i class="bi bi-briefcase me-2"></i> Jobs</a>
|
||||
<a class="nav-link" href="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">
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">Existing Jobs</div>
|
||||
<div class="card-body p-0" style="max-height: 70vh; overflow-y: auto;">
|
||||
<?php if (empty($jobs)): ?>
|
||||
<div class="p-4 text-center text-muted">No jobs found.</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($jobs as $job): ?>
|
||||
<div class="job-item <?= $currentJobId == $job['id'] ? 'active' : '' ?>" onclick="location.href='jobs.php?id=<?= $job['id'] ?>'">
|
||||
<div class="fw-bold"><?= htmlspecialchars($job['name']) ?></div>
|
||||
<div class="small text-muted">SN: <?= htmlspecialchars($job['serial_number'] ?: 'N/A') ?> • <?= $job['status'] ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<?php if ($currentJob): ?>
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between">
|
||||
Job Details: <?= htmlspecialchars($currentJob['name']) ?>
|
||||
<span class="badge bg-light text-dark border"><?= strtoupper($currentJob['status']) ?></span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row small mb-3">
|
||||
<div class="col-md-3"><strong>Serial:</strong> <?= htmlspecialchars($currentJob['serial_number']) ?></div>
|
||||
<div class="col-md-3"><strong>Quantity:</strong> <?= $currentJob['quantity'] ?></div>
|
||||
<div class="col-md-3"><strong>Due Date:</strong> <?= $currentJob['due_date'] ?></div>
|
||||
<div class="col-md-3 text-end"><button class="btn btn-sm btn-outline-danger">Delete Job</button></div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="mb-0 fw-bold">Components & Operations</h6>
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered bg-white mb-0">
|
||||
<thead class="table-light">
|
||||
<tr class="small">
|
||||
<th>Op Name</th>
|
||||
<th>Process</th>
|
||||
<th>Prio</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="small">
|
||||
<?php if (empty($comp['ops'])): ?>
|
||||
<tr><td colspan="4" class="text-center text-muted">No operations defined.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($comp['ops'] as $op): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($op['name']) ?></td>
|
||||
<td><?= strtoupper($op['process_type']) ?></td>
|
||||
<td><?= $op['priority'] ?></td>
|
||||
<td>
|
||||
<span class="badge bg-<?= $op['status'] === 'completed' ? 'success' : ($op['status'] === 'in_progress' ? 'info' : 'secondary') ?>">
|
||||
<?= $op['status'] ?>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="card py-5 text-center text-muted">
|
||||
<i class="bi bi-arrow-left h1 mb-3"></i>
|
||||
<h5>Select a job from the list or create a new one.</h5>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Job Modal -->
|
||||
<div class="modal fade" id="newJobModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<form method="POST" class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">New Job</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Job Name</label>
|
||||
<input type="text" name="name" class="form-control" required placeholder="e.g. Project X">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Quantity</label>
|
||||
<input type="number" name="quantity" class="form-control" value="1">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Due Date</label>
|
||||
<input type="date" name="due_date" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Serial Number</label>
|
||||
<input type="text" name="serial_number" class="form-control" placeholder="SN-XXXX">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Component Modal -->
|
||||
<div class="modal fade" id="newCompModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<form method="POST" class="modal-content">
|
||||
<input type="hidden" name="job_id" value="<?= $currentJobId ?>">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Add Component</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Component Name</label>
|
||||
<input type="text" name="comp_name" class="form-control" required placeholder="e.g. Chassis, Motor Mount">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" name="add_component" class="btn btn-primary">Add Component</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Operation Modal -->
|
||||
<div class="modal fade" id="newOpModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<form method="POST" class="modal-content">
|
||||
<input type="hidden" name="job_id" value="<?= $currentJobId ?>">
|
||||
<input type="hidden" name="comp_id" id="modal_comp_id">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Add Operation</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Operation Name</label>
|
||||
<input type="text" name="op_name" class="form-control" required placeholder="e.g. Laser Cut, MIG Weld">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Process Type</label>
|
||||
<select name="process_type" class="form-select">
|
||||
<option value="cutting">Cutting</option>
|
||||
<option value="welding">Welding</option>
|
||||
<option value="bending">Bending</option>
|
||||
<option value="assembly">Assembly</option>
|
||||
<option value="inspection">Inspection</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Priority (higher first)</label>
|
||||
<input type="number" name="priority" class="form-control" value="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" name="add_op" class="btn btn-primary">Add Operation</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
function setCompId(id) {
|
||||
document.getElementById('modal_comp_id').value = id;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
5
logout.php
Normal file
5
logout.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
session_start();
|
||||
session_destroy();
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
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"}]
|
||||
256
users.php
Normal file
256
users.php
Normal file
@ -0,0 +1,256 @@
|
||||
<?php
|
||||
session_start();
|
||||
require 'db/config.php';
|
||||
$db = db();
|
||||
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Handle User Creation/Update
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_user'])) {
|
||||
$id = $_POST['user_id'] ?? null;
|
||||
$name = $_POST['name'];
|
||||
$role = $_POST['role'];
|
||||
$processes = json_encode($_POST['processes'] ?? []);
|
||||
$pin = $_POST['pin'] ?? '';
|
||||
|
||||
if ($id) {
|
||||
// Update
|
||||
$sql = "UPDATE users SET name = ?, role = ?, assigned_processes = ? WHERE id = ?";
|
||||
$params = [$name, $role, $processes, $id];
|
||||
$db->prepare($sql)->execute($params);
|
||||
|
||||
if ($role === 'admin' && !empty($pin)) {
|
||||
$pinHash = password_hash($pin, PASSWORD_BCRYPT);
|
||||
$db->prepare("UPDATE users SET pin_hash = ? WHERE id = ?")->execute([$pinHash, $id]);
|
||||
}
|
||||
} else {
|
||||
// Create
|
||||
$pinHash = ($role === 'admin' && !empty($pin)) ? password_hash($pin, PASSWORD_BCRYPT) : null;
|
||||
$db->prepare("INSERT INTO users (name, role, assigned_processes, pin_hash) VALUES (?, ?, ?, ?)")
|
||||
->execute([$name, $role, $processes, $pinHash]);
|
||||
}
|
||||
header("Location: users.php?success=1");
|
||||
exit;
|
||||
}
|
||||
|
||||
$users = $db->query("SELECT * FROM users ORDER BY role ASC, name ASC")->fetchAll();
|
||||
$processTypes = ['cutting', 'welding', 'bending', 'assembly', 'inspection'];
|
||||
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>M-TRACK | User Management</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--sidebar-width: 240px;
|
||||
--bg: #f8fafc;
|
||||
--primary: #1e293b;
|
||||
--accent: #3b82f6;
|
||||
--text: #334155;
|
||||
--border: #e2e8f0;
|
||||
}
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
z-index: 1000;
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
.main-content {
|
||||
margin-left: var(--sidebar-width);
|
||||
padding: 2.5rem;
|
||||
}
|
||||
.card {
|
||||
background: white;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.badge-process {
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.625rem;
|
||||
border-radius: 4px;
|
||||
margin-right: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="sidebar">
|
||||
<h2>M-TRACK</h2>
|
||||
<nav class="nav nav-pills flex-column">
|
||||
<a class="nav-link" href="dashboard.php"><i class="bi bi-grid-fill me-2"></i> Dashboard</a>
|
||||
<a class="nav-link" href="jobs.php"><i class="bi bi-briefcase me-2"></i> Jobs</a>
|
||||
<a class="nav-link" href="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">
|
||||
<a class="nav-link text-danger" href="logout.php"><i class="bi bi-box-arrow-right me-2"></i> Logout</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>User Management</h1>
|
||||
<button class="btn btn-primary" onclick="resetUserModal()" data-bs-toggle="modal" data-bs-target="#userModal">
|
||||
<i class="bi bi-person-plus-fill me-1"></i> New User
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0 align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Role</th>
|
||||
<th>Assigned Processes</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($users as $user): ?>
|
||||
<?php $procs = json_decode($user['assigned_processes'] ?? '[]', true); ?>
|
||||
<tr>
|
||||
<td><div class="fw-bold"><?= htmlspecialchars($user['name']) ?></div></td>
|
||||
<td><span class="badge bg-<?= $user['role'] === 'admin' ? 'dark' : 'light text-dark border' ?> text-uppercase" style="font-size: 0.7rem;"><?= $user['role'] ?></span></td>
|
||||
<td>
|
||||
<?php if ($user['role'] === 'worker'): ?>
|
||||
<?php foreach ($procs as $p): ?>
|
||||
<span class="badge-process"><?= strtoupper($p) ?></span>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($procs)): ?>
|
||||
<span class="text-muted small">None assigned</span>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<span class="text-muted small">—</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<button class="btn btn-sm btn-outline-primary" onclick='editUser(<?= json_encode($user) ?>)' data-bs-toggle="modal" data-bs-target="#userModal">Edit</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Modal -->
|
||||
<div class="modal fade" id="userModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<form method="POST" class="modal-content" id="userForm">
|
||||
<input type="hidden" name="user_id" id="user_id">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modalTitle">New User</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Full Name</label>
|
||||
<input type="text" name="name" id="user_name" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Role</label>
|
||||
<select name="role" id="user_role" class="form-select" onchange="toggleRoleFields()">
|
||||
<option value="worker">Worker</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="worker_fields">
|
||||
<label class="form-label d-block">Assigned Processes</label>
|
||||
<div class="row">
|
||||
<?php foreach ($processTypes as $pt): ?>
|
||||
<div class="col-6 mb-2">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input proc-checkbox" type="checkbox" name="processes[]" value="<?= $pt ?>" id="proc_<?= $pt ?>">
|
||||
<label class="form-check-label text-capitalize" for="proc_<?= $pt ?>">
|
||||
<?= $pt ?>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="admin_fields" style="display:none;">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">PIN (4-6 digits)</label>
|
||||
<input type="password" name="pin" id="user_pin" class="form-control" placeholder="Leave blank to keep current PIN if editing">
|
||||
<small class="text-muted">Admins must enter this PIN to log in.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" name="save_user" class="btn btn-primary">Save User</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
function toggleRoleFields() {
|
||||
const role = document.getElementById('user_role').value;
|
||||
document.getElementById('worker_fields').style.display = role === 'worker' ? 'block' : 'none';
|
||||
document.getElementById('admin_fields').style.display = role === 'admin' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function resetUserModal() {
|
||||
document.getElementById('userForm').reset();
|
||||
document.getElementById('user_id').value = '';
|
||||
document.getElementById('modalTitle').innerText = 'New User';
|
||||
toggleRoleFields();
|
||||
}
|
||||
|
||||
function editUser(user) {
|
||||
document.getElementById('user_id').value = user.id;
|
||||
document.getElementById('user_name').value = user.name;
|
||||
document.getElementById('user_role').value = user.role;
|
||||
document.getElementById('modalTitle').innerText = 'Edit User: ' + user.name;
|
||||
|
||||
// Reset checkboxes
|
||||
document.querySelectorAll('.proc-checkbox').forEach(cb => cb.checked = false);
|
||||
|
||||
if (user.assigned_processes) {
|
||||
const procs = JSON.parse(user.assigned_processes);
|
||||
procs.forEach(p => {
|
||||
const cb = document.getElementById('proc_' + p);
|
||||
if (cb) cb.checked = true;
|
||||
});
|
||||
}
|
||||
|
||||
toggleRoleFields();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user