38815-vm/api/import_bom.php
2026-02-28 22:33:39 +00:00

244 lines
8.9 KiB
PHP

<?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()]);
}