Files
zibo-dashboard/public/userarea/import_worksheet_xlsx.php
T
2026-10-01 14:50:29 +02:00

506 lines
19 KiB
PHP

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