506 lines
19 KiB
PHP
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());
|
|
}
|