diff --git a/db/migrations/20260905000001_add_engine_and_routine_step.php b/db/migrations/20260905000001_add_engine_and_routine_step.php new file mode 100644 index 00000000..16a16b48 --- /dev/null +++ b/db/migrations/20260905000001_add_engine_and_routine_step.php @@ -0,0 +1,68 @@ +table('routine'); + if (!$routine->hasColumn('engine')) { + $routine + ->addColumn('engine', 'boolean', [ + 'default' => 0, + 'null' => false, + 'comment' => '0 = routine legacy su file, 1 = motore generico (routine_step)', + 'after' => 'filename', + ]) + ->update(); + } + + // 2) Nuova tabella routine_step + if (!$this->hasTable('routine_step')) { + $table = $this->table('routine_step', [ + 'id' => 'idstep', + 'signed' => false, + 'engine' => 'InnoDB', + 'collation' => 'utf8mb4_general_ci', + ]); + + $table + ->addColumn('idroutine', 'integer', [ + 'null' => false, + 'comment' => 'FK verso routine.idroutine', + ]) + ->addColumn('step_order', 'integer', [ + 'default' => 1, + 'null' => false, + 'comment' => 'Ordine di esecuzione dello step nella routine', + ]) + ->addColumn('operation', 'string', [ + 'limit' => 50, + 'null' => false, + 'comment' => 'Verbo operazione: merge, replace, trim, copy, clear, uppercase, lowercase', + ]) + ->addColumn('params', 'text', [ + 'null' => true, + 'comment' => 'Parametri operazione in formato JSON', + ]) + ->addColumn('active', 'boolean', [ + 'default' => 1, + 'null' => false, + 'comment' => 'Se 0 lo step viene saltato', + ]) + ->addIndex(['idroutine', 'step_order'], ['name' => 'idx_routine_order']) + ->create(); + } + } +} diff --git a/db/seeds/RoutineEngineSeed.php b/db/seeds/RoutineEngineSeed.php new file mode 100644 index 00000000..e50202f3 --- /dev/null +++ b/db/seeds/RoutineEngineSeed.php @@ -0,0 +1,80 @@ +getAdapter()->getConnection(); + + // 1) Le legacy restano engine = 0 (idempotente). + $pdo->exec("UPDATE routine SET engine = 0 WHERE engine IS NULL"); + + // 2) Crea/aggiorna la routine engine di esempio. + $name = 'merge_D_F_H_into_J'; + $description = 'Unisce le colonne D, F, H nella colonna J durante l\'importazione'; + $filename = '_engine.php'; + + $check = $pdo->prepare("SELECT idroutine FROM routine WHERE name = :name LIMIT 1"); + $check->execute([':name' => $name]); + $existing = $check->fetch(PDO::FETCH_ASSOC); + + if ($existing) { + $idroutine = (int)$existing['idroutine']; + $upd = $pdo->prepare( + "UPDATE routine + SET description = :descr, filename = :fn, engine = 1, instruction = :instr + WHERE idroutine = :id" + ); + $upd->execute([ + ':descr' => $description, + ':fn' => $filename, + ':instr' => 'Routine gestita dal motore generico (routine_step).', + ':id' => $idroutine, + ]); + } else { + $ins = $pdo->prepare( + "INSERT INTO routine (name, description, instruction, filename, engine, headerrow) + VALUES (:name, :descr, :instr, :fn, 1, 1)" + ); + $ins->execute([ + ':name' => $name, + ':descr' => $description, + ':instr' => 'Routine gestita dal motore generico (routine_step).', + ':fn' => $filename, + ]); + $idroutine = (int)$pdo->lastInsertId(); + } + + // 3) Ripulisci eventuali step precedenti di questa routine e reinserisci. + $del = $pdo->prepare("DELETE FROM routine_step WHERE idroutine = :id"); + $del->execute([':id' => $idroutine]); + + // Step unico: merge D,F,H -> J. Colonne espresse in lettere per leggibilità. + $params = json_encode([ + 'source_columns' => ['D', 'F', 'H'], + 'target_column' => 'J', + 'separator' => ' ', + 'skip_empty' => true, + ], JSON_UNESCAPED_UNICODE); + + $insStep = $pdo->prepare( + "INSERT INTO routine_step (idroutine, step_order, operation, params, active) + VALUES (:id, 1, 'merge', :params, 1)" + ); + $insStep->execute([':id' => $idroutine, ':params' => $params]); + } +} diff --git a/public/userarea/apply_routine.php b/public/userarea/apply_routine.php index 64670f3e..d9dbc20b 100644 --- a/public/userarea/apply_routine.php +++ b/public/userarea/apply_routine.php @@ -21,12 +21,44 @@ try { throw new Exception("Dati della routine mancanti."); } - $routineFile = __DIR__ . '/routines/' . $filename; + // --- NUOVO: risolvi la routine dal DB per decidere legacy vs motore --- + // Riusa la connessione PDO del progetto. + require_once __DIR__ . '/class/db-functions.php'; + $pdo = DBHandlerSelect::getInstance()->getConnection(); + + // Identifica la routine: preferisci idroutine se passato, altrimenti risali dal filename. + $idroutine = isset($input['routine_data']['idroutine']) + ? (int)$input['routine_data']['idroutine'] + : 0; + + if ($idroutine > 0) { + $stmt = $pdo->prepare("SELECT idroutine, name, filename, engine FROM routine WHERE idroutine = :id LIMIT 1"); + $stmt->execute([':id' => $idroutine]); + } else { + $stmt = $pdo->prepare("SELECT idroutine, name, filename, engine FROM routine WHERE filename = :fn LIMIT 1"); + $stmt->execute([':fn' => $filename]); + } + $routineRow = $stmt->fetch(); + + // engine = 1 -> motore generico (_engine.php); 0 o record assente -> legacy come oggi. + $useEngine = $routineRow && (int)$routineRow['engine'] === 1; + + if ($useEngine) { + // Il motore ha bisogno di sapere quale routine caricare dagli step. + $routineData['idroutine'] = (int)$routineRow['idroutine']; + $routineData['name'] = $routineData['name'] ?? ($routineRow['name'] ?? ''); + $routineFile = __DIR__ . '/routines/_engine.php'; + } else { + // Percorso legacy: il vecchio file della routine, identico a prima. + $legacyFilename = $routineRow['filename'] ?? $filename; + $routineFile = __DIR__ . '/routines/' . $legacyFilename; + } + if (file_exists($routineFile)) { include_once $routineFile; $routineData['xls_headers'] = $_SESSION['headers'] ?? []; applyRoutine($excelData, $routineData); // Modifica $excelData in place - error_log("Routine {$routineData['name']} applicata (file: {$filename}) per template {$template_id}, header row: {$headerrow}"); + error_log("Routine " . ($routineData['name'] ?? '') . " applicata (" . ($useEngine ? "motore" : "file: {$filename}") . ") per template {$template_id}, header row: {$headerrow}"); } else { throw new Exception("File della routine non trovato: $routineFile"); } diff --git a/public/userarea/include/navbar.php b/public/userarea/include/navbar.php index 101588ca..5f7d63cd 100644 --- a/public/userarea/include/navbar.php +++ b/public/userarea/include/navbar.php @@ -103,6 +103,14 @@ +
  • + +
    + +
    + +
    +
  • diff --git a/public/userarea/routine_admin.php b/public/userarea/routine_admin.php new file mode 100644 index 00000000..cf4be4fb --- /dev/null +++ b/public/userarea/routine_admin.php @@ -0,0 +1,549 @@ +hasRole('Admin') && !$user->hasRole('SuperUser')) { + header('Location: import_dashboard.php'); + exit; +} + +$db = DBHandlerSelect::getInstance(); +$pdo = $db->getConnection(); + +/* + * Nome file del motore generico condiviso da tutte le routine engine. + * Deve corrispondere al file presente in routines/. + */ +$engineFilename = '_engine.php'; + +/* + * CSRF token. + */ +if (empty($_SESSION['routine_admin_csrf'])) { + $_SESSION['routine_admin_csrf'] = bin2hex(random_bytes(32)); +} +$csrfToken = $_SESSION['routine_admin_csrf']; + +function cleanInput($value) +{ + return trim((string)($value ?? '')); +} + +function redirectWithMessage($type, $message) +{ + $_SESSION['routine_admin_flash'] = [ + 'type' => $type, + 'message' => $message + ]; + header('Location: ' . $_SERVER['PHP_SELF']); + exit; +} + +/* + * Gestione azioni POST. + */ +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + try { + $postedToken = $_POST['csrf_token'] ?? ''; + if (!hash_equals($csrfToken, $postedToken)) { + throw new Exception('Token di sicurezza non valido.'); + } + + $action = cleanInput($_POST['action'] ?? ''); + + if ($action === 'create' || $action === 'update') { + $name = cleanInput($_POST['name'] ?? ''); + $description = cleanInput($_POST['description'] ?? ''); + $engine = (int)($_POST['engine'] ?? 0) === 1 ? 1 : 0; + $headerrow = (int)($_POST['headerrow'] ?? 1); + $filename = cleanInput($_POST['filename'] ?? ''); + + if ($name === '') { + throw new Exception('Il nome della routine è obbligatorio.'); + } + if ($headerrow < 1) { + $headerrow = 1; + } + + /* + * Se è una routine a motore, il filename è forzato al file del motore. + * Se è legacy, serve un filename di file reale. + */ + if ($engine === 1) { + $filename = $GLOBALS['engineFilename']; + } else { + if ($filename === '') { + throw new Exception('Per una routine di tipo File indica il nome del file (es. fendi.php).'); + } + } + + if ($action === 'create') { + // instruction è NOT NULL nello schema: valorizziamo sempre. + $instruction = $engine === 1 + ? 'Routine gestita dal motore generico (routine_step).' + : ('Routine su file: ' . $filename); + + $stmt = $pdo->prepare(" + INSERT INTO routine (name, description, instruction, filename, engine, headerrow) + VALUES (:name, :descr, :instr, :fn, :engine, :hr) + "); + $stmt->execute([ + ':name' => $name, + ':descr' => ($description !== '' ? $description : null), + ':instr' => $instruction, + ':fn' => $filename, + ':engine' => $engine, + ':hr' => $headerrow, + ]); + + redirectWithMessage('success', 'Routine creata correttamente.'); + } else { + $idroutine = (int)($_POST['idroutine'] ?? 0); + if ($idroutine <= 0) { + throw new Exception('ID routine non valido.'); + } + + $stmt = $pdo->prepare(" + UPDATE routine + SET name = :name, + description = :descr, + filename = :fn, + engine = :engine, + headerrow = :hr + WHERE idroutine = :id + "); + $stmt->execute([ + ':name' => $name, + ':descr' => ($description !== '' ? $description : null), + ':fn' => $filename, + ':engine' => $engine, + ':hr' => $headerrow, + ':id' => $idroutine, + ]); + + redirectWithMessage('success', 'Routine aggiornata correttamente.'); + } + } + + if ($action === 'delete') { + $idroutine = (int)($_POST['idroutine'] ?? 0); + if ($idroutine <= 0) { + throw new Exception('ID routine non valido.'); + } + + // Rimuove prima gli eventuali step collegati, poi la routine. + $stmt = $pdo->prepare("DELETE FROM routine_step WHERE idroutine = :id"); + $stmt->execute([':id' => $idroutine]); + + $stmt = $pdo->prepare("DELETE FROM routine WHERE idroutine = :id"); + $stmt->execute([':id' => $idroutine]); + + redirectWithMessage('success', 'Routine eliminata correttamente.'); + } + + throw new Exception('Azione non valida.'); + } catch (Exception $e) { + redirectWithMessage('error', $e->getMessage()); + } +} + +/* + * Caricamento routine + conteggio step. + */ +$stmt = $pdo->query(" + SELECT + r.idroutine, + r.name, + r.description, + r.filename, + r.engine, + r.headerrow, + (SELECT COUNT(*) FROM routine_step s WHERE s.idroutine = r.idroutine) AS step_count + FROM routine r + ORDER BY r.name ASC +"); +$routines = $stmt->fetchAll(PDO::FETCH_ASSOC); + +$flash = $_SESSION['routine_admin_flash'] ?? null; +unset($_SESSION['routine_admin_flash']); +?> + + + + + + + + + + Gestione Routine - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/userarea/routine_preview.php b/public/userarea/routine_preview.php new file mode 100644 index 00000000..2d18b05e --- /dev/null +++ b/public/userarea/routine_preview.php @@ -0,0 +1,184 @@ + N, 'data' => [0=>.., 1=>.., ...]] ] + * - Include _engine.php e applica applyRoutine(). + * - Risponde JSON con la riga elaborata + intestazioni + lettere colonna. + */ + +ob_start(); +session_start(); +require_once '../../vendor/autoload.php'; + +use PhpOffice\PhpSpreadsheet\IOFactory; +use PhpOffice\PhpSpreadsheet\Cell\Coordinate; + +Dotenv\Dotenv::createImmutable(dirname(__DIR__, 2))->safeLoad(); +date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Rome'); + +header('Content-Type: application/json'); + +$response = [ + 'error' => '', + 'headers' => [], // intestazioni prese dalla header row + 'letters' => [], // lettere colonna (A, B, C, ...) allineate ai valori + 'row' => [], // valori della riga DOPO l'applicazione del motore + 'excelrow' => 0, // numero riga excel elaborata +]; + +/** + * Indice zero-based -> lettera colonna Excel (0 -> A, 25 -> Z, 26 -> AA...). + */ +function previewIndexToCol($index) +{ + $index = (int)$index; + if ($index < 0) { + return ''; + } + $letters = ''; + $index += 1; + while ($index > 0) { + $rem = ($index - 1) % 26; + $letters = chr(ord('A') + $rem) . $letters; + $index = intdiv($index - 1, 26); + } + return $letters; +} + +try { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + throw new Exception('Richiesta non valida.'); + } + + $idroutine = (int)($_POST['idroutine'] ?? 0); + if ($idroutine <= 0) { + throw new Exception('ID routine mancante.'); + } + + if ( + empty($_FILES['file']) || + !isset($_FILES['file']['tmp_name']) || + $_FILES['file']['error'] !== UPLOAD_ERR_OK + ) { + throw new Exception('Nessun file caricato o upload non riuscito.'); + } + + // --- Recupera la routine e verifica che sia a motore --- + require_once __DIR__ . '/class/db-functions.php'; + $pdo = DBHandlerSelect::getInstance()->getConnection(); + + $stmt = $pdo->prepare("SELECT idroutine, name, engine, headerrow FROM routine WHERE idroutine = :id LIMIT 1"); + $stmt->execute([':id' => $idroutine]); + $routine = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$routine) { + throw new Exception('Routine non trovata.'); + } + if ((int)$routine['engine'] !== 1) { + throw new Exception('Questa routine non è di tipo Motore: anteprima non disponibile.'); + } + + $headerRow = max(1, (int)$routine['headerrow']); // riga 1-based dell'intestazione + + // --- Legge l'XLS dal file temporaneo (nessun salvataggio) --- + $tmpPath = $_FILES['file']['tmp_name']; + + $reader = IOFactory::createReaderForFile($tmpPath); + $reader->setReadDataOnly(true); + $spreadsheet = $reader->load($tmpPath); + $sheet = $spreadsheet->getActiveSheet(); + + $highestColIndex = Coordinate::columnIndexFromString($sheet->getHighestDataColumn()); // 1-based + $highestRow = $sheet->getHighestDataRow(); + + // --- Intestazioni dalla header row --- + $headers = []; + $letters = []; + for ($c = 1; $c <= $highestColIndex; $c++) { + $val = $sheet->getCell([$c, $headerRow])->getValue(); + $headers[] = ($val === null ? '' : (string)$val); + $letters[] = previewIndexToCol($c - 1); // zero-based -> lettera + } + + // --- Trova la prima riga dati valorizzata dopo l'header --- + $dataRowNumber = null; + $rowValues = null; + + for ($r = $headerRow + 1; $r <= $highestRow; $r++) { + $vals = []; + $hasValue = false; + for ($c = 1; $c <= $highestColIndex; $c++) { + $v = $sheet->getCell([$c, $r])->getValue(); + $v = ($v === null ? '' : (string)$v); + if ($v !== '') { + $hasValue = true; + } + $vals[$c - 1] = $v; // indice zero-based, allineato alle colonne + } + if ($hasValue) { + $dataRowNumber = $r; + $rowValues = $vals; + break; + } + } + + if ($dataRowNumber === null) { + throw new Exception('Nessuna riga dati valorizzata trovata dopo la riga di intestazione (' . $headerRow . ').'); + } + + // --- Impacchetta nel formato excelData e applica il motore --- + $excelData = [ + [ + 'excelrow' => $dataRowNumber, + 'data' => $rowValues, + ], + ]; + + // Il motore legge gli step dal DB in base a idroutine. + $routineData = [ + 'idroutine' => $idroutine, + 'name' => $routine['name'], + 'headerrow' => $headerRow, + 'xls_headers' => $headers, + ]; + + require_once __DIR__ . '/routines/_engine.php'; + applyRoutine($excelData, $routineData); // modifica in place + + $processed = $excelData[0]['data'] ?? []; + + // Riallinea l'output all'ampiezza delle intestazioni (il merge può creare indici nuovi). + $maxIndex = max( + count($headers) - 1, + empty($processed) ? 0 : max(array_keys($processed)) + ); + + $outRow = []; + $outLetters = []; + $outHeaders = []; + for ($i = 0; $i <= $maxIndex; $i++) { + $outRow[] = array_key_exists($i, $processed) ? (string)$processed[$i] : ''; + $outLetters[] = previewIndexToCol($i); + $outHeaders[] = array_key_exists($i, $headers) ? $headers[$i] : ''; + } + + $response['headers'] = $outHeaders; + $response['letters'] = $outLetters; + $response['row'] = $outRow; + $response['excelrow'] = $dataRowNumber; +} catch (Throwable $e) { + $response['error'] = 'Errore anteprima: ' . $e->getMessage(); + error_log('routine_preview.php: ' . $e->getMessage()); +} + +ob_end_clean(); +echo json_encode($response, JSON_UNESCAPED_UNICODE); +exit; diff --git a/public/userarea/routine_steps.php b/public/userarea/routine_steps.php new file mode 100644 index 00000000..a56ff03a --- /dev/null +++ b/public/userarea/routine_steps.php @@ -0,0 +1,976 @@ +hasRole('Admin') && !$user->hasRole('SuperUser')) { + header('Location: import_dashboard.php'); + exit; +} + +$db = DBHandlerSelect::getInstance(); +$pdo = $db->getConnection(); + +/* + * CSRF token. + */ +if (empty($_SESSION['routine_steps_csrf'])) { + $_SESSION['routine_steps_csrf'] = bin2hex(random_bytes(32)); +} +$csrfToken = $_SESSION['routine_steps_csrf']; + +function cleanInput($value) +{ + return trim((string)($value ?? '')); +} + +/* + * Determina l'id routine dalla querystring (GET) o dal POST. + */ +$idroutine = 0; +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $idroutine = (int)($_POST['idroutine'] ?? 0); +} else { + $idroutine = (int)($_GET['idroutine'] ?? 0); +} + +function redirectToSteps($idroutine, $type, $message) +{ + $_SESSION['routine_steps_flash'] = [ + 'type' => $type, + 'message' => $message + ]; + header('Location: routine_steps.php?idroutine=' . (int)$idroutine); + exit; +} + +/* + * Converte lettera colonna Excel in indice zero-based. Accetta anche numeri. + * A=0, B=1, ... Z=25, AA=26, ... + */ +function colToIndex($ref) +{ + $ref = strtoupper(trim((string)$ref)); + if ($ref === '') { + return null; + } + if (ctype_digit($ref)) { + return (int)$ref; + } + $index = 0; + $len = strlen($ref); + for ($i = 0; $i < $len; $i++) { + $c = $ref[$i]; + if ($c < 'A' || $c > 'Z') { + return null; + } + $index = $index * 26 + (ord($c) - ord('A') + 1); + } + return $index - 1; +} + +/* + * Converte indice zero-based in lettera Excel (per mostrare gli step salvati). + */ +function indexToCol($index) +{ + $index = (int)$index; + if ($index < 0) { + return ''; + } + $letters = ''; + $index += 1; + while ($index > 0) { + $rem = ($index - 1) % 26; + $letters = chr(ord('A') + $rem) . $letters; + $index = intdiv($index - 1, 26); + } + return $letters; +} + +/* + * Normalizza una stringa di colonne separate da virgola ("D, F, H") in + * un array di indici zero-based, validando ogni riferimento. + */ +function parseColumnList($raw) +{ + $raw = trim((string)$raw); + if ($raw === '') { + return []; + } + $parts = preg_split('/[\s,;]+/', $raw); + $out = []; + foreach ($parts as $p) { + if ($p === '') { + continue; + } + $idx = colToIndex($p); + if ($idx === null) { + throw new Exception('Riferimento colonna non valido: "' . $p . '". Usa lettere come D, F, H.'); + } + $out[] = $idx; + } + return $out; +} + +/* + * Verifica esistenza routine ed engine = 1. + */ +$routine = null; +if ($idroutine > 0) { + $stmt = $pdo->prepare("SELECT idroutine, name, description, engine, headerrow FROM routine WHERE idroutine = :id LIMIT 1"); + $stmt->execute([':id' => $idroutine]); + $routine = $stmt->fetch(PDO::FETCH_ASSOC); +} + +if (!$routine) { + $_SESSION['routine_admin_flash'] = ['type' => 'error', 'message' => 'Routine non trovata.']; + header('Location: routine_admin.php'); + exit; +} + +if ((int)$routine['engine'] !== 1) { + $_SESSION['routine_admin_flash'] = ['type' => 'error', 'message' => 'Questa routine è di tipo File: gli step non sono configurabili da qui.']; + header('Location: routine_admin.php'); + exit; +} + +/* + * Vocabolario operazioni disponibili (deve combaciare con gli handler di _engine.php). + */ +$operations = ['merge', 'copy', 'clear', 'trim', 'replace', 'uppercase', 'lowercase']; + +/* + * Gestione azioni POST. + */ +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + try { + $postedToken = $_POST['csrf_token'] ?? ''; + if (!hash_equals($csrfToken, $postedToken)) { + throw new Exception('Token di sicurezza non valido.'); + } + + $action = cleanInput($_POST['action'] ?? ''); + + if ($action === 'create' || $action === 'update') { + $operation = strtolower(cleanInput($_POST['operation'] ?? '')); + $active = (int)($_POST['active'] ?? 1) === 1 ? 1 : 0; + + if (!in_array($operation, $operations, true)) { + throw new Exception('Operazione non valida.'); + } + + /* + * Costruzione params in base all'operazione. + * Le colonne arrivano come lettere e vengono convertite in indici. + */ + $params = []; + + if ($operation === 'merge') { + $sources = parseColumnList($_POST['source_columns'] ?? ''); + $target = colToIndex($_POST['target_column'] ?? ''); + if (empty($sources)) { + throw new Exception('Indica almeno una colonna sorgente (es. D, F, H).'); + } + if ($target === null) { + throw new Exception('Indica la colonna di destinazione (es. J).'); + } + $params = [ + 'source_columns' => $sources, + 'target_column' => $target, + 'separator' => (string)($_POST['separator'] ?? ' '), + 'skip_empty' => isset($_POST['skip_empty']) ? true : false, + ]; + } elseif ($operation === 'copy') { + $src = colToIndex($_POST['source_column'] ?? ''); + $dst = colToIndex($_POST['target_column'] ?? ''); + if ($src === null || $dst === null) { + throw new Exception('Indica colonna sorgente e destinazione (es. D e J).'); + } + $params = ['source_column' => $src, 'target_column' => $dst]; + } elseif ($operation === 'clear' || $operation === 'trim' || $operation === 'uppercase' || $operation === 'lowercase') { + $cols = parseColumnList($_POST['columns'] ?? ''); + if (empty($cols)) { + throw new Exception('Indica almeno una colonna (es. D, F).'); + } + $params = ['columns' => $cols]; + if ($operation === 'trim') { + $chars = (string)($_POST['chars'] ?? ''); + if ($chars !== '') { + $params['chars'] = $chars; + } + } + } elseif ($operation === 'replace') { + $cols = parseColumnList($_POST['columns'] ?? ''); + if (empty($cols)) { + throw new Exception('Indica almeno una colonna (es. D, F).'); + } + $params = [ + 'columns' => $cols, + 'search' => (string)($_POST['search'] ?? ''), + 'replace' => (string)($_POST['replace'] ?? ''), + 'regex' => isset($_POST['regex']) ? true : false, + ]; + } + + $paramsJson = json_encode($params, JSON_UNESCAPED_UNICODE); + + if ($action === 'create') { + // step_order = max + 1 + $stmt = $pdo->prepare("SELECT COALESCE(MAX(step_order), 0) + 1 AS next_order FROM routine_step WHERE idroutine = :id"); + $stmt->execute([':id' => $idroutine]); + $nextOrder = (int)$stmt->fetchColumn(); + + $stmt = $pdo->prepare(" + INSERT INTO routine_step (idroutine, step_order, operation, params, active) + VALUES (:id, :ord, :op, :params, :active) + "); + $stmt->execute([ + ':id' => $idroutine, + ':ord' => $nextOrder, + ':op' => $operation, + ':params' => $paramsJson, + ':active' => $active, + ]); + + redirectToSteps($idroutine, 'success', 'Step aggiunto correttamente.'); + } else { + $idstep = (int)($_POST['idstep'] ?? 0); + if ($idstep <= 0) { + throw new Exception('ID step non valido.'); + } + $stmt = $pdo->prepare(" + UPDATE routine_step + SET operation = :op, params = :params, active = :active + WHERE idstep = :idstep AND idroutine = :id + "); + $stmt->execute([ + ':op' => $operation, + ':params' => $paramsJson, + ':active' => $active, + ':idstep' => $idstep, + ':id' => $idroutine, + ]); + + redirectToSteps($idroutine, 'success', 'Step aggiornato correttamente.'); + } + } + + if ($action === 'delete') { + $idstep = (int)($_POST['idstep'] ?? 0); + if ($idstep <= 0) { + throw new Exception('ID step non valido.'); + } + $stmt = $pdo->prepare("DELETE FROM routine_step WHERE idstep = :idstep AND idroutine = :id"); + $stmt->execute([':idstep' => $idstep, ':id' => $idroutine]); + redirectToSteps($idroutine, 'success', 'Step eliminato.'); + } + + if ($action === 'move') { + $idstep = (int)($_POST['idstep'] ?? 0); + $dir = cleanInput($_POST['direction'] ?? ''); + if ($idstep <= 0 || !in_array($dir, ['up', 'down'], true)) { + throw new Exception('Parametri di spostamento non validi.'); + } + + // Carica step correnti ordinati. + $stmt = $pdo->prepare("SELECT idstep, step_order FROM routine_step WHERE idroutine = :id ORDER BY step_order ASC, idstep ASC"); + $stmt->execute([':id' => $idroutine]); + $all = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Trova posizione corrente. + $pos = null; + foreach ($all as $i => $s) { + if ((int)$s['idstep'] === $idstep) { + $pos = $i; + break; + } + } + + if ($pos !== null) { + $swapWith = $dir === 'up' ? $pos - 1 : $pos + 1; + if ($swapWith >= 0 && $swapWith < count($all)) { + $a = $all[$pos]; + $b = $all[$swapWith]; + // Scambia gli step_order dei due record. + $upd = $pdo->prepare("UPDATE routine_step SET step_order = :ord WHERE idstep = :idstep AND idroutine = :id"); + $upd->execute([':ord' => (int)$b['step_order'], ':idstep' => (int)$a['idstep'], ':id' => $idroutine]); + $upd->execute([':ord' => (int)$a['step_order'], ':idstep' => (int)$b['idstep'], ':id' => $idroutine]); + } + } + + redirectToSteps($idroutine, 'success', 'Ordine aggiornato.'); + } + + throw new Exception('Azione non valida.'); + } catch (Exception $e) { + redirectToSteps($idroutine, 'error', $e->getMessage()); + } +} + +/* + * Caricamento step della routine. + */ +$stmt = $pdo->prepare("SELECT idstep, step_order, operation, params, active FROM routine_step WHERE idroutine = :id ORDER BY step_order ASC, idstep ASC"); +$stmt->execute([':id' => $idroutine]); +$steps = $stmt->fetchAll(PDO::FETCH_ASSOC); + +/* + * Costruisce una descrizione leggibile dei params per la tabella. + */ +function describeStep($operation, array $p) +{ + $colsToLetters = function ($arr) { + return implode(', ', array_map('indexToCol', $arr)); + }; + switch ($operation) { + case 'merge': + return 'Unisce ' . $colsToLetters($p['source_columns'] ?? []) . + ' → ' . indexToCol($p['target_column'] ?? 0) . + ' (sep: "' . ($p['separator'] ?? ' ') . '")'; + case 'copy': + return 'Copia ' . indexToCol($p['source_column'] ?? 0) . ' → ' . indexToCol($p['target_column'] ?? 0); + case 'clear': + return 'Svuota ' . $colsToLetters($p['columns'] ?? []); + case 'trim': + return 'Trim ' . $colsToLetters($p['columns'] ?? []) . (isset($p['chars']) ? ' (chars: "' . $p['chars'] . '")' : ''); + case 'uppercase': + return 'Maiuscolo ' . $colsToLetters($p['columns'] ?? []); + case 'lowercase': + return 'Minuscolo ' . $colsToLetters($p['columns'] ?? []); + case 'replace': + return 'Sostituisci in ' . $colsToLetters($p['columns'] ?? []) . + ': "' . ($p['search'] ?? '') . '" → "' . ($p['replace'] ?? '') . '"' . + (!empty($p['regex']) ? ' (regex)' : ''); + default: + return $operation; + } +} + +/* + * Prepara i dati step per il JS (params già in lettere per il form di modifica). + */ +$stepsForJs = []; +foreach ($steps as $s) { + $p = json_decode($s['params'] ?? '[]', true); + if (!is_array($p)) { + $p = []; + } + // Converte indici -> lettere per il form. + $letters = function ($arr) { + return implode(', ', array_map('indexToCol', $arr)); + }; + $formData = ['operation' => $s['operation'], 'active' => (int)$s['active']]; + if ($s['operation'] === 'merge') { + $formData['source_columns'] = $letters($p['source_columns'] ?? []); + $formData['target_column'] = indexToCol($p['target_column'] ?? 0); + $formData['separator'] = $p['separator'] ?? ' '; + $formData['skip_empty'] = !empty($p['skip_empty']); + } elseif ($s['operation'] === 'copy') { + $formData['source_column'] = indexToCol($p['source_column'] ?? 0); + $formData['target_column'] = indexToCol($p['target_column'] ?? 0); + } elseif (in_array($s['operation'], ['clear', 'trim', 'uppercase', 'lowercase'], true)) { + $formData['columns'] = $letters($p['columns'] ?? []); + if ($s['operation'] === 'trim') { + $formData['chars'] = $p['chars'] ?? ''; + } + } elseif ($s['operation'] === 'replace') { + $formData['columns'] = $letters($p['columns'] ?? []); + $formData['search'] = $p['search'] ?? ''; + $formData['replace'] = $p['replace'] ?? ''; + $formData['regex'] = !empty($p['regex']); + } + $stepsForJs[(int)$s['idstep']] = $formData; +} + +$flash = $_SESSION['routine_steps_flash'] ?? null; +unset($_SESSION['routine_steps_flash']); +?> + + + + + + + + + + Step Routine - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?> + + + + + +
    + + + +
    +
    + + +
    +
    +
    +
    +
    Step routine:
    + + Configura le operazioni applicate all'XLS. Vengono eseguite dall'alto verso il basso. + +
    +
    + + Torna alle routine + + +
    +
    +
    + +
    +
    + Le colonne si indicano con le lettere dell'Excel (A, B, C, …). Per più colonne separale con la virgola: D, F, H. +
    + +
    + + + + + + + + + + + + + + + + + $s): ?> + + + + + + + + + + + +
    OrdineOperazioneDettaglioStatoAzioni
    + Nessuno step configurato. Usa "Aggiungi step" per iniziare. +
    +
    + + +
    +
    + + Attivo + + Disattivo + + + + +
    +
    + +
    + +
    +
    +
    Prova la routine
    + Carica un XLS di esempio: viene mostrata la prima riga dati dopo l'intestazione, elaborata con gli step qui sopra. Il file non viene salvato. +
    +
    + +
    +
    + + +
    +
    + +
    +
    + +
    + + +
    +
    + +
    +
    + +
    + + +
    + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/userarea/routines/_engine.php b/public/userarea/routines/_engine.php new file mode 100644 index 00000000..b07a4ead --- /dev/null +++ b/public/userarea/routines/_engine.php @@ -0,0 +1,296 @@ + indice diretto + if (ctype_digit($ref)) { + return (int)$ref; + } + // Lettere Excel -> indice zero-based (A=0, B=1, ..., Z=25, AA=26, ...) + $ref = strtoupper($ref); + $index = 0; + $len = strlen($ref); + for ($i = 0; $i < $len; $i++) { + $c = $ref[$i]; + if ($c < 'A' || $c > 'Z') { + continue; + } + $index = $index * 26 + (ord($c) - ord('A') + 1); + } + return $index - 1; + } + + /** Normalizza una lista di riferimenti colonna in indici zero-based. */ + function routineEngineColsToIndexes(array $refs): array + { + return array_map('routineEngineColToIndex', $refs); + } + + /** + * Carica gli step attivi di una routine dal DB, ordinati per step_order. + * Ritorna array di ['operation' => string, 'params' => array]. + */ + function routineEngineLoadSteps(int $idroutine): array + { + // Riusa la connessione PDO del progetto. + // db-functions.php sta in .../class/ rispetto alla cartella routines/. + // Aggiusta il require se nel tuo albero il path differisce. + $dbFile = dirname(__DIR__) . '/class/db-functions.php'; + if (!class_exists('DBHandlerSelect') && file_exists($dbFile)) { + require_once $dbFile; + } + + if (!class_exists('DBHandlerSelect')) { + throw new Exception("Motore routine: classe DBHandlerSelect non trovata (path db-functions.php da verificare)."); + } + + $pdo = DBHandlerSelect::getInstance()->getConnection(); + + $stmt = $pdo->prepare( + "SELECT operation, params + FROM routine_step + WHERE idroutine = :id AND active = 1 + ORDER BY step_order ASC, idstep ASC" + ); + $stmt->execute([':id' => $idroutine]); + $rows = $stmt->fetchAll(); + + $steps = []; + foreach ($rows as $r) { + $params = []; + if (!empty($r['params'])) { + $decoded = json_decode($r['params'], true); + if (is_array($decoded)) { + $params = $decoded; + } else { + error_log("Motore routine: params JSON non valido per operazione {$r['operation']} (routine {$idroutine})."); + } + } + $steps[] = ['operation' => strtolower(trim($r['operation'])), 'params' => $params]; + } + return $steps; + } + + /* ------------------------------------------------------------------ * + * HANDLERS – ogni operazione modifica $excelData in place. + * Aggiungere una capacità nuova = aggiungere un handler qui, una volta. + * ------------------------------------------------------------------ */ + + /** + * merge: unisce N colonne sorgente in una destinazione. + * params: source_columns [], target_column, separator=" ", skip_empty=true + */ + function routineOpMerge(array &$excelData, array $params): void + { + $sources = routineEngineColsToIndexes($params['source_columns'] ?? []); + $target = routineEngineColToIndex($params['target_column'] ?? 0); + $sep = array_key_exists('separator', $params) ? (string)$params['separator'] : ' '; + $skip = $params['skip_empty'] ?? true; + + foreach ($excelData as $rowIndex => &$row) { + if (!isset($row['data']) || !is_array($row['data'])) { + error_log("merge: struttura riga non valida all'indice {$rowIndex}."); + continue; + } + $parts = []; + foreach ($sources as $col) { + $val = trim((string)($row['data'][$col] ?? '')); + if ($skip && $val === '') { + continue; + } + $parts[] = $val; + } + $row['data'][$target] = implode($sep, $parts); + } + unset($row); + } + + /** + * copy: copia una colonna sorgente in una destinazione. + * params: source_column, target_column + */ + function routineOpCopy(array &$excelData, array $params): void + { + $src = routineEngineColToIndex($params['source_column'] ?? 0); + $dst = routineEngineColToIndex($params['target_column'] ?? 0); + foreach ($excelData as &$row) { + if (!isset($row['data']) || !is_array($row['data'])) { + continue; + } + $row['data'][$dst] = $row['data'][$src] ?? ''; + } + unset($row); + } + + /** + * clear: svuota una o più colonne. + * params: columns [] (oppure column singola) + */ + function routineOpClear(array &$excelData, array $params): void + { + $cols = $params['columns'] ?? (isset($params['column']) ? [$params['column']] : []); + $cols = routineEngineColsToIndexes($cols); + foreach ($excelData as &$row) { + if (!isset($row['data']) || !is_array($row['data'])) { + continue; + } + foreach ($cols as $c) { + $row['data'][$c] = ''; + } + } + unset($row); + } + + /** + * trim: rimuove spazi (o caratteri indicati) da una o più colonne. + * params: columns [] (o column), chars (opzionale) + */ + function routineOpTrim(array &$excelData, array $params): void + { + $cols = $params['columns'] ?? (isset($params['column']) ? [$params['column']] : []); + $cols = routineEngineColsToIndexes($cols); + $chars = $params['chars'] ?? null; + foreach ($excelData as &$row) { + if (!isset($row['data']) || !is_array($row['data'])) { + continue; + } + foreach ($cols as $c) { + $v = (string)($row['data'][$c] ?? ''); + $row['data'][$c] = $chars === null ? trim($v) : trim($v, $chars); + } + } + unset($row); + } + + /** + * replace: sostituzione testo in una o più colonne. + * params: columns [] (o column), search, replace="", regex=false + */ + function routineOpReplace(array &$excelData, array $params): void + { + $cols = $params['columns'] ?? (isset($params['column']) ? [$params['column']] : []); + $cols = routineEngineColsToIndexes($cols); + $search = $params['search'] ?? ''; + $replace = $params['replace'] ?? ''; + $regex = $params['regex'] ?? false; + + foreach ($excelData as &$row) { + if (!isset($row['data']) || !is_array($row['data'])) { + continue; + } + foreach ($cols as $c) { + $v = (string)($row['data'][$c] ?? ''); + if ($regex) { + $out = @preg_replace($search, $replace, $v); + $row['data'][$c] = $out === null ? $v : $out; + } else { + $row['data'][$c] = str_replace($search, $replace, $v); + } + } + } + unset($row); + } + + /** uppercase: maiuscolo su una o più colonne. params: columns [] (o column) */ + function routineOpUppercase(array &$excelData, array $params): void + { + $cols = $params['columns'] ?? (isset($params['column']) ? [$params['column']] : []); + $cols = routineEngineColsToIndexes($cols); + foreach ($excelData as &$row) { + if (!isset($row['data']) || !is_array($row['data'])) { + continue; + } + foreach ($cols as $c) { + $row['data'][$c] = mb_strtoupper((string)($row['data'][$c] ?? ''), 'UTF-8'); + } + } + unset($row); + } + + /** lowercase: minuscolo su una o più colonne. params: columns [] (o column) */ + function routineOpLowercase(array &$excelData, array $params): void + { + $cols = $params['columns'] ?? (isset($params['column']) ? [$params['column']] : []); + $cols = routineEngineColsToIndexes($cols); + foreach ($excelData as &$row) { + if (!isset($row['data']) || !is_array($row['data'])) { + continue; + } + foreach ($cols as $c) { + $row['data'][$c] = mb_strtolower((string)($row['data'][$c] ?? ''), 'UTF-8'); + } + } + unset($row); + } + + /** + * Entry point richiamato da apply_routine.php. + * Stessa firma delle routine legacy. + */ + function applyRoutine(&$excelData, $routineData = []) + { + $idroutine = isset($routineData['idroutine']) ? (int)$routineData['idroutine'] : 0; + if ($idroutine <= 0) { + throw new Exception("Motore routine: idroutine mancante in routineData."); + } + + $steps = routineEngineLoadSteps($idroutine); + if (empty($steps)) { + error_log("Motore routine: nessuno step attivo per routine {$idroutine}."); + return; + } + + // Dispatch table verbo -> handler + $handlers = [ + 'merge' => 'routineOpMerge', + 'copy' => 'routineOpCopy', + 'clear' => 'routineOpClear', + 'trim' => 'routineOpTrim', + 'replace' => 'routineOpReplace', + 'uppercase' => 'routineOpUppercase', + 'lowercase' => 'routineOpLowercase', + ]; + + foreach ($steps as $i => $step) { + $op = $step['operation']; + if (!isset($handlers[$op])) { + error_log("Motore routine: operazione sconosciuta '{$op}' (routine {$idroutine}, step " . ($i + 1) . ") — saltata."); + continue; + } + $handlers[$op]($excelData, $step['params']); + error_log("Motore routine: applicata operazione '{$op}' (routine {$idroutine}, step " . ($i + 1) . ")."); + } + + error_log("Motore routine {$idroutine} completato (" . count($steps) . " step)."); + } +} diff --git a/public/userarea/schemi_base_response.json b/public/userarea/schemi_base_response.json index 1178d4ed..bcbfd8dd 100644 --- a/public/userarea/schemi_base_response.json +++ b/public/userarea/schemi_base_response.json @@ -748,7 +748,7 @@ { "IdSchemaCustomFields": 180, "ConteggioClienti": 0, - "Nome": "Chloe'", + "Nome": "Chlo\u00e9_Leathergoods", "Descrizione": "\r\n" }, { @@ -838,7 +838,7 @@ { "IdSchemaCustomFields": 195, "ConteggioClienti": 0, - "Nome": "Chloe'_APPAREL", + "Nome": "Chlo\u00e9_Apparel", "Descrizione": "Schema aggiornato il 18\/12\/25 secondo modulo excel x codifica Elims\r\nGR" }, {