added routine engine

This commit is contained in:
2026-09-05 13:41:26 +02:00
parent 34f7490e94
commit 8f4ac3d53e
9 changed files with 2197 additions and 4 deletions
+34 -2
View File
@@ -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");
}
+8
View File
@@ -103,6 +103,14 @@
<div class="menu-title">Stats</div>
</a>
</li>
<li>
<a href="routine_admin.php">
<div class="parent-icon">
<i class="bx bx-bar-chart-alt-2"></i>
</div>
<div class="menu-title">Routines</div>
</a>
</li>
<li>
<a href="warm_cache_run.php">
<div class="parent-icon">
+549
View File
@@ -0,0 +1,549 @@
<?php
include('include/headscript.php');
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!$user->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']);
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>Gestione Routine - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<style>
.small-muted {
font-size: 12px;
color: #6c757d;
}
/* La tabella non deve sforare la pagina */
#routinesTable {
table-layout: fixed;
width: 100%;
}
#routinesTable td,
#routinesTable th {
overflow-wrap: break-word;
word-break: break-word;
white-space: normal;
vertical-align: middle;
}
/* Colonna descrizione: va a capo invece di allargare la tabella */
#routinesTable td.desc-cell {
white-space: normal;
overflow-wrap: break-word;
word-break: break-word;
max-width: 320px;
}
/* Le azioni restano su una riga */
#routinesTable td.table-actions {
white-space: nowrap;
}
.badge-engine {
background: #6f42c1;
color: #fff;
}
.badge-file {
background: #6c757d;
color: #fff;
}
.table-actions {
white-space: nowrap;
}
.filename-chip {
font-family: Consolas, Monaco, monospace;
font-size: 12px;
background: #f1f5f9;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 2px 8px;
color: #334155;
}
.step-count-pill {
display: inline-block;
min-width: 26px;
text-align: center;
font-weight: 600;
border-radius: 999px;
padding: 2px 10px;
background: #eef2ff;
color: #4338ca;
border: 1px solid #e0e7ff;
}
</style>
</head>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<?php include('top_stat_widget.php'); ?>
<div class="card radius-10">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between">
<div>
<h6 class="mb-0">Gestione Routine</h6>
<small class="text-muted">Crea e configura le routine di importazione. Le routine di tipo Motore sono configurabili a step senza codice.</small>
</div>
<button type="button" class="btn btn-primary" id="btnAddRoutine">
<i class="bx bx-plus"></i> Nuova Routine
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table id="routinesTable" class="table table-striped table-bordered align-middle">
<thead>
<tr>
<th>Nome</th>
<th>Descrizione</th>
<th style="width: 120px;">Tipo</th>
<th style="width: 160px;">File</th>
<th style="width: 90px;">Header row</th>
<th style="width: 80px;">Step</th>
<th style="width: 130px;">Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($routines as $row): ?>
<?php $isEngine = ((int)$row['engine'] === 1); ?>
<tr>
<td><?= htmlspecialchars($row['name'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
<td class="small-muted desc-cell"><?= htmlspecialchars($row['description'] ?? '', ENT_QUOTES, 'UTF-8'); ?></td>
<td>
<?php if ($isEngine): ?>
<span class="badge badge-engine">Motore</span>
<?php else: ?>
<span class="badge badge-file">File</span>
<?php endif; ?>
</td>
<td><span class="filename-chip"><?= htmlspecialchars($row['filename'] ?? '', ENT_QUOTES, 'UTF-8'); ?></span></td>
<td class="text-center"><?= (int)$row['headerrow']; ?></td>
<td class="text-center">
<?php if ($isEngine): ?>
<span class="step-count-pill"><?= (int)$row['step_count']; ?></span>
<?php else: ?>
<span class="small-muted">—</span>
<?php endif; ?>
</td>
<td class="table-actions">
<?php if ($isEngine): ?>
<a class="btn btn-sm btn-outline-success"
href="routine_steps.php?idroutine=<?= (int)$row['idroutine']; ?>"
title="Gestisci step" data-bs-toggle="tooltip">
<i class="bx bx-list-ul"></i>
</a>
<?php endif; ?>
<button
type="button"
class="btn btn-sm btn-outline-primary btnEditRoutine"
data-id="<?= (int)$row['idroutine']; ?>"
data-name="<?= htmlspecialchars($row['name'] ?? '', ENT_QUOTES, 'UTF-8'); ?>"
data-description="<?= htmlspecialchars($row['description'] ?? '', ENT_QUOTES, 'UTF-8'); ?>"
data-engine="<?= (int)$row['engine']; ?>"
data-filename="<?= htmlspecialchars($row['filename'] ?? '', ENT_QUOTES, 'UTF-8'); ?>"
data-headerrow="<?= (int)$row['headerrow']; ?>"
title="Modifica" data-bs-toggle="tooltip">
<i class="bx bx-edit"></i>
</button>
<button
type="button"
class="btn btn-sm btn-outline-danger btnDeleteRoutine"
data-id="<?= (int)$row['idroutine']; ?>"
data-name="<?= htmlspecialchars($row['name'] ?? '', ENT_QUOTES, 'UTF-8'); ?>"
title="Elimina" data-bs-toggle="tooltip">
<i class="bx bx-trash"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
</div>
<!-- Modal Routine -->
<div class="modal fade" id="routineModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<form method="POST" class="modal-content" id="routineForm">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" id="formAction" value="create">
<input type="hidden" name="idroutine" id="idroutine" value="">
<div class="modal-header">
<h5 class="modal-title" id="routineModalTitle">Nuova Routine</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<div class="modal-body">
<div class="row g-3">
<div class="col-md-12">
<label for="name" class="form-label">Nome *</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="col-md-12">
<label for="description" class="form-label">Descrizione</label>
<input type="text" class="form-control" id="description" name="description" maxlength="150">
</div>
<div class="col-md-6">
<label for="engine" class="form-label">Tipo routine *</label>
<select class="form-select" id="engine" name="engine" required>
<option value="1">Motore (step configurabili)</option>
<option value="0">File (legacy)</option>
</select>
<div class="small-muted mt-1">
Motore: configurabile a step da questa interfaccia. File: logica in un file PHP dedicato.
</div>
</div>
<div class="col-md-6">
<label for="headerrow" class="form-label">Riga intestazione (header row) *</label>
<input type="number" class="form-control" id="headerrow" name="headerrow" min="1" value="1" required>
</div>
<div class="col-md-12" id="filenameWrapper">
<label for="filename" class="form-label">Nome file</label>
<input type="text" class="form-control" id="filename" name="filename" placeholder="es. fendi.php">
<div class="small-muted mt-1" id="filenameHelp">
Per le routine di tipo File indica il nome del file PHP presente nella cartella routines/.
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-primary" id="saveRoutineButton">Salva Routine</button>
</div>
</form>
</div>
</div>
<!-- Delete Form -->
<form method="POST" id="deleteRoutineForm" style="display:none;">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="idroutine" id="deleteIdRoutine" value="">
</form>
<?php include('jsinclude.php'); ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
const routineModal = new bootstrap.Modal(document.getElementById('routineModal'));
const formAction = document.getElementById('formAction');
const idroutine = document.getElementById('idroutine');
const routineModalTitle = document.getElementById('routineModalTitle');
const saveRoutineButton = document.getElementById('saveRoutineButton');
const name = document.getElementById('name');
const description = document.getElementById('description');
const engine = document.getElementById('engine');
const headerrow = document.getElementById('headerrow');
const filename = document.getElementById('filename');
const filenameWrapper = document.getElementById('filenameWrapper');
const filenameHelp = document.getElementById('filenameHelp');
const engineFilename = "<?= htmlspecialchars($engineFilename, ENT_QUOTES, 'UTF-8'); ?>";
/*
* Mostra/nasconde il campo filename in base al tipo di routine.
* Motore -> filename forzato al file del motore e campo di sola lettura.
*/
function applyEngineUi() {
if (engine.value === '1') {
filename.value = engineFilename;
filename.readOnly = true;
filenameHelp.textContent = 'Le routine di tipo Motore usano automaticamente il file ' + engineFilename + '.';
} else {
if (filename.value === engineFilename) {
filename.value = '';
}
filename.readOnly = false;
filenameHelp.textContent = 'Per le routine di tipo File indica il nome del file PHP presente nella cartella routines/.';
}
}
engine.addEventListener('change', applyEngineUi);
function resetModalForCreate() {
formAction.value = 'create';
idroutine.value = '';
routineModalTitle.textContent = 'Nuova Routine';
saveRoutineButton.textContent = 'Crea Routine';
name.value = '';
description.value = '';
engine.value = '1';
headerrow.value = '1';
filename.value = '';
applyEngineUi();
}
function setModalForEdit(button) {
formAction.value = 'update';
idroutine.value = button.dataset.id || '';
routineModalTitle.textContent = 'Modifica Routine';
saveRoutineButton.textContent = 'Aggiorna Routine';
name.value = button.dataset.name || '';
description.value = button.dataset.description || '';
engine.value = button.dataset.engine === '1' ? '1' : '0';
headerrow.value = button.dataset.headerrow || '1';
filename.value = button.dataset.filename || '';
applyEngineUi();
}
document.getElementById('btnAddRoutine').addEventListener('click', function() {
resetModalForCreate();
routineModal.show();
});
document.querySelectorAll('.btnEditRoutine').forEach(function(button) {
button.addEventListener('click', function() {
setModalForEdit(button);
routineModal.show();
});
});
document.querySelectorAll('.btnDeleteRoutine').forEach(function(button) {
button.addEventListener('click', function() {
const id = button.dataset.id;
const nm = button.dataset.name || 'questa routine';
Swal.fire({
title: 'Eliminare la routine?',
text: 'Vuoi eliminare "' + nm + '"? Verranno rimossi anche gli step collegati. Operazione irreversibile.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Sì, elimina',
cancelButtonText: 'Annulla',
confirmButtonColor: '#d33'
}).then(function(result) {
if (result.isConfirmed) {
document.getElementById('deleteIdRoutine').value = id;
document.getElementById('deleteRoutineForm').submit();
}
});
});
});
if (typeof $ !== 'undefined' && $.fn.DataTable) {
$('#routinesTable').DataTable({
pageLength: 25,
order: [
[0, 'asc']
]
});
}
<?php if ($flash): ?>
Swal.fire({
icon: '<?= htmlspecialchars($flash['type'], ENT_QUOTES, 'UTF-8'); ?>',
title: '<?= $flash['type'] === 'success' ? 'Fatto' : ($flash['type'] === 'warning' ? 'Attenzione' : 'Errore'); ?>',
text: '<?= htmlspecialchars($flash['message'], ENT_QUOTES, 'UTF-8'); ?>'
});
<?php endif; ?>
});
</script>
</body>
</html>
+184
View File
@@ -0,0 +1,184 @@
<?php
/**
* routine_preview.php
* -------------------
* Anteprima di una routine a motore su un XLS caricato temporaneamente.
*
* - Riceve via multipart/form-data: `file` (XLS/XLSX) e `idroutine`.
* - NON salva nulla: il file viene letto dal tmp di PHP ed elaborato in RAM.
* - Legge la PRIMA riga dati subito dopo l'header row (routine.headerrow).
* - Impacchetta la riga nel formato excelData atteso dal motore:
* [ ['excelrow' => 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;
+976
View File
@@ -0,0 +1,976 @@
<?php
include('include/headscript.php');
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!$user->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']);
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>Step Routine - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<style>
.small-muted {
font-size: 12px;
color: #6c757d;
}
.table-actions {
white-space: nowrap;
}
.op-badge {
font-family: Consolas, Monaco, monospace;
font-size: 12px;
background: #eef2ff;
color: #4338ca;
border: 1px solid #e0e7ff;
border-radius: 6px;
padding: 2px 8px;
text-transform: uppercase;
}
.step-inactive {
opacity: 0.55;
}
.col-hint {
background: #f0fdf4;
border: 1px solid #bbf7d0;
color: #166534;
border-radius: 8px;
padding: 8px 12px;
font-size: 13px;
}
.order-btns .btn {
padding: 2px 8px;
}
</style>
</head>
<body>
<div class="wrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<?php include('top_stat_widget.php'); ?>
<div class="card radius-10">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between">
<div>
<h6 class="mb-0">Step routine: <?= htmlspecialchars($routine['name'], ENT_QUOTES, 'UTF-8'); ?></h6>
<small class="text-muted">
Configura le operazioni applicate all'XLS. Vengono eseguite dall'alto verso il basso.
</small>
</div>
<div>
<a href="routine_admin.php" class="btn btn-light">
<i class="bx bx-arrow-back"></i> Torna alle routine
</a>
<button type="button" class="btn btn-primary" id="btnAddStep">
<i class="bx bx-plus"></i> Aggiungi step
</button>
</div>
</div>
</div>
<div class="card-body">
<div class="col-hint mb-3">
Le colonne si indicano con le <strong>lettere dell'Excel</strong> (A, B, C, …). Per più colonne separale con la virgola: <strong>D, F, H</strong>.
</div>
<div class="table-responsive">
<table class="table table-bordered align-middle">
<thead>
<tr>
<th style="width: 70px;">Ordine</th>
<th style="width: 120px;">Operazione</th>
<th>Dettaglio</th>
<th style="width: 90px;">Stato</th>
<th style="width: 240px;">Azioni</th>
</tr>
</thead>
<tbody>
<?php if (empty($steps)): ?>
<tr>
<td colspan="5" class="text-center small-muted py-4">
Nessuno step configurato. Usa "Aggiungi step" per iniziare.
</td>
</tr>
<?php else: ?>
<?php foreach ($steps as $i => $s): ?>
<?php
$p = json_decode($s['params'] ?? '[]', true);
if (!is_array($p)) {
$p = [];
}
$isFirst = ($i === 0);
$isLast = ($i === count($steps) - 1);
$rowClass = ((int)$s['active'] === 1) ? '' : 'step-inactive';
?>
<tr class="<?= $rowClass; ?>">
<td class="text-center">
<div class="order-btns d-inline-flex flex-column gap-1">
<button type="button" class="btn btn-sm btn-outline-secondary btnMove"
data-idstep="<?= (int)$s['idstep']; ?>" data-direction="up"
<?= $isFirst ? 'disabled' : ''; ?>>
<i class="bx bx-chevron-up"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-secondary btnMove"
data-idstep="<?= (int)$s['idstep']; ?>" data-direction="down"
<?= $isLast ? 'disabled' : ''; ?>>
<i class="bx bx-chevron-down"></i>
</button>
</div>
</td>
<td><span class="op-badge"><?= htmlspecialchars($s['operation'], ENT_QUOTES, 'UTF-8'); ?></span></td>
<td><?= htmlspecialchars(describeStep($s['operation'], $p), ENT_QUOTES, 'UTF-8'); ?></td>
<td>
<?php if ((int)$s['active'] === 1): ?>
<span class="badge bg-success">Attivo</span>
<?php else: ?>
<span class="badge bg-secondary">Disattivo</span>
<?php endif; ?>
</td>
<td class="table-actions">
<button type="button" class="btn btn-sm btn-outline-primary btnEditStep"
data-idstep="<?= (int)$s['idstep']; ?>">
Modifica
</button>
<button type="button" class="btn btn-sm btn-outline-danger btnDeleteStep"
data-idstep="<?= (int)$s['idstep']; ?>">
Elimina
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<hr class="my-4">
<div class="d-flex align-items-center justify-content-between mb-2">
<div>
<h6 class="mb-0">Prova la routine</h6>
<small class="text-muted">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.</small>
</div>
</div>
<div class="row g-2 align-items-end">
<div class="col-md-8">
<label for="previewFile" class="form-label">File XLS / XLSX di prova</label>
<input type="file" class="form-control" id="previewFile" accept=".xls,.xlsx">
</div>
<div class="col-md-4">
<button type="button" class="btn btn-outline-primary w-100" id="btnRunPreview">
<i class="bx bx-play-circle"></i> Esegui anteprima
</button>
</div>
</div>
<div id="previewStatus" class="small-muted mt-2"></div>
<div class="table-responsive mt-3" id="previewResultWrapper" style="display:none;">
<table class="table table-bordered table-sm align-middle" id="previewTable">
<thead>
<tr>
<th style="width: 90px;">Colonna</th>
<th style="width: 40%;">Intestazione</th>
<th>Valore dopo la routine</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="overlay toggle-icon"></div>
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<?php include('include/footer.php'); ?>
</div>
<!-- Modal Step -->
<div class="modal fade" id="stepModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<form method="POST" class="modal-content" id="stepForm">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="idroutine" value="<?= (int)$idroutine; ?>">
<input type="hidden" name="action" id="stepFormAction" value="create">
<input type="hidden" name="idstep" id="idstep" value="">
<div class="modal-header">
<h5 class="modal-title" id="stepModalTitle">Aggiungi step</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<div class="modal-body">
<div class="row g-3">
<div class="col-md-8">
<label for="operation" class="form-label">Operazione *</label>
<select class="form-select" id="operation" name="operation" required>
<option value="merge">merge — unisci più colonne in una</option>
<option value="copy">copy — copia una colonna in un'altra</option>
<option value="clear">clear — svuota colonne</option>
<option value="trim">trim — rimuovi spazi</option>
<option value="replace">replace — sostituisci testo</option>
<option value="uppercase">uppercase — MAIUSCOLO</option>
<option value="lowercase">lowercase — minuscolo</option>
</select>
</div>
<div class="col-md-4">
<label for="active" class="form-label">Stato *</label>
<select class="form-select" id="active" name="active" required>
<option value="1">Attivo</option>
<option value="0">Disattivo</option>
</select>
</div>
<!-- Campi MERGE -->
<div class="col-md-8 op-field op-merge">
<label class="form-label">Colonne sorgente *</label>
<input type="text" class="form-control" name="source_columns" id="merge_source" placeholder="es. D, F, H">
<div class="small-muted mt-1">Lettere separate da virgola.</div>
</div>
<div class="col-md-4 op-field op-merge">
<label class="form-label">Colonna destinazione *</label>
<input type="text" class="form-control" name="target_column" id="merge_target" placeholder="es. J">
</div>
<div class="col-md-8 op-field op-merge">
<label class="form-label">Separatore</label>
<input type="text" class="form-control" name="separator" id="merge_sep" value=" " placeholder="spazio">
<div class="small-muted mt-1">Carattere inserito tra i valori uniti. Default: spazio.</div>
</div>
<div class="col-md-4 op-field op-merge d-flex align-items-end">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="skip_empty" id="merge_skip" checked>
<label class="form-check-label" for="merge_skip">Ignora valori vuoti</label>
</div>
</div>
<!-- Campi COPY -->
<div class="col-md-6 op-field op-copy">
<label class="form-label">Colonna sorgente *</label>
<input type="text" class="form-control" name="source_column" id="copy_source" placeholder="es. D">
</div>
<div class="col-md-6 op-field op-copy">
<label class="form-label">Colonna destinazione *</label>
<input type="text" class="form-control" name="target_column_copy" id="copy_target" placeholder="es. J">
</div>
<!-- Campi COLONNE (clear/trim/uppercase/lowercase/replace) -->
<div class="col-md-12 op-field op-columns">
<label class="form-label">Colonne *</label>
<input type="text" class="form-control" name="columns" id="columns_input" placeholder="es. D, F">
<div class="small-muted mt-1">Una o più lettere separate da virgola.</div>
</div>
<!-- Campo TRIM chars -->
<div class="col-md-12 op-field op-trim">
<label class="form-label">Caratteri da rimuovere (opzionale)</label>
<input type="text" class="form-control" name="chars" id="trim_chars" placeholder="lascia vuoto per spazi">
</div>
<!-- Campi REPLACE -->
<div class="col-md-6 op-field op-replace">
<label class="form-label">Testo da cercare</label>
<input type="text" class="form-control" name="search" id="replace_search">
</div>
<div class="col-md-6 op-field op-replace">
<label class="form-label">Testo sostitutivo</label>
<input type="text" class="form-control" name="replace" id="replace_replace">
</div>
<div class="col-md-12 op-field op-replace">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="regex" id="replace_regex">
<label class="form-check-label" for="replace_regex">
Usa espressione regolare (regex) — solo se sai cosa stai facendo
</label>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-primary" id="saveStepButton">Salva step</button>
</div>
</form>
</div>
</div>
<!-- Form nascosti -->
<form method="POST" id="deleteStepForm" style="display:none;">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="idroutine" value="<?= (int)$idroutine; ?>">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="idstep" id="deleteIdStep" value="">
</form>
<form method="POST" id="moveStepForm" style="display:none;">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="idroutine" value="<?= (int)$idroutine; ?>">
<input type="hidden" name="action" value="move">
<input type="hidden" name="idstep" id="moveIdStep" value="">
<input type="hidden" name="direction" id="moveDirection" value="">
</form>
<?php include('jsinclude.php'); ?>
<script>
const stepsData = <?= json_encode($stepsForJs, JSON_UNESCAPED_UNICODE); ?>;
document.addEventListener('DOMContentLoaded', function() {
const stepModal = new bootstrap.Modal(document.getElementById('stepModal'));
const stepFormAction = document.getElementById('stepFormAction');
const idstep = document.getElementById('idstep');
const stepModalTitle = document.getElementById('stepModalTitle');
const saveStepButton = document.getElementById('saveStepButton');
const operation = document.getElementById('operation');
const active = document.getElementById('active');
// Mappa quali gruppi di campi mostrare per ogni operazione.
const fieldGroups = {
merge: ['op-merge'],
copy: ['op-copy'],
clear: ['op-columns'],
trim: ['op-columns', 'op-trim'],
replace: ['op-columns', 'op-replace'],
uppercase: ['op-columns'],
lowercase: ['op-columns']
};
function showFieldsForOperation(op) {
// Nasconde tutti i gruppi.
document.querySelectorAll('.op-field').forEach(function(el) {
el.style.display = 'none';
});
// Mostra i gruppi dell'operazione scelta.
(fieldGroups[op] || []).forEach(function(cls) {
document.querySelectorAll('.' + cls).forEach(function(el) {
el.style.display = '';
});
});
}
operation.addEventListener('change', function() {
showFieldsForOperation(operation.value);
});
function resetStepForm() {
document.getElementById('stepForm').reset();
// reset non ripristina i checkbox "checked" di default in modo affidabile: li forziamo
document.getElementById('merge_skip').checked = true;
document.getElementById('replace_regex').checked = false;
document.getElementById('merge_sep').value = ' ';
}
function openForCreate() {
stepFormAction.value = 'create';
idstep.value = '';
stepModalTitle.textContent = 'Aggiungi step';
saveStepButton.textContent = 'Aggiungi step';
resetStepForm();
operation.value = 'merge';
active.value = '1';
showFieldsForOperation('merge');
stepModal.show();
}
function openForEdit(id) {
const data = stepsData[id];
if (!data) {
return;
}
stepFormAction.value = 'update';
idstep.value = id;
stepModalTitle.textContent = 'Modifica step';
saveStepButton.textContent = 'Aggiorna step';
resetStepForm();
operation.value = data.operation;
active.value = String(data.active);
// Popola i campi in base all'operazione.
if (data.operation === 'merge') {
document.getElementById('merge_source').value = data.source_columns || '';
document.getElementById('merge_target').value = data.target_column || '';
document.getElementById('merge_sep').value = (data.separator !== undefined ? data.separator : ' ');
document.getElementById('merge_skip').checked = !!data.skip_empty;
} else if (data.operation === 'copy') {
document.getElementById('copy_source').value = data.source_column || '';
document.getElementById('copy_target').value = data.target_column || '';
} else if (['clear', 'trim', 'uppercase', 'lowercase'].includes(data.operation)) {
document.getElementById('columns_input').value = data.columns || '';
if (data.operation === 'trim') {
document.getElementById('trim_chars').value = data.chars || '';
}
} else if (data.operation === 'replace') {
document.getElementById('columns_input').value = data.columns || '';
document.getElementById('replace_search').value = data.search || '';
document.getElementById('replace_replace').value = data.replace || '';
document.getElementById('replace_regex').checked = !!data.regex;
}
showFieldsForOperation(data.operation);
stepModal.show();
}
/*
* NOTA sul campo destinazione di copy:
* l'input HTML si chiama target_column_copy per non collidere con merge,
* ma il backend si aspetta target_column. Rinominiamo al submit.
*/
document.getElementById('stepForm').addEventListener('submit', function(e) {
if (operation.value === 'copy') {
const copyTarget = document.getElementById('copy_target');
// Crea/aggiorna un campo hidden target_column col valore di copy.
let hidden = document.getElementById('copy_target_hidden');
if (!hidden) {
hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.name = 'target_column';
hidden.id = 'copy_target_hidden';
this.appendChild(hidden);
}
hidden.value = copyTarget.value;
}
});
document.getElementById('btnAddStep').addEventListener('click', openForCreate);
document.querySelectorAll('.btnEditStep').forEach(function(btn) {
btn.addEventListener('click', function() {
openForEdit(btn.dataset.idstep);
});
});
document.querySelectorAll('.btnDeleteStep').forEach(function(btn) {
btn.addEventListener('click', function() {
Swal.fire({
title: 'Eliminare lo step?',
text: 'Operazione irreversibile.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Sì, elimina',
cancelButtonText: 'Annulla',
confirmButtonColor: '#d33'
}).then(function(result) {
if (result.isConfirmed) {
document.getElementById('deleteIdStep').value = btn.dataset.idstep;
document.getElementById('deleteStepForm').submit();
}
});
});
});
document.querySelectorAll('.btnMove').forEach(function(btn) {
btn.addEventListener('click', function() {
document.getElementById('moveIdStep').value = btn.dataset.idstep;
document.getElementById('moveDirection').value = btn.dataset.direction;
document.getElementById('moveStepForm').submit();
});
});
// --- Anteprima routine su XLS caricato (nessun salvataggio) ---
const previewFile = document.getElementById('previewFile');
const btnRunPreview = document.getElementById('btnRunPreview');
const previewStatus = document.getElementById('previewStatus');
const previewResultWrapper = document.getElementById('previewResultWrapper');
const previewTableBody = document.querySelector('#previewTable tbody');
const currentIdRoutine = "<?= (int)$idroutine; ?>";
btnRunPreview.addEventListener('click', function() {
if (!previewFile.files || previewFile.files.length === 0) {
Swal.fire({
icon: 'info',
title: 'Nessun file',
text: 'Seleziona un file XLS di prova.'
});
return;
}
const formData = new FormData();
formData.append('idroutine', currentIdRoutine);
formData.append('file', previewFile.files[0]);
previewStatus.textContent = 'Elaborazione in corso...';
previewResultWrapper.style.display = 'none';
previewTableBody.innerHTML = '';
btnRunPreview.disabled = true;
fetch('routine_preview.php', {
method: 'POST',
body: formData,
cache: 'no-store'
})
.then(function(res) {
return res.json();
})
.then(function(data) {
btnRunPreview.disabled = false;
if (data.error) {
previewStatus.textContent = '';
Swal.fire({
icon: 'warning',
title: 'Anteprima non riuscita',
text: data.error
});
return;
}
previewStatus.textContent = 'Riga elaborata: riga Excel ' + data.excelrow + '.';
const letters = data.letters || [];
const headers = data.headers || [];
const row = data.row || [];
for (let i = 0; i < row.length; i++) {
const tr = document.createElement('tr');
const tdCol = document.createElement('td');
tdCol.innerHTML = '<strong>' + (letters[i] || '') + '</strong>';
const tdHead = document.createElement('td');
tdHead.className = 'small-muted';
tdHead.textContent = headers[i] || '';
const tdVal = document.createElement('td');
tdVal.textContent = row[i] || '';
tr.appendChild(tdCol);
tr.appendChild(tdHead);
tr.appendChild(tdVal);
previewTableBody.appendChild(tr);
}
previewResultWrapper.style.display = '';
})
.catch(function(err) {
btnRunPreview.disabled = false;
previewStatus.textContent = '';
Swal.fire({
icon: 'error',
title: 'Errore',
text: 'Impossibile eseguire l\'anteprima.'
});
console.error(err);
});
});
<?php if ($flash): ?>
Swal.fire({
icon: '<?= htmlspecialchars($flash['type'], ENT_QUOTES, 'UTF-8'); ?>',
title: '<?= $flash['type'] === 'success' ? 'Fatto' : ($flash['type'] === 'warning' ? 'Attenzione' : 'Errore'); ?>',
text: '<?= htmlspecialchars($flash['message'], ENT_QUOTES, 'UTF-8'); ?>'
});
<?php endif; ?>
});
</script>
</body>
</html>
+296
View File
@@ -0,0 +1,296 @@
<?php
/**
* Routine motore generico (_engine.php)
* -------------------------------------
* File UNICO condiviso da tutte le routine con engine = 1.
*
* Espone la stessa firma delle routine legacy:
*
* applyRoutine(&$excelData, $routineData)
*
* così apply_routine.php lo include allo stesso modo. La differenza è che qui
* NON c'è logica hardcoded: gli step vengono letti dalla tabella routine_step
* (in base a routineData['idroutine']) ed eseguiti in ordine.
*
* Ogni step ha una `operation` (verbo del vocabolario) e `params` JSON.
* Le colonne nei params possono essere indici zero-based (3) o lettere Excel ("D").
*/
if (!function_exists('applyRoutine')) {
/**
* Converte un riferimento colonna in indice zero-based.
* Accetta int/indici numerici (3) o lettere Excel ("D", "AA", ...).
*/
function routineEngineColToIndex($ref): int
{
if (is_int($ref)) {
return $ref;
}
$ref = trim((string)$ref);
if ($ref === '') {
return 0;
}
// Numerico puro -> 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).");
}
}
+2 -2
View File
@@ -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"
},
{