976 lines
43 KiB
PHP
976 lines
43 KiB
PHP
<?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>
|