import fogli di lavoro

This commit is contained in:
2026-10-01 14:50:29 +02:00
parent 328772f8da
commit 63674ea6be
7 changed files with 937 additions and 17 deletions
+1 -1
View File
@@ -42,7 +42,7 @@
"laravel/tinker": "^2.7", "laravel/tinker": "^2.7",
"laravel/ui": "^4.0", "laravel/ui": "^4.0",
"phpmailer/phpmailer": "^6.9", "phpmailer/phpmailer": "^6.9",
"phpoffice/phpspreadsheet": "^4.1", "phpoffice/phpspreadsheet": "^5.10",
"proengsoft/laravel-jsvalidation": "^4.0.0", "proengsoft/laravel-jsvalidation": "^4.0.0",
"robmorgan/phinx": "^0.16.11", "robmorgan/phinx": "^0.16.11",
"socialiteproviders/microsoft": "^4.7", "socialiteproviders/microsoft": "^4.7",
Generated
+22 -16
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "076e7721d08cfea8b06ce75dd8c6c576", "content-hash": "7dbd2f50581b518d3ed8feecb1117520",
"packages": [ "packages": [
{ {
"name": "akaunting/laravel-setting", "name": "akaunting/laravel-setting",
@@ -4247,16 +4247,16 @@
}, },
{ {
"name": "phpoffice/phpspreadsheet", "name": "phpoffice/phpspreadsheet",
"version": "4.1.0", "version": "5.10.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git", "url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "6ff18c3a8df3a945492f75ce455d77f7ad55dd5c" "reference": "eb18727acf6b1f4cc67145a52ab04f9fd4c28d53"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/6ff18c3a8df3a945492f75ce455d77f7ad55dd5c", "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/eb18727acf6b1f4cc67145a52ab04f9fd4c28d53",
"reference": "6ff18c3a8df3a945492f75ce455d77f7ad55dd5c", "reference": "eb18727acf6b1f4cc67145a52ab04f9fd4c28d53",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -4264,6 +4264,7 @@
"ext-ctype": "*", "ext-ctype": "*",
"ext-dom": "*", "ext-dom": "*",
"ext-fileinfo": "*", "ext-fileinfo": "*",
"ext-filter": "*",
"ext-gd": "*", "ext-gd": "*",
"ext-iconv": "*", "ext-iconv": "*",
"ext-libxml": "*", "ext-libxml": "*",
@@ -4277,27 +4278,29 @@
"maennchen/zipstream-php": "^2.1 || ^3.0", "maennchen/zipstream-php": "^2.1 || ^3.0",
"markbaker/complex": "^3.0", "markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0", "markbaker/matrix": "^3.0",
"php": "^8.1", "php": "^8.2",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0" "psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
}, },
"require-dev": { "require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-main", "dealerdirect/phpcodesniffer-composer-installer": "dev-main",
"dompdf/dompdf": "^2.0 || ^3.0", "dompdf/dompdf": "^2.0 || ^3.0",
"ext-intl": "*",
"ext-openssl": "*",
"friendsofphp/php-cs-fixer": "^3.2", "friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.3", "mitoteam/jpgraph": "^10.5",
"mpdf/mpdf": "^8.1.1", "mpdf/mpdf": "^8.1.1",
"phpcompatibility/php-compatibility": "^9.3", "phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^1.1", "phpstan/phpstan": "^1.1 || ^2.0",
"phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0",
"phpunit/phpunit": "^10.5", "phpstan/phpstan-phpunit": "^1.0 || ^2.0",
"phpunit/phpunit": "^10.5 || ^11.0",
"squizlabs/php_codesniffer": "^3.7", "squizlabs/php_codesniffer": "^3.7",
"tecnickcom/tcpdf": "^6.5" "tecnickcom/tcpdf": ">=6.8.0 <7.0.0"
}, },
"suggest": { "suggest": {
"dompdf/dompdf": "Option for rendering PDF with PDF Writer", "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", "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
"mpdf/mpdf": "Option for rendering PDF with PDF Writer", "mpdf/mpdf": "Option for rendering PDF with PDF Writer",
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
@@ -4330,6 +4333,9 @@
}, },
{ {
"name": "Adrien Crivelli" "name": "Adrien Crivelli"
},
{
"name": "Owen Leibman"
} }
], ],
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
@@ -4346,9 +4352,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", "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", "name": "phpoption/phpoption",
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddSourceXlsxToWorkSheets extends AbstractMigration
{
public function change(): void
{
$this->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();
}
}
+83
View File
@@ -0,0 +1,83 @@
<?php
/**
* attach_worksheet_file.php
* -------------------------------------------------------------------------
* Riceve worksheet_id + file .xlsx (campo "xlsx_file"), salva il file in
* uploads/worksheets/ e lo collega al foglio (colonne source_xlsx_*).
* Chiamato dal flusso di import DOPO il salvataggio della testata.
* -------------------------------------------------------------------------
*/
ini_set('display_errors', 0);
error_reporting(E_ALL);
while (ob_get_level()) {
ob_end_clean();
}
header('Content-Type: application/json; charset=utf-8');
include('include/headscript.php');
function jexit($a)
{
echo json_encode($a, JSON_UNESCAPED_UNICODE);
exit;
}
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jexit(['success' => 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()]);
}
@@ -0,0 +1,51 @@
<?php
/**
* download_worksheet_file.php?id=WORKSHEET_ID
* -------------------------------------------------------------------------
* Restituisce il file Excel di origine allegato a un foglio di lavoro,
* con il nome file originale. Protetto contro path-traversal.
* -------------------------------------------------------------------------
*/
include('include/headscript.php');
$id = isset($_GET['id']) && is_numeric($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id <= 0) {
http_response_code(400);
exit('ID non valido');
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->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;
+505
View File
@@ -0,0 +1,505 @@
<?php
/**
* import_worksheet_xlsx.php
* -------------------------------------------------------------------------
* Riceve un file .xlsx (modulo ZIBOGOMMA "FOGLIO DI LAVORO") via POST
* multipart (campo file: "xlsx_file") e restituisce una PROPOSTA in JSON
* pronta per essere revisionata dall'utente in manage-worksheet.php.
*
* NON scrive nulla sul database: fa solo lettura (mescole, packaging_items)
* per proporre gli abbinamenti. Il salvataggio resta agli endpoint esistenti.
*
* Dipendenze: solo estensioni PHP standard (zip, xml/simplexml). Niente Composer.
* -------------------------------------------------------------------------
*/
ini_set('display_errors', 0);
error_reporting(E_ALL);
while (ob_get_level()) {
ob_end_clean();
}
header('Content-Type: application/json; charset=utf-8');
// Connessione DB (stesso pattern di manage-worksheet.php)
include('include/headscript.php');
require __DIR__ . '/../../vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Shared\Date as XlsDate;
function fail($msg)
{
echo json_encode(['success' => 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());
}
+249
View File
@@ -963,6 +963,28 @@ $isEdit = ($worksheet_id > 0);
padding: 12px; 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;
}
</style> </style>
</head> </head>
@@ -983,6 +1005,13 @@ $isEdit = ($worksheet_id > 0);
</div> </div>
<div class="top-actions"> <div class="top-actions">
<input type="file" id="xlsxImportInput" accept=".xlsx" style="display:none">
<button type="button" class="btn btn-history" id="btnImportXlsx">📥 Importa da Excel</button>
<?php if ($worksheet && !empty($worksheet['source_xlsx_path'])): ?>
<a class="btn btn-history" href="download_worksheet_file.php?id=<?= (int)$worksheet_id ?>" target="_blank">
📎 Excel di origine
</a>
<?php endif; ?>
<button class="btn btn-history" onclick="location.href='worksheets.php'">↩️ Storico fogli</button> <button class="btn btn-history" onclick="location.href='worksheets.php'">↩️ Storico fogli</button>
</div> </div>
</div> </div>
@@ -1594,6 +1623,12 @@ $isEdit = ($worksheet_id > 0);
window.history.replaceState({}, '', newUrl); window.history.replaceState({}, '', newUrl);
} }
// Import da Excel in corso: salva righe e ricarica
if (window.importStaged) {
window.finalizeImportIfAny(data.id);
return;
}
Swal.fire({ Swal.fire({
icon: 'success', icon: 'success',
title: 'Testata salvata', title: 'Testata salvata',
@@ -1853,6 +1888,220 @@ $isEdit = ($worksheet_id > 0);
}); });
}); });
</script> </script>
<script>
$(function() {
window.importStaged = null;
function postForm(obj) {
return fetch('', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams(obj)
}).then(r => r.json()).catch(() => ({
success: false
}));
}
function setVal(name, val) {
if (val === null || val === undefined) return;
const $el = $('[name="' + name + '"]');
if ($el.length) $el.val(val);
}
function markReview(name, info) {
const $el = $('#' + name);
if (!$el.length) return;
info = info || {};
$el.addClass('import-review');
$el.val(info.value ? info.value : ''); // pre-seleziona il suggerimento se c'è
const $wrap = $el.closest('[class*="col-"]');
let $tip = $wrap.find('.import-hint');
if (!$tip.length) {
$tip = $('<div class="import-hint"></div>');
$el.after($tip);
}
$tip.text(info.value ?
'Suggerito dal file: ' + (info.label || info.value) + ' — verifica' :
'Dal file: ' + (info.raw || '') + ' — scegli il valore');
}
function renderBanner(data) {
const bits = [];
(data.mescole || []).forEach(m =>
bits.push('Mescola “' + m.raw + '” ' + (m.idmescola ? '✓' : '⚠ da abbinare a mano')));
(data.packages || []).forEach(p =>
bits.push('Imballo “' + p.raw + '” (' + p.source + ') ' + (p.package_code ? '✓' : '⚠ da aggiungere a mano')));
let html = '<div class="import-banner"><strong>Import completato.</strong> Controlla i campi evidenziati in giallo, poi premi “Salva testata”.';
if (bits.length) html += '<ul style="margin:8px 0 0 18px;">' + bits.map(b => '<li>' + b + '</li>').join('') + '</ul>';
if ((data.warnings || []).length) html += '<div style="color:#b45309;margin-top:6px;">' + data.warnings.join('<br>') + '</div>';
html += '</div>';
let $b = $('#importBanner');
if (!$b.length) {
$b = $('<div id="importBanner"></div>');
$('#worksheetHeaderForm').prepend($b);
}
$b.html(html);
}
// Salvataggio righe dopo la testata (chiamato dal handler esistente)
window.finalizeImportIfAny = async function(newId) {
const staged = window.importStaged;
if (!staged) return false;
const unresolved = [];
for (const p of (staged.packages || [])) {
if (p.package_code) {
await postForm({
ajax: 1,
action: 'save_package_row',
worksheet_id: newId,
package_code: p.package_code
});
} else {
unresolved.push('Imballo “' + p.raw + '” (' + p.source + ')');
}
}
for (const m of (staged.mescole || [])) {
if (m.idmescola) {
await postForm({
ajax: 1,
action: 'save_mix_row',
worksheet_id: newId,
idmescola: m.idmescola,
mix_weight_g_m: m.mix_weight_g_m ?? '',
required_density: m.required_density ?? '',
required_hardness_shore_a: m.required_hardness_shore_a ?? '',
lubrication_type: m.lubrication_type ?? '',
lubrication_notes: ''
});
} else {
unresolved.push('Mescola “' + m.raw + '”');
}
}
window.importStaged = null;
// Allega il file Excel di origine al foglio appena creato
if (window.importFile) {
try {
const fd = new FormData();
fd.append('worksheet_id', newId);
fd.append('xlsx_file', window.importFile);
await fetch('attach_worksheet_file.php', {
method: 'POST',
body: fd
});
} catch (e) {
/* se l'allegato fallisce non blocchiamo il flusso */
}
window.importFile = null;
}
let msg = 'Testata e righe importate salvate correttamente.';
if (unresolved.length) msg += '\n\nDa aggiungere a mano dopo il ricaricamento:\n• ' + unresolved.join('\n• ');
await Swal.fire({
icon: unresolved.length ? 'warning' : 'success',
title: 'Import completato',
text: msg
});
window.location.href = 'manage-worksheet.php?id=' + newId;
return true;
};
$('#btnImportXlsx').on('click', function() {
const wsId = parseInt($('#wsId').val() || '0', 10);
if (wsId > 0) {
Swal.fire({
icon: 'info',
title: 'Import solo su foglio nuovo',
text: 'Apri “Nuovo Foglio di Lavoro” per importare un Excel.'
});
return;
}
$('#xlsxImportInput').val('').click();
});
$('#xlsxImportInput').on('change', function() {
const file = this.files && this.files[0];
if (!file) return;
window.importFile = file; // conserva il file per allegarlo dopo il salvataggio
const fd = new FormData();
fd.append('xlsx_file', file);
Swal.fire({
title: 'Lettura Excel…',
allowOutsideClick: false,
didOpen: () => Swal.showLoading()
});
fetch('import_worksheet_xlsx.php', {
method: 'POST',
body: fd
})
.then(async r => {
const t = await r.text();
try {
return JSON.parse(t);
} catch (e) {
throw new Error('Risposta non valida dal parser:\n' + t.slice(0, 300));
}
})
.then(data => {
Swal.close();
if (!data.success) {
Swal.fire({
icon: 'error',
title: 'Errore import',
text: data.message || 'Errore sconosciuto'
});
return;
}
const h = data.header || {};
setVal('worksheet_date', h.worksheet_date);
setVal('customer_name', h.customer_name);
setVal('profile_type_code', h.profile_type_code);
setVal('marking', h.marking);
setVal('prod_control_measure_settings', h.prod_control_measure_settings);
setVal('packages_or_pieces_per_box', h.packages_or_pieces_per_box);
setVal('boxes_or_packages_per_pallet', h.boxes_or_packages_per_pallet);
setVal('speed_expected_kg_h', h.speed_expected_kg_h);
setVal('speed_expected_m_h', h.speed_expected_m_h);
setVal('notes', h.notes);
const lk = data.header_lookups || {};
Object.keys(lk).forEach(name => {
if (lk[name] && lk[name].needs_review) markReview(name, lk[name]);
});
window.importStaged = {
mescole: data.mescole || [],
packages: data.packages || [],
warnings: data.warnings || []
};
renderBanner(data);
Swal.fire({
icon: 'success',
title: 'Excel letto',
text: 'Controlla i campi evidenziati, poi premi “Salva testata”.',
timer: 1800
});
})
.catch(err => {
Swal.close();
Swal.fire({
icon: 'error',
title: 'Errore',
text: err.message
});
});
});
});
</script>
</body> </body>
</html> </html>