From 63674ea6bee2100ea28a6f2182371306f61ff5ae Mon Sep 17 00:00:00 2001 From: solocla Date: Thu, 1 Oct 2026 14:50:29 +0200 Subject: [PATCH] import fogli di lavoro --- composer.json | 2 +- composer.lock | 38 +- ...4082312_add_source_xlsx_to_work_sheets.php | 26 + public/userarea/attach_worksheet_file.php | 83 +++ public/userarea/download_worksheet_file.php | 51 ++ public/userarea/import_worksheet_xlsx.php | 505 ++++++++++++++++++ public/userarea/manage-worksheet.php | 249 +++++++++ 7 files changed, 937 insertions(+), 17 deletions(-) create mode 100644 db/migrations/20260924082312_add_source_xlsx_to_work_sheets.php create mode 100644 public/userarea/attach_worksheet_file.php create mode 100644 public/userarea/download_worksheet_file.php create mode 100644 public/userarea/import_worksheet_xlsx.php diff --git a/composer.json b/composer.json index cec9b8e9..aec952ea 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,7 @@ "laravel/tinker": "^2.7", "laravel/ui": "^4.0", "phpmailer/phpmailer": "^6.9", - "phpoffice/phpspreadsheet": "^4.1", + "phpoffice/phpspreadsheet": "^5.10", "proengsoft/laravel-jsvalidation": "^4.0.0", "robmorgan/phinx": "^0.16.11", "socialiteproviders/microsoft": "^4.7", diff --git a/composer.lock b/composer.lock index b37a2d55..9eb91e94 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "076e7721d08cfea8b06ce75dd8c6c576", + "content-hash": "7dbd2f50581b518d3ed8feecb1117520", "packages": [ { "name": "akaunting/laravel-setting", @@ -4247,16 +4247,16 @@ }, { "name": "phpoffice/phpspreadsheet", - "version": "4.1.0", + "version": "5.10.0", "source": { "type": "git", "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", - "reference": "6ff18c3a8df3a945492f75ce455d77f7ad55dd5c" + "reference": "eb18727acf6b1f4cc67145a52ab04f9fd4c28d53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/6ff18c3a8df3a945492f75ce455d77f7ad55dd5c", - "reference": "6ff18c3a8df3a945492f75ce455d77f7ad55dd5c", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/eb18727acf6b1f4cc67145a52ab04f9fd4c28d53", + "reference": "eb18727acf6b1f4cc67145a52ab04f9fd4c28d53", "shasum": "" }, "require": { @@ -4264,6 +4264,7 @@ "ext-ctype": "*", "ext-dom": "*", "ext-fileinfo": "*", + "ext-filter": "*", "ext-gd": "*", "ext-iconv": "*", "ext-libxml": "*", @@ -4277,27 +4278,29 @@ "maennchen/zipstream-php": "^2.1 || ^3.0", "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", - "php": "^8.1", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0", + "php": "^8.2", "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "dev-main", "dompdf/dompdf": "^2.0 || ^3.0", + "ext-intl": "*", + "ext-openssl": "*", "friendsofphp/php-cs-fixer": "^3.2", - "mitoteam/jpgraph": "^10.3", + "mitoteam/jpgraph": "^10.5", "mpdf/mpdf": "^8.1.1", "phpcompatibility/php-compatibility": "^9.3", - "phpstan/phpstan": "^1.1", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^10.5", + "phpstan/phpstan": "^1.1 || ^2.0", + "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0", + "phpstan/phpstan-phpunit": "^1.0 || ^2.0", + "phpunit/phpunit": "^10.5 || ^11.0", "squizlabs/php_codesniffer": "^3.7", - "tecnickcom/tcpdf": "^6.5" + "tecnickcom/tcpdf": ">=6.8.0 <7.0.0" }, "suggest": { "dompdf/dompdf": "Option for rendering PDF with PDF Writer", - "ext-intl": "PHP Internationalization Functions", + "ext-intl": "PHP Internationalization Functions, required for NumberFormat Wizard and StringHelper::setLocale()", + "ext-openssl": "Handline Agile-encrypted Xlsx spreadsheets", "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", "mpdf/mpdf": "Option for rendering PDF with PDF Writer", "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" @@ -4330,6 +4333,9 @@ }, { "name": "Adrien Crivelli" + }, + { + "name": "Owen Leibman" } ], "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", @@ -4346,9 +4352,9 @@ ], "support": { "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", - "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/4.1.0" + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.10.0" }, - "time": "2025-03-02T06:52:24+00:00" + "time": "2026-09-17T03:54:50+00:00" }, { "name": "phpoption/phpoption", diff --git a/db/migrations/20260924082312_add_source_xlsx_to_work_sheets.php b/db/migrations/20260924082312_add_source_xlsx_to_work_sheets.php new file mode 100644 index 00000000..2e021695 --- /dev/null +++ b/db/migrations/20260924082312_add_source_xlsx_to_work_sheets.php @@ -0,0 +1,26 @@ +table('work_sheets') + ->addColumn('source_xlsx_name', 'string', [ + 'limit' => 255, + 'null' => true, + 'default' => null, + 'after' => 'notes', + ]) + ->addColumn('source_xlsx_path', 'string', [ + 'limit' => 255, + 'null' => true, + 'default' => null, + 'after' => 'source_xlsx_name', + ]) + ->update(); + } +} diff --git a/public/userarea/attach_worksheet_file.php b/public/userarea/attach_worksheet_file.php new file mode 100644 index 00000000..a3c3a9de --- /dev/null +++ b/public/userarea/attach_worksheet_file.php @@ -0,0 +1,83 @@ + false, 'message' => 'Metodo non valido']); + } + + $worksheet_id = isset($_POST['worksheet_id']) && is_numeric($_POST['worksheet_id']) + ? (int)$_POST['worksheet_id'] : 0; + if ($worksheet_id <= 0) { + jexit(['success' => false, 'message' => 'worksheet_id mancante o non valido']); + } + + if (!isset($_FILES['xlsx_file']) || $_FILES['xlsx_file']['error'] !== UPLOAD_ERR_OK) { + jexit(['success' => false, 'message' => 'File non ricevuto']); + } + $file = $_FILES['xlsx_file']; + + if ($file['size'] > 5 * 1024 * 1024) { + jexit(['success' => false, 'message' => 'File troppo grande (max 5 MB)']); + } + $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + if ($ext !== 'xlsx') { + jexit(['success' => false, 'message' => 'Sono ammessi solo file .xlsx']); + } + + $db = DBHandlerSelect::getInstance(); + $pdo = $db->getConnection(); + + // il foglio deve esistere + $chk = $pdo->prepare("SELECT id FROM work_sheets WHERE id = ?"); + $chk->execute([$worksheet_id]); + if (!$chk->fetchColumn()) { + jexit(['success' => false, 'message' => 'Foglio di lavoro inesistente']); + } + + // cartella di destinazione (accanto allo script) + $dir = __DIR__ . '/uploads/worksheets'; + if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) { + jexit(['success' => false, 'message' => 'Impossibile creare la cartella uploads/worksheets']); + } + + $origName = $file['name']; + $safeStored = 'ws_' . $worksheet_id . '_' . date('Ymd_His') . '.xlsx'; + $destAbs = $dir . '/' . $safeStored; + $destRel = 'uploads/worksheets/' . $safeStored; // relativo alla cartella dello script + + if (!move_uploaded_file($file['tmp_name'], $destAbs)) { + jexit(['success' => false, 'message' => 'Salvataggio del file fallito']); + } + + $upd = $pdo->prepare("UPDATE work_sheets SET source_xlsx_name = ?, source_xlsx_path = ? WHERE id = ?"); + $upd->execute([$origName, $destRel, $worksheet_id]); + + jexit(['success' => true, 'name' => $origName, 'path' => $destRel]); +} catch (Exception $e) { + jexit(['success' => false, 'message' => $e->getMessage()]); +} diff --git a/public/userarea/download_worksheet_file.php b/public/userarea/download_worksheet_file.php new file mode 100644 index 00000000..58e0310f --- /dev/null +++ b/public/userarea/download_worksheet_file.php @@ -0,0 +1,51 @@ +getConnection(); + +$stmt = $pdo->prepare("SELECT source_xlsx_name, source_xlsx_path FROM work_sheets WHERE id = ?"); +$stmt->execute([$id]); +$row = $stmt->fetch(PDO::FETCH_ASSOC); + +if (!$row || empty($row['source_xlsx_path'])) { + http_response_code(404); + exit('Nessun file allegato a questo foglio'); +} + +// Risoluzione sicura: il file deve stare dentro uploads/worksheets +$abs = __DIR__ . '/' . $row['source_xlsx_path']; +$real = realpath($abs); +$baseDir = realpath(__DIR__ . '/uploads/worksheets'); + +if ($real === false || $baseDir === false || strpos($real, $baseDir) !== 0 || !is_file($real)) { + http_response_code(404); + exit('File non trovato'); +} + +$downloadName = $row['source_xlsx_name'] !== '' ? $row['source_xlsx_name'] : basename($real); +$asciiName = preg_replace('/[^A-Za-z0-9._-]/', '_', $downloadName); + +header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); +header("Content-Disposition: attachment; filename=\"{$asciiName}\"; filename*=UTF-8''" . rawurlencode($downloadName)); +header('Content-Length: ' . filesize($real)); +header('X-Content-Type-Options: nosniff'); +header('Cache-Control: private, max-age=0, must-revalidate'); + +readfile($real); +exit; diff --git a/public/userarea/import_worksheet_xlsx.php b/public/userarea/import_worksheet_xlsx.php new file mode 100644 index 00000000..03b8e4ab --- /dev/null +++ b/public/userarea/import_worksheet_xlsx.php @@ -0,0 +1,505 @@ + false, 'message' => $msg], JSON_UNESCAPED_UNICODE); + exit; +} + +/* ========================================================================= + * 1) READER XLSX (senza dipendenze) + * ========================================================================= */ + +function excel_serial_to_date($serial) +{ + $days = (int)floor((float)$serial); + $base = new DateTime('1899-12-30'); + $base->modify("+{$days} day"); + return $base->format('Y-m-d'); +} + +function read_xlsx_grid($path) +{ + try { + $reader = IOFactory::createReader('Xlsx'); + // niente setReadDataOnly(true): serve il formato per riconoscere le date + $spreadsheet = $reader->load($path); + } catch (\Throwable $e) { + throw new Exception('Impossibile leggere il file .xlsx: ' . $e->getMessage()); + } + + $sheet = $spreadsheet->getSheet(0); // primo foglio + $grid = []; + + foreach ($sheet->getRowIterator() as $row) { + $rowN = $row->getRowIndex(); + $it = $row->getCellIterator(); + $it->setIterateOnlyExistingCells(true); + foreach ($it as $c) { + $colL = $c->getColumn(); // lettera colonna, es. "D" + + // IMPORTANTE: nel modulo ZIBOGOMMA quasi tutte le celle sono formule + // che puntano a un file esterno ('[1] Base2'!...). Il valore reale sta + // nella CACHE della cella. Perciò: + // - cella formula -> getOldCalculatedValue() (valore in cache, NO ricalcolo) + // - cella normale -> getValue() + $val = $c->isFormula() ? $c->getOldCalculatedValue() : $c->getValue(); + + if ($val === null || $val === '') continue; + + if (XlsDate::isDateTime($c) && is_numeric($val)) { + $val = XlsDate::excelToDateTimeObject((float)$val)->format('Y-m-d'); + } elseif (is_numeric($val)) { + $val = $val + 0; + } + $grid[$rowN][$colL] = $val; + } + } + return $grid; +} +/* ========================================================================= + * 2) ESTRATTORE label-anchored (ancorato alle etichette in colonna A) + * ========================================================================= */ + +function norm($s) +{ + $s = (string)$s; + $s = str_replace( + ['à', 'è', 'é', 'ì', 'ò', 'ù', 'À', 'È', 'É', 'Ì', 'Ò', 'Ù'], + ['a', 'e', 'e', 'i', 'o', 'u', 'a', 'e', 'e', 'i', 'o', 'u'], + $s + ); + $s = strtolower($s); + $s = preg_replace('/[^a-z0-9\/ ]+/', ' ', $s); + $s = preg_replace('/\s+/', ' ', $s); + return trim($s); +} + +function cell($grid, $row, $col) +{ + return isset($grid[$row][$col]) ? trim((string)$grid[$row][$col]) : ''; +} + +function row_by_label($grid, $label, $col = 'A') +{ + $target = norm($label); + foreach ($grid as $rn => $cols) { + $a = norm($cols[$col] ?? ''); + if ($a === $target || ($target !== '' && strpos($a, $target) === 0)) return $rn; + } + return null; +} + +function val_at_label($grid, $label, $valcol = 'D') +{ + $r = row_by_label($grid, $label); + return $r ? cell($grid, $r, $valcol) : ''; +} + +function is_known_label($a) +{ + $known = [ + 'foglio di lavoro', + 'data', + 'cliente', + 'tipo profilo', + 'peso a metro calcolato g/m', + 'nome mescola in entrata', + 'densita richiesta g/cm3', + 'durezza richiesta shore a', + 'marchiatura', + 'lubrificazione', + 'impostazione misure per il controllo della produzione', + 'impostazione lunghezza spezzoni', + 'frequenza di controllo', + 'frequenza di controllo disegno', + 'frequenza di controllo in dima', + 'modalita di controllo in dima', + 'confezione richiesta', + 'metri/confezione', + 'tipo scatola', + 'n confezioni o pezzi per scatola', + 'metri/scatola', + 'tipo di bancale', + 'n confezioni o scatole per bancale', + 'velocita di produzione' + ]; + foreach ($known as $k) if (strpos($a, $k) === 0) return true; + return false; +} + +function get_freq_cut($grid) +{ + foreach ($grid as $rn => $cols) { + if (norm($cols['A'] ?? '') === 'frequenza di controllo') return cell($grid, $rn, 'D'); + } + return ''; +} + +function num_or_null($v) +{ + if ($v === '' || $v === null) return null; + return is_numeric($v) ? $v + 0 : null; +} + +function dotdec_to_comma($v) +{ + // 1.2 -> 1,2 ma lascia intatto "+/-0,02" + return preg_replace('/(\d)\.(\d)/', '$1,$2', (string)$v); +} + +function extract_fields($grid) +{ + $out = []; + + $wd = val_at_label($grid, 'data', 'D'); + if ($wd !== '' && is_numeric($wd)) $wd = excel_serial_to_date($wd); + $out['worksheet_date'] = $wd; + + $out['customer_name'] = val_at_label($grid, 'cliente', 'D'); + $out['profile_type_code'] = val_at_label($grid, 'tipo profilo', 'D'); + $out['marking'] = strtoupper(val_at_label($grid, 'marchiatura', 'D')); + + // prod_control: riga "impostazione misure..." + continuazione senza label + "impostazione lunghezza spezzoni" + $lines = []; + $r = row_by_label($grid, 'impostazione misure per il controllo'); + if ($r) { + for ($i = $r; $i <= $r + 1; $i++) { + $d = cell($grid, $i, 'D'); + $e = cell($grid, $i, 'E'); + if ($d !== '' && ($i === $r || norm(cell($grid, $i, 'A')) === '')) { + $lines[] = trim($d . ' ' . $e); + } + } + } + $rl = row_by_label($grid, 'impostazione lunghezza spezzoni'); + if ($rl) { + $d = cell($grid, $rl, 'D'); + $e = cell($grid, $rl, 'E'); + if ($d !== '') $lines[] = trim($d . ' ' . $e); + } + $out['prod_control_measure_settings'] = implode("\n", $lines); + + // lookup da evidenziare (RAW, value scelto dall'utente) + $out['raw_freq_cut'] = get_freq_cut($grid); + $out['raw_freq_drawing'] = val_at_label($grid, 'frequenza di controllo disegno', 'D'); + $out['raw_freq_jig'] = val_at_label($grid, 'frequenza di controllo in dima', 'D'); + $out['raw_mode_jig'] = val_at_label($grid, 'modalita di controllo in dima', 'D'); + + // mescola + $out['mescola_raw'] = val_at_label($grid, 'nome mescola in entrata', 'D'); + $out['mix_weight_g_m'] = num_or_null(val_at_label($grid, 'peso a metro calcolato', 'D')); + $rd = row_by_label($grid, 'densita richiesta'); + $out['required_density'] = $rd ? dotdec_to_comma(trim(cell($grid, $rd, 'D') . ' ' . cell($grid, $rd, 'E'))) : ''; + $rh = row_by_label($grid, 'durezza richiesta'); + $out['required_hardness_shore_a'] = $rh ? dotdec_to_comma(trim(cell($grid, $rh, 'D') . ' ' . cell($grid, $rh, 'E'))) : ''; + $out['lubrication_type'] = strtoupper(val_at_label($grid, 'lubrificazione', 'D')); + + // packaging (3 celle contenitore -> 3 righe) + $out['pkg_confezione_raw'] = val_at_label($grid, 'confezione richiesta', 'D'); + $out['pkg_scatola_raw'] = val_at_label($grid, 'tipo scatola', 'D'); + $out['pkg_bancale_raw'] = val_at_label($grid, 'tipo di bancale', 'D'); + + $out['packages_or_pieces_per_box'] = num_or_null(val_at_label($grid, 'n confezioni o pezzi per scatola', 'D')); + $out['boxes_or_packages_per_pallet'] = num_or_null(val_at_label($grid, 'n confezioni o scatole per bancale', 'D')); + + // velocità + $out['speed_expected_kg_h'] = null; + $out['speed_expected_m_h'] = null; + $rv = row_by_label($grid, 'velocita di produzione'); + if ($rv) { + $out['speed_expected_kg_h'] = num_or_null(cell($grid, $rv, 'C')); + for ($i = $rv; $i <= $rv + 2; $i++) { + if (norm(cell($grid, $i, 'B')) === 'm/h') { + $out['speed_expected_m_h'] = num_or_null(cell($grid, $i, 'C')); + break; + } + } + } + + // note: annotazioni libere colonna E + righe istruzione in fondo + $noteBits = []; + foreach (['confezione richiesta', 'metri/confezione', 'metri/scatola'] as $lbl) { + $rr = row_by_label($grid, $lbl); + if ($rr && cell($grid, $rr, 'E') !== '') $noteBits[] = cell($grid, $rr, 'E'); + } + foreach ($grid as $rn => $cols) { + $a = norm($cols['A'] ?? ''); + if ($a !== '' && cell($grid, $rn, 'D') === '' && cell($grid, $rn, 'C') === '' && !is_known_label($a)) { + if (strlen($a) > 12) $noteBits[] = trim((string)$cols['A']); + } + } + $out['notes'] = implode("\n", array_values(array_unique(array_filter($noteBits)))); + + return $out; +} + +/* ========================================================================= + * 3) MATCHER su DB + * ========================================================================= */ + +function norm_mesc($s) +{ + $s = strtolower((string)$s); + $s = preg_replace('/\s+/', ' ', $s); + return trim($s); +} + +function match_mescola($raw, array $mescole) +{ + $r = norm_mesc($raw); + if ($r === '') return [null, 'none', null]; + foreach ($mescole as $m) if (norm_mesc($m['nome']) === $r) return [(int)$m['id'], 'exact', $m['nome']]; + foreach ($mescole as $m) { + foreach (explode('/', $m['nome']) as $part) { + if (norm_mesc($part) === $r) return [(int)$m['id'], 'exact-part', $m['nome']]; + } + } + foreach ($mescole as $m) if (norm_mesc($m['nomeuscita'] ?? '') === $r) return [(int)$m['id'], 'exact-uscita', $m['nome']]; + foreach ($mescole as $m) if ($r !== '' && strpos(norm_mesc($m['nome']), $r) !== false) return [(int)$m['id'], 'contains', $m['nome']]; + return [null, 'none', null]; +} + +function dim_tokens($s) +{ + $s = strtolower((string)$s); + $s = str_replace(',', '.', $s); + preg_match_all('/\d+(?:\.\d+)?/', $s, $m); + $nums = $m[0]; + if (count($nums) < 2) return []; + $forms = []; + $forms[] = implode('x', $nums); + $mm = array_map(function ($n) { + $f = (float)$n; + if ($f < 10 && strpos($n, '.') !== false) return (string)round($f * 1000); + return (string)($f == (int)$f ? (int)$f : $f); + }, $nums); + $forms[] = implode('x', $mm); + return array_values(array_unique($forms)); +} + +function match_packaging($raw, array $packaging, $preferCat = null) +{ + $cands = dim_tokens($raw); + if (!$cands) return [null, 'none', null, null]; + $best = null; + foreach ($packaging as $p) { + $itemForms = array_merge(dim_tokens($p['item_name']), dim_tokens($p['item_code'])); + foreach ($cands as $c) { + if (in_array($c, $itemForms, true)) { + if ($preferCat && $p['category'] === $preferCat) { + return [(int)$p['id'], 'dimension', $p['item_name'], $p['category']]; + } + if ($best === null) $best = $p; + } + } + } + if ($best) return [(int)$best['id'], 'dimension', $best['item_name'], $best['category']]; + return [null, 'none', null, null]; +} + + +/* ---- match generico su dropdown (ws_lookup_options) ---- */ +function lk_norm($s) +{ + $s = (string)$s; + $s = str_replace( + ['à', 'è', 'é', 'ì', 'ò', 'ù', 'À', 'È', 'É', 'Ì', 'Ò', 'Ù'], + ['a', 'e', 'e', 'i', 'o', 'u', 'a', 'e', 'e', 'i', 'o', 'u'], + $s + ); + $s = strtolower($s); + $s = preg_replace('/[^a-z0-9 ]+/', ' ', $s); + $s = preg_replace('/\s+/', ' ', $s); + return trim($s); +} +function lk_toks($s) +{ + $t = array_filter(explode(' ', lk_norm($s)), fn($x) => $x !== ''); + return array_values(array_unique($t)); +} +/** abbina il testo Excel a una voce del dropdown. Ritorna [value|null, label|null, confidence] */ +function match_lookup($raw, array $options) +{ + $r = lk_norm($raw); + if ($r === '') return [null, null, 'none']; + // tier 1: uguale a value o label + foreach ($options as $o) { + if (lk_norm($o['value']) === $r || lk_norm($o['label']) === $r) { + return [$o['value'], $o['label'], 'exact']; + } + } + // tier 2: sovrapposizione token >= 0.8 (set più piccolo di almeno 2 token) + $rt = lk_toks($raw); + $best = null; + $bestScore = 0.0; + foreach ($options as $o) { + $ot = lk_toks($o['label']); + if (count($ot) < 2 || count($rt) < 2) continue; + $score = count(array_intersect($rt, $ot)) / min(count($rt), count($ot)); + if ($score > $bestScore) { + $bestScore = $score; + $best = $o; + } + } + if ($best && $bestScore >= 0.8) return [$best['value'], $best['label'], 'fuzzy']; + return [null, null, 'none']; +} +/** costruisce la voce JSON per un lookup di testata */ +function build_lookup($raw, array $options) +{ + [$v, $lab, $conf] = match_lookup($raw, $options); + return [ + 'raw' => $raw, + 'value' => $v, // null = nessun suggerimento + 'label' => $lab, + 'confidence' => $conf, + 'needs_review' => ($raw !== ''), // sempre da verificare se c'era un valore nel file + ]; +} + +/* ========================================================================= + * 4) MAIN + * ========================================================================= */ + +try { + if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_FILES['xlsx_file'])) { + fail('Nessun file ricevuto.'); + } + $file = $_FILES['xlsx_file']; + if ($file['error'] !== UPLOAD_ERR_OK) { + fail('Errore nel caricamento del file (codice ' . $file['error'] . ').'); + } + if ($file['size'] > 5 * 1024 * 1024) { + fail('File troppo grande (max 5 MB).'); + } + $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + if ($ext !== 'xlsx') { + fail('Formato non supportato: carica un file .xlsx.'); + } + + $db = DBHandlerSelect::getInstance(); + $pdo = $db->getConnection(); + + $grid = read_xlsx_grid($file['tmp_name']); + $fields = extract_fields($grid); + + $warnings = []; + + // enum di sicurezza + if ($fields['marking'] !== '' && !in_array($fields['marking'], ['S', 'N'], true)) { + $warnings[] = "Marchiatura '{$fields['marking']}' non riconosciuta (attese S/N)."; + $fields['marking'] = ''; + } + if ($fields['lubrication_type'] !== '' && !in_array($fields['lubrication_type'], ['S', 'G', 'N'], true)) { + $warnings[] = "Lubrificazione '{$fields['lubrication_type']}' non riconosciuta (attese S/G/N)."; + $fields['lubrication_type'] = ''; + } + + // carica tabelle di riferimento + $mescole = $pdo->query("SELECT id, nome, nomeuscita FROM mescole WHERE is_active = 1")->fetchAll(PDO::FETCH_ASSOC); + $packaging = $pdo->query("SELECT id, category, item_name, item_code FROM packaging_items WHERE is_active = 1")->fetchAll(PDO::FETCH_ASSOC); + + // opzioni dei dropdown (ws_lookup_options) raggruppate per categoria + $lookupOpts = []; + foreach ($pdo->query("SELECT category, value, label FROM ws_lookup_options WHERE is_active = 1 ORDER BY sort_order ASC")->fetchAll(PDO::FETCH_ASSOC) as $o) { + $lookupOpts[$o['category']][] = $o; + } + + // --- mescola (1 riga) --- + [$mid, $mconf, $mname] = match_mescola($fields['mescola_raw'], $mescole); + $mescoleOut = []; + if ($fields['mescola_raw'] !== '') { + $mescoleOut[] = [ + 'raw' => $fields['mescola_raw'], + 'idmescola' => $mid, + 'matched_name' => $mname, + 'confidence' => $mconf, + 'needs_review' => ($mid === null || $mconf === 'contains'), + 'mix_weight_g_m' => $fields['mix_weight_g_m'], + 'required_density' => $fields['required_density'], + 'required_hardness_shore_a' => $fields['required_hardness_shore_a'], + 'lubrication_type' => $fields['lubrication_type'], + ]; + } + + // --- packaging (3 celle contenitore -> righe) --- + $packagesOut = []; + $pkgSpecs = [ + ['source' => 'Confezione richiesta', 'raw' => $fields['pkg_confezione_raw'], 'cat' => null], + ['source' => 'Tipo scatola', 'raw' => $fields['pkg_scatola_raw'], 'cat' => 'BOX'], + ['source' => 'Tipo di bancale', 'raw' => $fields['pkg_bancale_raw'], 'cat' => 'PALLET'], + ]; + foreach ($pkgSpecs as $spec) { + if ($spec['raw'] === '') continue; + [$pid, $pconf, $pname, $pcat] = match_packaging($spec['raw'], $packaging, $spec['cat']); + $packagesOut[] = [ + 'source' => $spec['source'], + 'raw' => $spec['raw'], + 'package_code' => $pid, + 'matched_name' => $pname, + 'category' => $pcat, + 'confidence' => $pconf, + 'needs_review' => ($pid === null), + ]; + } + + // --- risposta --- + echo json_encode([ + 'success' => true, + 'header' => [ + 'worksheet_date' => $fields['worksheet_date'], + 'customer_name' => $fields['customer_name'], + 'profile_type_code' => $fields['profile_type_code'], + 'marking' => $fields['marking'], + 'prod_control_measure_settings' => $fields['prod_control_measure_settings'], + 'packages_or_pieces_per_box' => $fields['packages_or_pieces_per_box'], + 'boxes_or_packages_per_pallet' => $fields['boxes_or_packages_per_pallet'], + 'speed_expected_kg_h' => $fields['speed_expected_kg_h'], + 'speed_expected_m_h' => $fields['speed_expected_m_h'], + 'notes' => $fields['notes'], + ], + // lookup da EVIDENZIARE: value = null, l'utente sceglie dal menu + 'header_lookups' => [ + 'control_frequency_cut' => build_lookup($fields['raw_freq_cut'], $lookupOpts['control_frequency_cut'] ?? []), + 'control_frequency_drawing' => build_lookup($fields['raw_freq_drawing'], $lookupOpts['control_frequency_drawing'] ?? []), + 'control_frequency_jig' => build_lookup($fields['raw_freq_jig'], $lookupOpts['control_frequency_jig'] ?? []), + 'control_mode_jig' => build_lookup($fields['raw_mode_jig'], $lookupOpts['control_mode_jig'] ?? []), + ], + 'mescole' => $mescoleOut, + 'packages' => $packagesOut, + 'warnings' => $warnings, + ], JSON_UNESCAPED_UNICODE); + exit; +} catch (Exception $e) { + fail($e->getMessage()); +} diff --git a/public/userarea/manage-worksheet.php b/public/userarea/manage-worksheet.php index e115f032..a5528182 100644 --- a/public/userarea/manage-worksheet.php +++ b/public/userarea/manage-worksheet.php @@ -963,6 +963,28 @@ $isEdit = ($worksheet_id > 0); padding: 12px; } } + + .import-review { + background: #fff8db !important; + border-color: #f59e0b !important; + } + + .import-hint { + font-size: 0.78rem; + color: #92400e; + margin-top: 4px; + font-weight: 700; + } + + .import-banner { + background: #ecfeff; + border: 1px solid #a5f3fc; + color: #155e75; + border-radius: 14px; + padding: 14px 16px; + margin-bottom: 16px; + font-size: 0.9rem; + } @@ -983,6 +1005,13 @@ $isEdit = ($worksheet_id > 0);
+ + + + + 📎 Excel di origine + +
@@ -1594,6 +1623,12 @@ $isEdit = ($worksheet_id > 0); window.history.replaceState({}, '', newUrl); } + // Import da Excel in corso: salva righe e ricarica + if (window.importStaged) { + window.finalizeImportIfAny(data.id); + return; + } + Swal.fire({ icon: 'success', title: 'Testata salvata', @@ -1853,6 +1888,220 @@ $isEdit = ($worksheet_id > 0); }); }); + \ No newline at end of file