added routine engine
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Aggiunge il flag `engine` alla tabella `routine` esistente (0 = legacy su file,
|
||||
* 1 = motore generico basato su routine_step) e crea la tabella `routine_step`.
|
||||
*
|
||||
* Migrazione puramente additiva: le routine legacy esistenti (id 1..4) restano
|
||||
* intatte e con engine = 0 continuano a funzionare come prima.
|
||||
*/
|
||||
final class AddEngineAndRoutineStep extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
// 1) Flag engine sulla tabella esistente `routine`
|
||||
$routine = $this->table('routine');
|
||||
if (!$routine->hasColumn('engine')) {
|
||||
$routine
|
||||
->addColumn('engine', 'boolean', [
|
||||
'default' => 0,
|
||||
'null' => false,
|
||||
'comment' => '0 = routine legacy su file, 1 = motore generico (routine_step)',
|
||||
'after' => 'filename',
|
||||
])
|
||||
->update();
|
||||
}
|
||||
|
||||
// 2) Nuova tabella routine_step
|
||||
if (!$this->hasTable('routine_step')) {
|
||||
$table = $this->table('routine_step', [
|
||||
'id' => 'idstep',
|
||||
'signed' => false,
|
||||
'engine' => 'InnoDB',
|
||||
'collation' => 'utf8mb4_general_ci',
|
||||
]);
|
||||
|
||||
$table
|
||||
->addColumn('idroutine', 'integer', [
|
||||
'null' => false,
|
||||
'comment' => 'FK verso routine.idroutine',
|
||||
])
|
||||
->addColumn('step_order', 'integer', [
|
||||
'default' => 1,
|
||||
'null' => false,
|
||||
'comment' => 'Ordine di esecuzione dello step nella routine',
|
||||
])
|
||||
->addColumn('operation', 'string', [
|
||||
'limit' => 50,
|
||||
'null' => false,
|
||||
'comment' => 'Verbo operazione: merge, replace, trim, copy, clear, uppercase, lowercase',
|
||||
])
|
||||
->addColumn('params', 'text', [
|
||||
'null' => true,
|
||||
'comment' => 'Parametri operazione in formato JSON',
|
||||
])
|
||||
->addColumn('active', 'boolean', [
|
||||
'default' => 1,
|
||||
'null' => false,
|
||||
'comment' => 'Se 0 lo step viene saltato',
|
||||
])
|
||||
->addIndex(['idroutine', 'step_order'], ['name' => 'idx_routine_order'])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Seed\AbstractSeed;
|
||||
|
||||
/**
|
||||
* Seed dimostrativo del motore generico.
|
||||
*
|
||||
* - Assicura che le routine legacy esistenti abbiano engine = 0 (nessun impatto).
|
||||
* - Crea una routine di esempio "merge_D_F_H_into_J" con engine = 1
|
||||
* e il relativo step: unisce le colonne D, F, H nella colonna J.
|
||||
*
|
||||
* Nota: filename per le routine engine punta al file motore condiviso _engine.php,
|
||||
* così apply_routine.php lo risolve correttamente anche se il flusso invia il filename.
|
||||
*/
|
||||
final class RoutineEngineSeed extends AbstractSeed
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$pdo = $this->getAdapter()->getConnection();
|
||||
|
||||
// 1) Le legacy restano engine = 0 (idempotente).
|
||||
$pdo->exec("UPDATE routine SET engine = 0 WHERE engine IS NULL");
|
||||
|
||||
// 2) Crea/aggiorna la routine engine di esempio.
|
||||
$name = 'merge_D_F_H_into_J';
|
||||
$description = 'Unisce le colonne D, F, H nella colonna J durante l\'importazione';
|
||||
$filename = '_engine.php';
|
||||
|
||||
$check = $pdo->prepare("SELECT idroutine FROM routine WHERE name = :name LIMIT 1");
|
||||
$check->execute([':name' => $name]);
|
||||
$existing = $check->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($existing) {
|
||||
$idroutine = (int)$existing['idroutine'];
|
||||
$upd = $pdo->prepare(
|
||||
"UPDATE routine
|
||||
SET description = :descr, filename = :fn, engine = 1, instruction = :instr
|
||||
WHERE idroutine = :id"
|
||||
);
|
||||
$upd->execute([
|
||||
':descr' => $description,
|
||||
':fn' => $filename,
|
||||
':instr' => 'Routine gestita dal motore generico (routine_step).',
|
||||
':id' => $idroutine,
|
||||
]);
|
||||
} else {
|
||||
$ins = $pdo->prepare(
|
||||
"INSERT INTO routine (name, description, instruction, filename, engine, headerrow)
|
||||
VALUES (:name, :descr, :instr, :fn, 1, 1)"
|
||||
);
|
||||
$ins->execute([
|
||||
':name' => $name,
|
||||
':descr' => $description,
|
||||
':instr' => 'Routine gestita dal motore generico (routine_step).',
|
||||
':fn' => $filename,
|
||||
]);
|
||||
$idroutine = (int)$pdo->lastInsertId();
|
||||
}
|
||||
|
||||
// 3) Ripulisci eventuali step precedenti di questa routine e reinserisci.
|
||||
$del = $pdo->prepare("DELETE FROM routine_step WHERE idroutine = :id");
|
||||
$del->execute([':id' => $idroutine]);
|
||||
|
||||
// Step unico: merge D,F,H -> J. Colonne espresse in lettere per leggibilità.
|
||||
$params = json_encode([
|
||||
'source_columns' => ['D', 'F', 'H'],
|
||||
'target_column' => 'J',
|
||||
'separator' => ' ',
|
||||
'skip_empty' => true,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
$insStep = $pdo->prepare(
|
||||
"INSERT INTO routine_step (idroutine, step_order, operation, params, active)
|
||||
VALUES (:id, 1, 'merge', :params, 1)"
|
||||
);
|
||||
$insStep->execute([':id' => $idroutine, ':params' => $params]);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
@@ -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).");
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user