Compare commits

...

3 Commits

Author SHA1 Message Date
solocla d088364a0d report search 2026-06-10 11:02:43 +02:00
solocla 6e43a178af added fixed field in json 2026-06-09 09:46:21 +02:00
solocla 25bd916221 import insert json 2026-06-08 12:45:37 +02:00
15 changed files with 3262 additions and 315 deletions
@@ -257,35 +257,56 @@ class VisualLimsApiClient
}
/**
* Recupera contenuto binario - Adattato per https://bvcpsitaly-elims.com/limsapi
* Get raw/binary content from VisualLims API.
* Used for PDF downloads from MediaFile/DownloadStream.
*/
public function getRaw($endpoint)
{
$token = $this->getToken();
// IMPORTANTE: usa /odata/ e NON /api/odata/
$url = "{$this->baseUrl}/odata/{$endpoint}";
/*
* Normal JSON OData calls use:
* {$this->baseUrl}/api/odata/...
*
* Media file downloads use:
* {$this->baseUrl}/api/MediaFile/DownloadStream...
*/
$url = rtrim($this->baseUrl, '/') . '/api/' . ltrim($endpoint, '/');
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer {$token}",
"Accept: */*"
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer {$token}",
"Accept: application/pdf,*/*"
],
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 60
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
$curlError = curl_error($ch);
curl_close($ch);
if ($response === false) {
throw new Exception("Errore cURL: " . $curl_error);
throw new Exception("Errore cURL download raw: " . $curlError);
}
if ($http_code !== 200) {
throw new Exception("HTTP {$http_code} su endpoint: " . $url);
if ($httpCode < 200 || $httpCode >= 300) {
throw new Exception(
"Errore HTTP {$httpCode} durante download raw. Content-Type: {$contentType}. Response: " .
substr($response, 0, 500)
);
}
if (empty($response)) {
throw new Exception("Risposta vuota dal download raw.");
}
return $response;
+46
View File
@@ -0,0 +1,46 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__FILE__) . '/class/VisualLimsApiClient.class.php';
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$idRapportoFile = isset($_GET['id_rapporto_file']) ? (int)$_GET['id_rapporto_file'] : 0;
if ($idRapportoFile <= 0) {
throw new Exception("Parametro id_rapporto_file mancante o non valido.");
}
/*
* This endpoint returns the PDF binary stream.
* Do not call this with the normal get() method because get() expects JSON.
*/
$endpoint = "MediaFile/DownloadStream?objectType=RapportoFile&propertyName=FileContent&objectKey={$idRapportoFile}";
$api = VisualLimsApiClient::getInstance();
$pdfContent = $api->getRaw($endpoint);
if (empty($pdfContent)) {
throw new Exception("PDF vuoto o non ricevuto dal server.");
}
$fileName = "rapporto_{$idRapportoFile}.pdf";
header('Content-Type: application/pdf');
header('Content-Disposition: inline; filename="' . $fileName . '"');
header('Content-Length: ' . strlen($pdfContent));
header('Cache-Control: private, max-age=0, must-revalidate');
header('Pragma: public');
echo $pdfContent;
exit;
} catch (Exception $e) {
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
@@ -0,0 +1,63 @@
<?php
require_once 'include/headscript.php';
header('Content-Type: application/json');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Invalid request method']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$template_id = isset($input['template_id']) ? (int)$input['template_id'] : 0;
$code = isset($input['code']) ? trim($input['code']) : '';
if ($template_id <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Missing or invalid template_id']);
exit;
}
if ($code === '') {
http_response_code(400);
echo json_encode(['error' => 'Missing code']);
exit;
}
/*
* TODO: Replace this block with your real API call.
* Expected response for import_json.php:
* {
* "success": true,
* "reference": "CODE123",
* "json": { ... real JSON payload ... }
* }
*/
$sampleJson = [
'data' => [[
'type' => 'trf_data_request',
'id' => $code,
'attributes' => [
'trf_type' => 'apparel',
'service_required' => 'regular',
'submitter_information' => [
'submitter_type' => 'supplier'
]
]
]]
];
echo json_encode([
'success' => true,
'reference' => $code,
'json' => $sampleJson
]);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
+143
View File
@@ -0,0 +1,143 @@
<?php
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once __DIR__ . '/class/VisualLimsApiClient.class.php';
header('Content-Type: application/json; charset=utf-8');
ini_set('display_errors', '0');
error_reporting(E_ALL);
try {
$api = VisualLimsApiClient::getInstance();
$idCliente = isset($_GET['id_cliente']) ? (int)$_GET['id_cliente'] : 0;
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 3;
$signedStatus = trim($_GET['signed_status'] ?? 'all');
if ($idCliente <= 0) {
throw new Exception("Parametro id_cliente mancante o non valido.");
}
/*
* Allowed limits only.
* This prevents risky wide queries on the live LIMS.
*/
$allowedLimits = [1, 3, 5, 10];
if (!in_array($limit, $allowedLimits, true)) {
$limit = 3;
}
/*
* Allowed signature filters.
*/
$allowedSignedStatuses = ['all', 'signed', 'not_signed'];
if (!in_array($signedStatus, $allowedSignedStatuses, true)) {
$signedStatus = 'all';
}
/*
* Base filter by customer.
* We already verified that Rapporto can expand Cliente and returns Cliente.IdCliente.
*/
$filters = [
"Cliente/IdCliente eq {$idCliente}"
];
if ($signedStatus === 'signed') {
$filters[] = "Firmato eq true";
}
if ($signedStatus === 'not_signed') {
$filters[] = "Firmato eq false";
}
$filter = implode(' and ', $filters);
/*
* Important:
* - $top limits the number of reports.
* - $orderby=Data desc gets the latest reports first.
* - $expand=RapportiFiles retrieves only the PDF file metadata, not the binary PDF.
*/
$params = [
'$filter' => $filter,
'$select' => 'IdRapporto,CodiceRapporto,Data,Versione,Firmato,DataStampa',
'$expand' => 'RapportiFiles',
'$orderby' => 'Data desc',
'$top' => $limit
];
$endpoint = "Rapporto?" . http_build_query($params);
file_put_contents(
__DIR__ . '/last_rapporti_cliente_endpoint.txt',
'[' . date('Y-m-d H:i:s') . '] ' . $endpoint . PHP_EOL,
FILE_APPEND
);
$data = $api->get($endpoint);
$items = $data['value'] ?? [];
if (!is_array($items)) {
$items = [];
}
$reports = [];
foreach ($items as $item) {
$rapportiFiles = $item['RapportiFiles'] ?? [];
$pdfFiles = [];
if (is_array($rapportiFiles)) {
foreach ($rapportiFiles as $file) {
$idRapportoFile = intval($file['IdRapportoFile'] ?? 0);
if ($idRapportoFile > 0) {
$pdfFiles[] = [
'id_rapporto_file' => $idRapportoFile,
'file_name' => $file['FileName'] ?? null,
'categoria' => $file['Categoria'] ?? null,
'tipo_rapporto' => $file['TipoRapporto'] ?? null,
'download_url' => "download_rapporto_pdf.php?id_rapporto_file={$idRapportoFile}"
];
}
}
}
$reports[] = [
'id_rapporto' => $item['IdRapporto'] ?? null,
'codice_rapporto' => $item['CodiceRapporto'] ?? null,
'data' => $item['Data'] ?? null,
'data_stampa' => $item['DataStampa'] ?? null,
'versione' => $item['Versione'] ?? null,
'firmato' => $item['Firmato'] ?? null,
'pdf_files' => $pdfFiles
];
}
echo json_encode([
'success' => true,
'id_cliente' => $idCliente,
'limit' => $limit,
'signed_status' => $signedStatus,
'endpoint' => $endpoint,
'count' => count($reports),
'reports' => $reports
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log.txt',
date('Y-m-d H:i:s') . ' - get_rapporti_cliente.php - ' . $e->getMessage() . PHP_EOL,
FILE_APPEND
);
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
+46 -7
View File
@@ -17,6 +17,7 @@ try {
// rapporto_by_codice_expand_step.php?codice=2541111&step=files_campioni
$codiceRapporto = trim($_GET['codice'] ?? '');
// Safe step mode: default is base, but allows controlled read-only steps
$step = trim($_GET['step'] ?? 'base');
if ($codiceRapporto === '') {
@@ -25,10 +26,9 @@ try {
$allowedSteps = [
'base' => '',
'files' => 'RapportiFiles',
'allegati' => 'RapportiAllegati',
'campioni' => 'CampioniDatiRapporto',
'files_campioni' => 'RapportiFiles,CampioniDatiRapporto'
'files' => 'RapportiFiles,Cliente',
'cliente' => 'Cliente'
];
if (!array_key_exists($step, $allowedSteps)) {
@@ -37,7 +37,8 @@ try {
// Escape OData per eventuali apostrofi
$codiceRapportoSafe = str_replace("'", "''", $codiceRapporto);
// Safe version of codice rapporto for filenames
$codiceRapportoFileSafe = preg_replace('/[^a-zA-Z0-9_-]/', '_', $codiceRapporto);
/*
* STEP 1 - Trova IdRapporto partendo da CodiceRapporto.
* Query leggera, con $select e $top=1.
@@ -107,15 +108,43 @@ try {
$detailData = $api->get($detailEndpoint);
$pdfFiles = [];
if ($step === 'files') {
$rapportiFiles = $detailData['RapportiFiles'] ?? [];
if (is_array($rapportiFiles)) {
foreach ($rapportiFiles as $file) {
$idRapportoFile = intval($file['IdRapportoFile'] ?? 0);
if ($idRapportoFile > 0) {
$pdfFiles[] = [
'id_rapporto_file' => $idRapportoFile,
'file_name' => $file['FileName'] ?? null,
'categoria' => $file['Categoria'] ?? null,
'tipo_rapporto' => $file['TipoRapporto'] ?? null,
'download_endpoint' => "MediaFile/DownloadStream?objectType=RapportoFile&propertyName=FileContent&objectKey={$idRapportoFile}"
];
}
}
}
}
$clienteData = null;
if ($step === 'cliente' || $step === 'files') {
$clienteData = $detailData['Cliente'] ?? null;
}
file_put_contents(
__DIR__ . "/rapporto_codice_{$codiceRapportoSafe}_{$step}.json",
__DIR__ . "/rapporto_codice_{$codiceRapportoFileSafe}_{$step}.json",
json_encode([
'search' => $searchData,
'detail' => $detailData
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
);
echo json_encode([
$response = [
'success' => true,
'codice_rapporto' => $codiceRapporto,
'id_rapporto' => $rapportoId,
@@ -124,7 +153,17 @@ try {
'detail_endpoint' => $detailEndpoint,
'rapporto_base' => $rapportoBase,
'data' => $detailData
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
];
if ($step === 'files') {
$response['pdf_files'] = $pdfFiles;
}
if ($step === 'cliente' || $step === 'files') {
$response['cliente'] = $clienteData;
}
echo json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
file_put_contents(
__DIR__ . '/error_log.txt',
+15 -4
View File
@@ -237,7 +237,18 @@
const iconClass = getTemplateIcon(sourceType);
const btn = document.createElement("a");
btn.href = `import_xls2.php?id=${template.id}`;
// Redirect based on template source type
if (sourceType === 'XLS') {
btn.href = `import_xls2.php?id=${template.id}`;
} else if (sourceType === 'API' || sourceType === 'JSON') {
btn.href = `import_json.php?id=${template.id}`;
} else if (sourceType === 'PDF') {
btn.href = `import_pdf.php?id=${template.id}`;
} else {
btn.href = `import_xls2.php?id=${template.id}`;
}
btn.className = `btn ${sizeClass}`;
btn.style.backgroundColor = template.button_bg_color || '#0d6efd';
btn.style.color = template.button_text_color || '#ffffff';
@@ -245,9 +256,9 @@
btn.setAttribute("data-source-type", sourceType);
btn.innerHTML = `
<i class="${iconClass} template-icon"></i>
<span>${escapeHtml(template.button_label || 'Unnamed')}</span>
`;
<i class="${iconClass} template-icon"></i>
<span>${escapeHtml(template.button_label || 'Unnamed')}</span>
`;
return btn;
}
+95 -13
View File
@@ -25,6 +25,7 @@ $rows = json_decode(urldecode($_POST['rows'] ?? '[]'), true);
$excelrows = json_decode(urldecode($_POST['excelrows'] ?? '[]'), true);
$newFilename = $_POST['filename'];
$source_type = strtolower(trim($_POST['source_type'] ?? 'xls'));
$_SESSION['template_id'] = $template_id;
$_SESSION['selected_rows'] = $selected_rows;
@@ -37,6 +38,7 @@ error_log("Received Data - Template ID: $template_id, Selected Rows: " . json_en
error_log("Columns: " . json_encode($columns));
error_log("Rows: " . json_encode($rows));
error_log("Excelrows: " . json_encode($excelrows));
error_log("Source type: " . $source_type);
$user_id = $iduserlogin ?? 1;
@@ -47,7 +49,22 @@ $pdo = $db->getConnection();
$importReferenceCode = date('YmdHis') . '-' . uniqid();
// Recupera tutti i mapping dal template
$stmt = $pdo->prepare("SELECT id, excel_column, data_type, is_required, manual_default, is_manual, field_label, field_id, main_field, auto_value FROM template_mapping WHERE template_id = ?");
$stmt = $pdo->prepare("
SELECT
id,
excel_column,
json_node,
data_type,
is_required,
manual_default,
is_manual,
field_label,
field_id,
main_field,
auto_value
FROM template_mapping
WHERE template_id = ?
");
$stmt->execute([$template_id]);
$allMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -67,12 +84,20 @@ foreach ($allMappings as $mapping) {
// Inserisci le righe selezionate in datadb
$insertedIds = [];
foreach ($selected_rows as $rowIndex) {
$row = $rows[$rowIndex] ?? null;
$excelrow = $excelrows[$rowIndex] ?? null;
foreach ($selected_rows as $loopIndex => $rowIndex) {
if ($source_type === 'json') {
// JSON import sends only selected rows in rows/excelrows
$row = $rows[$loopIndex] ?? null;
$excelrow = $excelrows[$loopIndex] ?? ('JSON-' . ($loopIndex + 1));
} else {
// XLS import keeps original row indexes
$row = $rows[$rowIndex] ?? null;
$excelrow = $excelrows[$rowIndex] ?? null;
}
if ($row === null || $excelrow === null) {
error_log("Errore: riga o excelrow mancante per rowIndex $rowIndex");
error_log("Errore: riga o excelrow mancante. Source type: $source_type, loopIndex: $loopIndex, rowIndex: $rowIndex");
continue;
}
@@ -104,14 +129,72 @@ foreach ($selected_rows as $rowIndex) {
foreach ($allMappings as $mapping) {
$fieldValue = null;
if (!$mapping['is_manual']) {
$excelColumn = trim($mapping['excel_column']);
$excelColumnIndex = array_search($excelColumn, array_map('trim', $columns));
if ($excelColumnIndex !== false && isset($row[$excelColumnIndex]) && $row[$excelColumnIndex] !== '') {
$fieldValue = $row[$excelColumnIndex];
error_log("Found Excel column '$excelColumn' at index $excelColumnIndex, value: " . var_export($fieldValue, true));
$sourceColumn = '';
if ($source_type === 'json') {
$sourceColumn = trim($mapping['json_node'] ?? '');
} else {
$sourceColumn = trim($mapping['excel_column'] ?? '');
}
// Fallback: if JSON node is empty, try excel_column
if ($sourceColumn === '') {
$sourceColumn = trim($mapping['excel_column'] ?? '');
}
$columnsTrimmed = array_map('trim', $columns);
$candidateColumns = [];
if ($sourceColumn !== '') {
$candidateColumns[] = $sourceColumn;
if ($source_type === 'json') {
// Common JSON path variants
$candidateColumns[] = preg_replace('/^data\[\]\./', '', $sourceColumn);
$candidateColumns[] = preg_replace('/^data\.0\./', '', $sourceColumn);
$candidateColumns[] = str_replace('data[].', 'data.0.', $sourceColumn);
$candidateColumns[] = str_replace('data.0.', 'data[].', $sourceColumn);
}
}
// Remove empty and duplicate candidates
$candidateColumns = array_values(array_unique(array_filter($candidateColumns, function ($value) {
return trim((string)$value) !== '';
})));
$sourceColumnIndex = false;
$matchedColumn = '';
foreach ($candidateColumns as $candidateColumn) {
$candidateColumn = trim($candidateColumn);
$index = array_search($candidateColumn, $columnsTrimmed);
if ($index !== false) {
$sourceColumnIndex = $index;
$matchedColumn = $candidateColumn;
break;
}
}
if ($sourceColumnIndex !== false && isset($row[$sourceColumnIndex]) && $row[$sourceColumnIndex] !== '') {
$fieldValue = $row[$sourceColumnIndex];
error_log(
"Found source column. Original: '$sourceColumn', Matched: '$matchedColumn', Index: $sourceColumnIndex, Value: " .
var_export($fieldValue, true)
);
} else {
$fieldValue = $mapping['manual_default'] ?? '';
error_log("Excel column '$excelColumn' not found or empty, using default: " . var_export($fieldValue, true));
error_log(
"Source column not found or empty. Original: '$sourceColumn'. Candidates: " .
json_encode($candidateColumns) .
". Available columns: " .
json_encode($columnsTrimmed) .
". Using default: " .
var_export($fieldValue, true)
);
}
switch ($mapping['data_type']) {
case 'INT':
@@ -147,7 +230,7 @@ foreach ($selected_rows as $rowIndex) {
if ($mapping['is_required'] && (is_null($fieldValue) || $fieldValue === '')) {
error_log("Required field missing for mapping ID: " . $mapping['id'] . ", field: " . $mapping['field_label']);
}
error_log("Inserting into import_data_details - Mapping ID: " . $mapping['id'] . ", Field Value: " . var_export($fieldValue, true) . ", Is Manual: " . $mapping['is_manual'] . ", Excel Column: " . ($mapping['excel_column'] ?? 'N/A') . ", Manual Default: " . ($mapping['manual_default'] ?? 'N/A'));
error_log("Inserting into import_data_details - Mapping ID: " . $mapping['id'] . ", Field Value: " . var_export($fieldValue, true) . ", Is Manual: " . $mapping['is_manual'] . ", Source Column: " . ($sourceColumn ?? 'N/A') . ", Source Type: " . $source_type . ", Manual Default: " . ($mapping['manual_default'] ?? 'N/A'));
$stmt = $pdo->prepare("INSERT INTO import_data_details (id, mapping_id, field_value) VALUES (?, ?, ?)");
$stmt->execute([$iddatadb, $mapping['id'], $fieldValue]);
error_log("Inserted into import_data_details for ID $iddatadb, Mapping ID: " . $mapping['id'] . ", Field Value: " . var_export($fieldValue, true));
@@ -158,4 +241,3 @@ $_SESSION['inserted_ids'] = $insertedIds;
header("Location: imported.php?id=" . urlencode($template_id) . "&importref=" . urlencode($importReferenceCode));
exit;
?>
+819
View File
@@ -0,0 +1,819 @@
<?php
include('include/headscript.php');
// Check if a valid template ID has been provided
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
header("Location: xlstemplates_grid.php?status=error&message=" . urlencode("Invalid ID"));
exit;
}
$id = intval($_GET['id']);
// Load template
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("SELECT * FROM excel_templates WHERE id = ?");
$stmt->execute([$id]);
$template = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$template) {
header("Location: template_dashboard.php?status=error&message=" . urlencode("Template not found"));
exit;
}
// Check mappings
$stmt = $pdo->prepare("SELECT id FROM template_mapping WHERE template_id = ?");
$stmt->execute([$id]);
$hasMappings = $stmt->fetch(PDO::FETCH_ASSOC);
error_log("Loaded JSON import template: " . print_r($template, true));
?>
<!doctype html>
<html lang="en">
<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'); ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.top-scrollbar {
overflow-x: auto;
overflow-y: hidden;
width: 100%;
height: 18px;
margin-bottom: 8px;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
background: #f8f9fa;
display: none;
}
.top-scrollbar-inner {
height: 1px;
}
.table-container {
overflow-x: auto;
max-width: 100%;
margin-bottom: 20px;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 10px;
text-align: left;
border: 1px solid #dee2e6;
min-width: 120px;
max-width: 260px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
.table th:first-child,
.table td:first-child {
min-width: 50px;
max-width: 50px;
}
.table th {
background-color: #f8f9fa;
position: relative;
cursor: col-resize;
user-select: none;
}
.table th .resize-handle {
position: absolute;
top: 0;
right: 0;
width: 5px;
height: 100%;
cursor: col-resize;
background: transparent;
}
.table th .resize-handle:hover {
background: #007bff;
}
.loader {
display: none;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 10px auto;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.column-filters th {
background: #ffffff;
cursor: default;
}
.column-filters input {
width: 100%;
min-width: 80px;
}
.json-code-input {
font-size: 1.15rem;
min-height: 48px;
}
.json-paste-area {
min-height: 260px;
font-family: Consolas, Monaco, monospace;
font-size: 13px;
}
.json-help-box {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 12px 14px;
font-size: 13px;
}
.source-badge {
font-size: 12px;
}
</style>
<title><?= htmlspecialchars($template['name']) ?> - JSON Import - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
</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="mb-3 text">
<a href="imported.php?id=<?= $id ?>" class="btn btn-warning me-2">Imported (i)</a>
<a href="tolims.php?id=<?= $id ?>" class="btn btn-success">To LIMS (l)</a>
</div>
<div class="card radius-10">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<div>
<h6 class="mb-0"><?= htmlspecialchars($template['name']) ?> - JSON Import</h6>
<small>
Template ID: <?= $id ?>
</small>
</div>
<span class="badge bg-info text-dark">JSON mode</span>
</div>
</div>
<div class="card-body">
<?php if (!$hasMappings): ?>
<div class="alert alert-warning" role="alert">
Nessun mapping trovato per questo template. Configura i mapping prima di procedere.
</div>
<?php endif; ?>
<div class="json-help-box mb-3">
<strong>Flusso:</strong> inserisci/scansiona un codice per recuperare il JSON da API, oppure incolla manualmente un JSON nel secondo tab.
Ogni JSON aggiunto diventa una riga della tabella di preview. Quando hai finito, seleziona le righe e clicca <strong>Prosegui</strong>.
</div>
<ul class="nav nav-tabs" id="jsonImportTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="api-code-tab" data-bs-toggle="tab" data-bs-target="#api-code-pane" type="button" role="tab" aria-controls="api-code-pane" aria-selected="true">
Code / Barcode
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="paste-json-tab" data-bs-toggle="tab" data-bs-target="#paste-json-pane" type="button" role="tab" aria-controls="paste-json-pane" aria-selected="false">
Paste JSON
</button>
</li>
</ul>
<div class="tab-content border border-top-0 p-3 mb-3" id="jsonImportTabsContent">
<div class="tab-pane fade show active" id="api-code-pane" role="tabpanel" aria-labelledby="api-code-tab" tabindex="0">
<form id="jsonCodeForm" class="row g-3 align-items-end">
<div class="col-lg-8">
<label for="json_code" class="form-label">Code / Barcode</label>
<input type="text" class="form-control json-code-input" id="json_code" name="json_code" placeholder="Write or scan code" autocomplete="off" <?= !$hasMappings ? 'disabled' : '' ?>>
<small class="text-muted">Lo scanner barcode normalmente scrive qui il codice e invia Enter.</small>
</div>
<div class="col-lg-4 d-flex gap-2">
<button type="submit" class="btn btn-primary flex-fill" <?= !$hasMappings ? 'disabled' : '' ?>>Load JSON</button>
<button type="button" class="btn btn-outline-secondary" id="clearCodeBtn">Clear</button>
</div>
</form>
</div>
<div class="tab-pane fade" id="paste-json-pane" role="tabpanel" aria-labelledby="paste-json-tab" tabindex="0">
<form id="pasteJsonForm">
<div class="mb-3">
<label for="manual_json_reference" class="form-label">Reference / filename</label>
<input type="text" class="form-control" id="manual_json_reference" placeholder="Optional reference, e.g. manual-json-001">
</div>
<div class="mb-3">
<label for="manual_json" class="form-label">Paste JSON</label>
<textarea class="form-control json-paste-area" id="manual_json" placeholder='{"data":[{"id":"MM000620","attributes":{"trf_type":"apparel"}}]}' <?= !$hasMappings ? 'disabled' : '' ?>></textarea>
</div>
<button type="submit" class="btn btn-primary" <?= !$hasMappings ? 'disabled' : '' ?>>Add pasted JSON</button>
<button type="button" class="btn btn-outline-secondary" id="clearManualJsonBtn">Clear</button>
</form>
</div>
</div>
<div class="loader" id="loader"></div>
<div id="errorContainer" class="alert alert-danger mt-3" style="display:none;"></div>
<div id="successContainer" class="alert alert-success mt-3" style="display:none;"></div>
<div id="tableContainer"></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>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.min.js"></script>
<?php include('jsinclude.php'); ?>
<script>
document.addEventListener("DOMContentLoaded", function() {
const TEMPLATE_ID = <?= (int)$id ?>;
const API_ENDPOINT = 'get_json_by_code.php';
const INCLUDE_SOURCE_CODE_COLUMN = true;
const UNWRAP_SINGLE_DATA_ITEM = true;
const jsonCodeForm = document.getElementById('jsonCodeForm');
const pasteJsonForm = document.getElementById('pasteJsonForm');
const jsonCodeInput = document.getElementById('json_code');
const manualJsonInput = document.getElementById('manual_json');
const manualJsonReferenceInput = document.getElementById('manual_json_reference');
const clearCodeBtn = document.getElementById('clearCodeBtn');
const clearManualJsonBtn = document.getElementById('clearManualJsonBtn');
const loader = document.getElementById('loader');
const errorContainer = document.getElementById('errorContainer');
const successContainer = document.getElementById('successContainer');
const tableContainer = document.getElementById('tableContainer');
let jsonRows = [];
let columns = [];
if (jsonCodeInput && !jsonCodeInput.disabled) {
jsonCodeInput.focus();
}
jsonCodeForm.addEventListener('submit', function(e) {
e.preventDefault();
const code = jsonCodeInput.value.trim();
if (!code) {
showError('Inserisci o scansiona un codice.');
return;
}
loadJsonFromApi(code);
});
pasteJsonForm.addEventListener('submit', function(e) {
e.preventDefault();
const rawJson = manualJsonInput.value.trim();
const reference = manualJsonReferenceInput.value.trim() || 'manual-json-' + (jsonRows.length + 1);
if (!rawJson) {
showError('Incolla un JSON prima di aggiungerlo.');
return;
}
try {
const parsedJson = JSON.parse(rawJson);
addJsonRow(parsedJson, reference, 'paste');
manualJsonInput.value = '';
manualJsonReferenceInput.value = '';
showSuccess('JSON incollato aggiunto correttamente.');
} catch (err) {
showError('JSON non valido: ' + err.message);
}
});
clearCodeBtn.addEventListener('click', function() {
jsonCodeInput.value = '';
jsonCodeInput.focus();
});
clearManualJsonBtn.addEventListener('click', function() {
manualJsonInput.value = '';
manualJsonReferenceInput.value = '';
});
function loadJsonFromApi(code) {
hideMessages();
loader.style.display = 'block';
fetch(API_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
template_id: TEMPLATE_ID,
code: code
})
})
.then(response => {
if (!response.ok) {
throw new Error('HTTP status ' + response.status);
}
return response.json();
})
.then(responseData => {
loader.style.display = 'none';
if (responseData.error) {
showError(responseData.error);
return;
}
let jsonPayload = responseData;
let reference = code;
// Supported endpoint response format:
// { success: true, json: {...}, reference: "..." }
if (responseData.json !== undefined) {
jsonPayload = responseData.json;
reference = responseData.reference || responseData.filename || responseData.code || code;
}
addJsonRow(jsonPayload, reference, 'api');
jsonCodeInput.value = '';
jsonCodeInput.focus();
showSuccess('JSON recuperato e aggiunto correttamente.');
})
.catch(error => {
loader.style.display = 'none';
showError('Errore durante il recupero del JSON: ' + error.message);
});
}
function addJsonRow(jsonPayload, reference, sourceType) {
const normalizedPayload = normalizeJsonPayload(jsonPayload);
const flattened = flattenJson(normalizedPayload);
if (INCLUDE_SOURCE_CODE_COLUMN) {
flattened.source_code = reference;
flattened.source_type = sourceType;
}
const newColumns = Object.keys(flattened).filter(col => !columns.includes(col));
columns = columns.concat(newColumns);
jsonRows.push({
excelrow: 'JSON-' + (jsonRows.length + 1),
reference: reference,
sourceType: sourceType,
flat: flattened
});
renderTable();
}
function normalizeJsonPayload(payload) {
if (
UNWRAP_SINGLE_DATA_ITEM &&
payload &&
typeof payload === 'object' &&
!Array.isArray(payload) &&
Array.isArray(payload.data) &&
payload.data.length === 1 &&
payload.data[0] &&
typeof payload.data[0] === 'object'
) {
return payload.data[0];
}
return payload;
}
function flattenJson(value, prefix = '', result = {}) {
if (value === null || value === undefined) {
result[prefix || 'value'] = '';
return result;
}
if (Array.isArray(value)) {
if (value.length === 0) {
result[prefix || 'value'] = '';
return result;
}
value.forEach((item, index) => {
const newPrefix = prefix ? prefix + '.' + index : String(index);
flattenJson(item, newPrefix, result);
});
return result;
}
if (typeof value === 'object') {
const keys = Object.keys(value);
if (keys.length === 0) {
result[prefix || 'value'] = '';
return result;
}
keys.forEach(key => {
const newPrefix = prefix ? prefix + '.' + key : key;
flattenJson(value[key], newPrefix, result);
});
return result;
}
result[prefix || 'value'] = String(value);
return result;
}
function buildImportData() {
const rows = jsonRows.map(row => columns.map(col => row.flat[col] ?? ''));
const excelData = rows.map((rowData, index) => ({
excelrow: jsonRows[index].excelrow,
data: rowData
}));
return {
template_id: TEMPLATE_ID,
columns: columns,
rows: rows,
excel_data: excelData,
filename: 'json_import_' + new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
};
}
function renderTable() {
if (jsonRows.length === 0) {
tableContainer.innerHTML = '';
return;
}
const data = buildImportData();
let html = `
<form id="selectRowsForm" action="import_insert.php" method="POST">
<input type="hidden" name="template_id" value="${escapeHtml(data.template_id)}">
<input type="hidden" name="columns" value="${encodeURIComponent(JSON.stringify(data.columns))}">
<input type="hidden" name="rows" id="selectedRowsData" value="">
<input type="hidden" name="excelrows" id="selectedExcelRowsData" value="">
<input type="hidden" name="filename" value="${escapeHtml(data.filename)}">
<input type="hidden" name="source_type" value="json">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
<div>
<strong>JSON rows loaded:</strong> ${jsonRows.length}
<span class="badge bg-secondary source-badge ms-2">Columns: ${data.columns.length}</span>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-danger btn-sm" id="clearAllRowsBtn">Clear all rows</button>
<button type="submit" class="btn btn-primary" id="proceedButtonTop" disabled>Prosegui</button>
</div>
</div>
<div class="top-scrollbar" id="topTableScrollbar">
<div class="top-scrollbar-inner" id="topTableScrollbarInner"></div>
</div>
<div class="table-container" id="mainTableContainer">
<table class="table table-striped table-bordered" id="importPreviewTable">
<thead>
<tr>
<th><input type="checkbox" id="selectAll"> Select</th>
${data.columns.map(col => `<th title="${escapeHtml(col)}">${escapeHtml(readableColumnLabel(col))}<div class="resize-handle"></div></th>`).join('')}
<th>Action</th>
</tr>
<tr class="column-filters">
<th></th>
${data.columns.map((col, i) => `
<th>
<input type="text"
class="form-control form-control-sm column-filter"
data-col-index="${i}"
placeholder="Filter...">
</th>
`).join('')}
<th></th>
</tr>
</thead>
<tbody>
${data.excel_data.map((row, index) => `
<tr data-row-index="${index}">
<td><input type="checkbox" class="row-checkbox" name="selected_rows[]" value="${index}" data-excelrow="${escapeHtml(row.excelrow)}"></td>
${row.data.map(cell => `<td title="${escapeHtml(cell)}">${escapeHtml(cell)}</td>`).join('')}
<td><button type="button" class="btn btn-sm btn-outline-danger remove-row-btn" data-row-index="${index}">Remove</button></td>
</tr>
`).join('')}
</tbody>
</table>
</div>
<button type="submit" class="btn btn-primary mt-3" id="proceedButtonBottom" disabled>Prosegui</button>
</form>
`;
tableContainer.innerHTML = html;
bindTableEvents(data);
}
function bindTableEvents(data) {
const selectRowsForm = document.getElementById('selectRowsForm');
const clearAllRowsBtn = document.getElementById('clearAllRowsBtn');
const removeRowButtons = document.querySelectorAll('.remove-row-btn');
selectRowsForm.addEventListener('submit', function(e) {
const checkedBoxes = Array.from(document.querySelectorAll('.row-checkbox:checked'));
if (checkedBoxes.length === 0) {
e.preventDefault();
alert('Seleziona almeno una riga.');
return;
}
const selectedRows = [];
const selectedExcelRows = [];
checkedBoxes.forEach((cb, newIndex) => {
const originalIndex = parseInt(cb.value, 10);
if (data.rows && data.rows[originalIndex]) {
selectedRows.push(data.rows[originalIndex]);
}
if (data.excel_data && data.excel_data[originalIndex]) {
selectedExcelRows.push(data.excel_data[originalIndex].excelrow);
}
// Reindex selected_rows so import_insert.php receives only the reduced rows array
cb.value = newIndex;
});
document.getElementById('selectedRowsData').value =
encodeURIComponent(JSON.stringify(selectedRows));
document.getElementById('selectedExcelRowsData').value =
encodeURIComponent(JSON.stringify(selectedExcelRows));
});
clearAllRowsBtn.addEventListener('click', function() {
if (!confirm('Vuoi rimuovere tutte le righe JSON caricate?')) return;
jsonRows = [];
columns = [];
renderTable();
showSuccess('Righe JSON rimosse.');
jsonCodeInput.focus();
});
removeRowButtons.forEach(button => {
button.addEventListener('click', function() {
const rowIndex = parseInt(this.dataset.rowIndex, 10);
jsonRows.splice(rowIndex, 1);
rebuildColumnsFromRows();
renderTable();
});
});
const topTableScrollbar = document.getElementById('topTableScrollbar');
const topTableScrollbarInner = document.getElementById('topTableScrollbarInner');
const mainTableContainer = document.getElementById('mainTableContainer');
const importPreviewTable = document.getElementById('importPreviewTable');
function updateTopTableScrollbar() {
if (!topTableScrollbar || !topTableScrollbarInner || !mainTableContainer || !importPreviewTable) return;
topTableScrollbarInner.style.width = importPreviewTable.scrollWidth + 'px';
if (mainTableContainer.scrollWidth > mainTableContainer.clientWidth) {
topTableScrollbar.style.display = 'block';
} else {
topTableScrollbar.style.display = 'none';
}
}
let syncingTop = false;
let syncingBottom = false;
if (topTableScrollbar && mainTableContainer) {
topTableScrollbar.addEventListener('scroll', function() {
if (syncingBottom) return;
syncingTop = true;
mainTableContainer.scrollLeft = topTableScrollbar.scrollLeft;
syncingTop = false;
});
mainTableContainer.addEventListener('scroll', function() {
if (syncingTop) return;
syncingBottom = true;
topTableScrollbar.scrollLeft = mainTableContainer.scrollLeft;
syncingBottom = false;
});
}
updateTopTableScrollbar();
setTimeout(updateTopTableScrollbar, 100);
setTimeout(updateTopTableScrollbar, 300);
window.addEventListener('resize', updateTopTableScrollbar);
const proceedButtonTop = document.getElementById('proceedButtonTop');
const proceedButtonBottom = document.getElementById('proceedButtonBottom');
const selectAllCheckbox = document.getElementById('selectAll');
const checkboxes = document.querySelectorAll('.row-checkbox');
function updateProceedButton() {
const enabled = Array.from(document.querySelectorAll('.row-checkbox')).some(cb => cb.checked);
if (proceedButtonTop) proceedButtonTop.disabled = !enabled;
if (proceedButtonBottom) proceedButtonBottom.disabled = !enabled;
}
selectAllCheckbox.addEventListener('change', function() {
const visibleRows = Array.from(document.querySelectorAll('#importPreviewTable tbody tr'))
.filter(row => row.style.display !== 'none');
visibleRows.forEach(row => {
const checkbox = row.querySelector('.row-checkbox');
if (checkbox) {
checkbox.checked = this.checked;
}
});
updateProceedButton();
});
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
const visibleCheckboxes = Array.from(document.querySelectorAll('#importPreviewTable tbody tr'))
.filter(row => row.style.display !== 'none')
.map(row => row.querySelector('.row-checkbox'))
.filter(cb => cb !== null);
selectAllCheckbox.checked =
visibleCheckboxes.length > 0 &&
visibleCheckboxes.every(cb => cb.checked);
updateProceedButton();
});
});
const thElements = document.querySelectorAll('#importPreviewTable th');
thElements.forEach((th, index) => {
if (index === 0) return;
const resizeHandle = th.querySelector('.resize-handle');
if (resizeHandle) {
resizeHandle.addEventListener('mousedown', (e) => {
e.preventDefault();
const startX = e.clientX;
const startWidth = th.offsetWidth;
const onMouseMove = (e) => {
const newWidth = Math.max(60, startWidth + (e.clientX - startX));
th.style.width = `${newWidth}px`;
th.style.minWidth = `${newWidth}px`;
th.style.maxWidth = `${newWidth}px`;
const cells = document.querySelectorAll(`#importPreviewTable td:nth-child(${index + 1})`);
cells.forEach(cell => {
cell.style.width = `${newWidth}px`;
cell.style.minWidth = `${newWidth}px`;
cell.style.maxWidth = `${newWidth}px`;
});
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
}
});
const rows = document.querySelectorAll('#importPreviewTable tbody tr');
const filterInputs = document.querySelectorAll('.column-filter');
const activeFilters = {};
function applyColumnFilters() {
rows.forEach(row => {
let visible = true;
for (const [colIndexStr, filterValue] of Object.entries(activeFilters)) {
const colIndex = parseInt(colIndexStr, 10);
const cell = row.cells[colIndex + 1];
const cellText = (cell?.textContent || '').toLowerCase();
const searchText = (filterValue || '').toLowerCase().trim();
if (searchText && !cellText.includes(searchText)) {
visible = false;
break;
}
}
row.style.display = visible ? '' : 'none';
});
const visibleCheckboxes = Array.from(document.querySelectorAll('#importPreviewTable tbody tr'))
.filter(row => row.style.display !== 'none')
.map(row => row.querySelector('.row-checkbox'))
.filter(cb => cb !== null);
selectAllCheckbox.checked =
visibleCheckboxes.length > 0 &&
visibleCheckboxes.every(cb => cb.checked);
updateProceedButton();
}
filterInputs.forEach(input => {
input.addEventListener('input', function() {
const colIndex = this.dataset.colIndex;
activeFilters[colIndex] = this.value;
applyColumnFilters();
});
});
updateProceedButton();
}
function rebuildColumnsFromRows() {
columns = [];
jsonRows.forEach(row => {
Object.keys(row.flat).forEach(col => {
if (!columns.includes(col)) {
columns.push(col);
}
});
});
}
function readableColumnLabel(columnName) {
if (!columnName) return 'Column without name';
return columnName;
}
function showError(message) {
successContainer.style.display = 'none';
errorContainer.textContent = message;
errorContainer.style.display = 'block';
}
function showSuccess(message) {
errorContainer.style.display = 'none';
successContainer.textContent = message;
successContainer.style.display = 'block';
setTimeout(() => {
successContainer.style.display = 'none';
}, 3500);
}
function hideMessages() {
errorContainer.style.display = 'none';
successContainer.style.display = 'none';
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
});
</script>
</body>
</html>
+9 -3
View File
@@ -22,7 +22,7 @@
<ul>
<!-- <li> <a href="index.php"><i class='bx bx-radio-circle'></i>Default</a>
</li> -->
<li> <a href="import_dashboard.php"><i class='bx bx-radio-circle'></i>XLS Import</a>
<li> <a href="import_dashboard.php"><i class='bx bx-radio-circle'></i>Import AREA</a>
</li>
@@ -57,8 +57,14 @@
</li>
<li class="menu-label">Reports</li>
<li>
<a href="rapporti_cliente_lookup.php" target="">
<div class="parent-icon"><i class="bx bx-file-find"></i>
</div>
<div class="menu-title">Ricerca Reports</div>
</a>
</li>
<li class="menu-label">Others</li>
+334 -54
View File
@@ -87,10 +87,19 @@ $mappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Recupera i fixed fields dalla tabella template_fixed_mapping
$stmt = $pdo->prepare("
SELECT id, fixed_field_key, is_manual, data_type, is_required, default_value, is_visible_import
FROM template_fixed_mapping
WHERE template_id = ?
ORDER BY id ASC
SELECT
id,
fixed_field_key,
is_manual,
data_type,
is_required,
default_value,
default_source,
json_node,
is_visible_import
FROM template_fixed_mapping
WHERE template_id = ?
ORDER BY id ASC
");
$stmt->execute([$id]);
$fixedMappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -447,19 +456,18 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
<td>
<?php
$isSceltaMultipla = ($mapping['data_type'] === 'SceltaMultipla');
$isApiSceltaMultipla = ($sourceType === 'API' && $isSceltaMultipla);
$autoValue = $mapping['auto_value'] ?? 'none';
$hasAuto = ($autoValue && $autoValue !== 'none');
if ($isSceltaMultipla) {
$mappingValue = 'manual';
} elseif ($hasAuto) {
$mappingValue = 'auto';
} elseif ($sourceType === 'XLS' && !empty($mapping['excel_column'])) {
$mappingValue = 'xls';
} elseif ($sourceType === 'API' && !empty($mapping['json_node'])) {
if ($sourceType === 'API' && !empty($mapping['json_node'])) {
$mappingValue = 'json';
} elseif ((int)$mapping['is_manual'] === 1) {
} elseif ($sourceType === 'XLS' && !$isSceltaMultipla && !empty($mapping['excel_column'])) {
$mappingValue = 'xls';
} elseif (!$isSceltaMultipla && $hasAuto) {
$mappingValue = 'auto';
} elseif ((int)$mapping['is_manual'] === 1 || $isSceltaMultipla) {
$mappingValue = 'manual';
} else {
$mappingValue = '';
@@ -469,17 +477,18 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
<select class="form-select mapping-select"
data-id="<?php echo (int)$mapping['id']; ?>"
data-field-id="<?php echo (int)$mapping['field_id']; ?>"
<?php echo $isSceltaMultipla ? 'disabled' : ''; ?>>
data-is-scelta-multipla="<?php echo $isSceltaMultipla ? '1' : '0'; ?>"
<?php echo ($isSceltaMultipla && $sourceType !== 'API') ? 'disabled' : ''; ?>>
<option value="">Select Option</option>
<?php if ($sourceType === 'XLS' && !$isSceltaMultipla): ?>
<option value="xls" <?php echo ($mappingValue === 'xls') ? 'selected' : ''; ?>>Map to XLS Column</option>
<?php elseif ($sourceType === 'API'): ?>
<option value="json" <?php echo ($mappingValue === 'json') ? 'selected' : ''; ?>>Map to JSON Node</option>
<?php endif; ?>
<?php if (!$isSceltaMultipla): ?>
<option value="">Select Option</option>
<?php if ($sourceType === 'XLS'): ?>
<option value="xls" <?php echo ($mappingValue === 'xls') ? 'selected' : ''; ?>>Map to XLS Column</option>
<?php elseif ($sourceType === 'API'): ?>
<option value="json" <?php echo ($mappingValue === 'json') ? 'selected' : ''; ?>>Map to JSON Node</option>
<?php endif; ?>
<option value="auto" <?php echo ($mappingValue === 'auto') ? 'selected' : ''; ?>>Auto value</option>
<?php endif; ?>
@@ -615,39 +624,68 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
<td><?php echo htmlspecialchars($fm['data_type']); ?></td>
<td>
<?php
$fixedDefaultSource = $fm['default_source'] ?? 'manual';
$fixedJsonNode = $fm['json_node'] ?? '';
$showFixedManual = ($fixedDefaultSource !== 'json');
$showFixedJson = ($fixedDefaultSource === 'json');
?>
<?php if ($fm['data_type'] === 'DATE'): ?>
<input type="date"
class="form-control fixed-default-input"
<?php if ($sourceType === 'API'): ?>
<select class="form-select fixed-source-select"
data-fixed-id="<?php echo (int)$fm['id']; ?>"
value="<?php echo htmlspecialchars($fm['default_value'] ?? ''); ?>">
<?php elseif (in_array($fm['fixed_field_key'], [
'ClienteResponsabile',
'ClienteFornitore',
'ClienteAnalisi',
'MoltiplicatorePrezzo',
'AnagraficaCertestObject',
'AnagraficaCertestService'
])): ?>
<select class="form-select fixed-default-select"
data-fixed-id="<?php echo (int)$fm['id']; ?>"
data-fixed-key="<?php echo htmlspecialchars($fm['fixed_field_key']); ?>"
data-current-value="<?php echo htmlspecialchars($fm['default_value'] ?? ''); ?>">
<option value="">Loading...</option>
style="margin-bottom:6px;">
<option value="manual" <?php echo ($fixedDefaultSource === 'manual') ? 'selected' : ''; ?>>
Manual value
</option>
<option value="json" <?php echo ($fixedDefaultSource === 'json') ? 'selected' : ''; ?>>
Map to JSON Node
</option>
</select>
<?php else: ?>
<input type="text"
class="form-control fixed-default-input"
<select class="form-select fixed-json-node-select"
data-fixed-id="<?php echo (int)$fm['id']; ?>"
value="<?php echo htmlspecialchars($fm['default_value'] ?? ''); ?>">
data-current-json="<?php echo htmlspecialchars($fixedJsonNode, ENT_QUOTES, 'UTF-8'); ?>"
style="display:<?php echo $showFixedJson ? 'block' : 'none'; ?>; margin-bottom:6px;">
</select>
<?php endif; ?>
<div class="fixed-manual-wrapper"
style="display:<?php echo $showFixedManual ? 'block' : 'none'; ?>;">
<?php if ($fm['data_type'] === 'DATE'): ?>
<input type="date"
class="form-control fixed-default-input"
data-fixed-id="<?php echo (int)$fm['id']; ?>"
value="<?php echo htmlspecialchars($fm['default_value'] ?? ''); ?>">
<?php elseif (in_array($fm['fixed_field_key'], [
'ClienteResponsabile',
'ClienteFornitore',
'ClienteAnalisi',
'MoltiplicatorePrezzo',
'AnagraficaCertestObject',
'AnagraficaCertestService'
])): ?>
<select class="form-select fixed-default-select"
data-fixed-id="<?php echo (int)$fm['id']; ?>"
data-fixed-key="<?php echo htmlspecialchars($fm['fixed_field_key']); ?>"
data-current-value="<?php echo htmlspecialchars($fm['default_value'] ?? ''); ?>">
<option value="">Loading...</option>
</select>
<?php else: ?>
<input type="text"
class="form-control fixed-default-input"
data-fixed-id="<?php echo (int)$fm['id']; ?>"
value="<?php echo htmlspecialchars($fm['default_value'] ?? ''); ?>">
<?php endif; ?>
</div>
</td>
</tr>
@@ -1183,6 +1221,46 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
initSelect2ForJsonDropdowns();
}
function updateFixedJsonDropdowns() {
document.querySelectorAll('select.fixed-json-node-select').forEach(select => {
let currentValue = select.value || select.dataset.currentJson || '';
let options = availableJsonNodes
.map(node => {
const clean = String(node || '').trim();
if (!clean) return '';
const info = jsonNodeLabels[clean] || {};
const shortName = info.shortName || getLastJsonNodeName(clean);
const sample = info.sample || '';
let label = shortName;
if (sample) {
label += ` — ${sample}`;
}
const isSelected = clean === currentValue ? 'selected' : '';
return `
<option
value="${escapeHtmlAttr(clean)}"
data-short-name="${escapeHtmlAttr(shortName)}"
data-sample="${escapeHtmlAttr(sample)}"
data-full-path="${escapeHtmlAttr(clean)}"
${isSelected}>
${escapeHtmlText(label)}
</option>
`;
})
.join('');
select.innerHTML = '<option value="">Select JSON Node</option>' + options;
select.dataset.currentJson = currentValue;
});
initSelect2ForFixedJsonDropdowns();
}
function initSelect2ForJsonDropdowns() {
if (!(window.jQuery && $.fn.select2)) return;
@@ -1266,7 +1344,81 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
});
}
function initSelect2ForFixedJsonDropdowns() {
if (!(window.jQuery && $.fn.select2)) return;
$('.fixed-json-node-select').each(function() {
const $el = $(this);
if (this.style.display === 'none') {
return;
}
if ($el.hasClass('select2-hidden-accessible')) {
$el.select2('destroy');
}
$el.select2({
width: '100%',
placeholder: 'Select JSON Node',
allowClear: true,
templateResult: function(data) {
if (!data.id) return data.text;
const option = data.element;
const shortName = option.getAttribute('data-short-name') || data.text;
const sample = option.getAttribute('data-sample') || '';
const $row = $('<span class="select2-json-row"></span>');
$row.append(`<span class="select2-json-node">${escapeHtmlText(shortName)}</span>`);
if (sample) {
$row.append(`<span class="select2-json-value">${escapeHtmlText(sample)}</span>`);
}
return $row;
},
templateSelection: function(data) {
if (!data.id) return data.text;
const option = data.element;
const shortName = option.getAttribute('data-short-name') || data.text;
const sample = option.getAttribute('data-sample') || '';
if (sample) {
return `${shortName} — ${sample}`;
}
return shortName;
},
matcher: function(params, data) {
if ($.trim(params.term) === '') {
return data;
}
const term = params.term.toLowerCase();
const option = data.element;
const fullPath = option?.getAttribute('data-full-path')?.toLowerCase() || '';
const shortName = option?.getAttribute('data-short-name')?.toLowerCase() || '';
const sample = option?.getAttribute('data-sample')?.toLowerCase() || '';
if (
fullPath.includes(term) ||
shortName.includes(term) ||
sample.includes(term)
) {
return data;
}
return null;
}
});
});
}
function saveJsonNodes(sampleJson, nodes) {
return fetch('update_api_json_nodes.php', {
@@ -1411,6 +1563,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
availableJsonNodes = nodes;
usedJsonNodesFromDB = [];
updateJsonDropdowns();
updateFixedJsonDropdowns();
try {
const data = await saveJsonNodes(rawJson, nodes);
@@ -1556,9 +1709,12 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
return;
}
const mappingSelect = tr?.querySelector('.mapping-select');
const currentMappingType = mappingSelect ? mappingSelect.value : 'manual';
saveMapping(
mappingId,
'manual',
currentMappingType || 'manual',
el.value,
xlsSelect ? xlsSelect.value : null,
null,
@@ -1588,9 +1744,12 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
return;
}
const mappingSelect = tr?.querySelector('.mapping-select');
const currentMappingType = mappingSelect ? mappingSelect.value : 'manual';
saveMapping(
mappingId,
'manual',
currentMappingType || 'manual',
el.value,
xlsSelect ? xlsSelect.value : null,
null,
@@ -1674,6 +1833,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
const removeBtn = tr.querySelector('.remove-xls');
const removeJsonBtn = tr.querySelector('.remove-json');
const isSceltaMultipla = mappingSelect.dataset.isSceltaMultipla === '1';
function destroyJsonSelect2() {
if (jsonSelect && window.jQuery && $(jsonSelect).hasClass('select2-hidden-accessible')) {
@@ -1716,8 +1876,12 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
if (autoSelect) autoSelect.style.display = 'none';
if (manualInput) {
manualInput.style.display = 'none';
manualInput.value = '';
if (isSceltaMultipla) {
manualInput.style.display = 'block';
} else {
manualInput.style.display = 'none';
manualInput.value = '';
}
}
if (mappedColumn) mappedColumn.style.display = 'none';
@@ -1958,13 +2122,19 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
let mappingId = event.target.getAttribute('data-id');
let xlsSelect = tr.querySelector('.xls-columns');
let jsonSelect = tr.querySelector('.json-nodes');
let mappingSelect = tr.querySelector('.mapping-select');
let currentMappingType = mappingSelect ? mappingSelect.value : 'manual';
console.log("Manual default dropdown changed:", {
id: mappingId,
value: event.target.value
value: event.target.value,
mappingType: currentMappingType
});
saveMapping(
mappingId,
'manual',
currentMappingType || 'manual',
event.target.value,
xlsSelect ? xlsSelect.value : null,
null,
@@ -2063,7 +2233,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
mapping_type: mappingType,
excel_column: mappingType === 'xls' ? excelColumn : null,
json_node: mappingType === 'json' ? jsonNode : null,
manual_default: mappingType === 'manual' ? defaultValue : null,
manual_default: defaultValue,
auto_value: mappingType === 'auto' ? (autoValue || 'none') : 'none',
tablename: "<?php echo $template['target_table']; ?>"
})
@@ -2092,6 +2262,115 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
// FIXED FIELDS (DB autosave)
// =======================
// =======================
// FIXED FIELDS: source manual/json autosave
// Only used for API/JSON templates
// =======================
document.addEventListener('change', function(e) {
if (!e.target.classList.contains('fixed-source-select')) return;
const sourceSelect = e.target;
const fixedId = sourceSelect.dataset.fixedId;
const tr = sourceSelect.closest('tr');
const manualWrapper = tr.querySelector('.fixed-manual-wrapper');
const jsonSelect = tr.querySelector('.fixed-json-node-select');
if (sourceSelect.value === 'json') {
if (manualWrapper) {
manualWrapper.style.display = 'none';
}
if (jsonSelect) {
jsonSelect.style.display = 'block';
updateFixedJsonDropdowns();
}
} else {
if (jsonSelect) {
if (window.jQuery && $(jsonSelect).hasClass('select2-hidden-accessible')) {
$(jsonSelect).select2('destroy');
}
jsonSelect.value = '';
jsonSelect.dataset.currentJson = '';
jsonSelect.style.display = 'none';
}
if (manualWrapper) {
manualWrapper.style.display = 'block';
}
}
fetch('update_fixed_field.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: fixedId,
field: 'default_source',
value: sourceSelect.value
})
})
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(data.message || 'Update failed');
}
fixedStatus('✅ Saved');
})
.catch(error => {
console.error(error);
fixedStatus('❌ Save error', true);
});
});
// =======================
// FIXED FIELDS: JSON node autosave
// =======================
function saveFixedJsonNode(selectEl) {
const fixedId = selectEl.dataset.fixedId;
const jsonNode = selectEl.value || '';
fetch('update_fixed_field.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: fixedId,
field: 'json_node',
value: jsonNode
})
})
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(data.message || 'Update failed');
}
selectEl.dataset.currentJson = jsonNode;
fixedStatus('✅ Saved');
})
.catch(error => {
console.error(error);
fixedStatus('❌ Save error', true);
});
}
document.addEventListener('change', function(e) {
if (!e.target.classList.contains('fixed-json-node-select')) return;
saveFixedJsonNode(e.target);
});
if (window.jQuery) {
$(document).on('select2:select select2:clear', '.fixed-json-node-select', function() {
saveFixedJsonNode(this);
});
}
function escapeHtml(str) {
return String(str ?? '')
.replaceAll('&', '&amp;')
@@ -2328,6 +2607,7 @@ $apiSampleJson = $template['api_sample_json'] ?? '';
}
updateJsonDropdowns();
updateFixedJsonDropdowns();
}
fillFixedDropdowns().then(() => {
+700
View File
@@ -0,0 +1,700 @@
<?php include('include/headscript.php'); ?>
<!doctype html>
<html lang="en">
<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'); ?>
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<link href="assets/plugins/datatable/css/dataTables.bootstrap5.min.css" rel="stylesheet" />
<title>TRF-Project - Customer Reports</title>
<style>
.lookup-wrapper {
width: 100%;
max-width: none;
margin: 0;
}
.lookup-title {
font-size: 18px;
font-weight: 700;
}
.lookup-subtitle {
font-size: 13px;
color: #6c757d;
}
.compact-card .card-body {
padding: 1rem;
}
.table-report th {
font-size: 12px;
text-transform: uppercase;
color: #6c757d;
white-space: nowrap;
}
.table-report td {
font-size: 13px;
vertical-align: middle;
}
.status-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
.status-pill-success {
background: #e8fff1;
color: #198754;
}
.status-pill-warning {
background: #fff3cd;
color: #b58100;
}
.pdf-icon-link {
width: 36px;
height: 36px;
border-radius: 10px;
background: #dc3545;
color: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 20px;
text-decoration: none;
transition: all 0.15s ease-in-out;
}
.pdf-icon-link:hover {
color: #fff;
opacity: 0.85;
transform: translateY(-1px);
}
.no-pdf {
font-size: 12px;
color: #adb5bd;
font-weight: 600;
}
.empty-state {
border: 1px dashed #ced4da;
border-radius: 10px;
padding: 24px;
text-align: center;
color: #6c757d;
background: #fafafa;
}
.spinner-inline {
display: none;
margin-left: 8px;
}
.customer-box {
display: none;
border: 1px solid #e9ecef;
background: #fff;
border-radius: 10px;
padding: 12px 14px;
margin-bottom: 15px;
}
.customer-label {
font-size: 11px;
text-transform: uppercase;
color: #6c757d;
font-weight: 700;
}
.customer-value {
font-size: 14px;
color: #212529;
font-weight: 700;
}
.json-preview {
display: none;
background: #111827;
color: #e5e7eb;
border-radius: 10px;
padding: 14px;
font-size: 12px;
max-height: 420px;
overflow: auto;
white-space: pre-wrap;
}
@media (max-width: 768px) {
.table-responsive {
border-radius: 10px;
}
}
.select2-container {
width: 100% !important;
}
.select2-container--default .select2-selection--single {
height: 38px;
border: 1px solid #ced4da;
border-radius: 6px;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: 36px;
font-size: 14px;
color: #212529;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 36px;
}
.select2-dropdown {
z-index: 9999;
}
</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="lookup-wrapper">
<div class="card radius-10 compact-card">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<div>
<div class="lookup-title">Customer Test Reports</div>
<div class="lookup-subtitle">
Select a VisualLims customer and retrieve the latest reports with PDF links.
</div>
</div>
</div>
</div>
<div class="card-body">
<form id="customerReportsForm" class="row g-3 align-items-end">
<div class="col-md-5">
<label for="idCliente" class="form-label fw-semibold">Customer</label>
<select id="idCliente" name="idCliente" class="form-select">
<option value="">Loading customers...</option>
</select>
</div>
<div class="col-md-2">
<label for="limitReports" class="form-label fw-semibold">Limit</label>
<select id="limitReports" name="limitReports" class="form-select">
<option value="1">Last 1</option>
<option value="3" selected>Last 3</option>
<option value="5">Last 5</option>
<option value="10">Last 10</option>
</select>
</div>
<div class="col-md-2">
<label for="signedStatus" class="form-label fw-semibold">Status</label>
<select id="signedStatus" name="signedStatus" class="form-select">
<option value="all" selected>All</option>
<option value="signed">Signed</option>
<option value="not_signed">Not signed</option>
</select>
</div>
<div class="col-md-3">
<button type="submit" id="btnSearchReports" class="btn btn-primary w-100">
<i class="bx bx-search"></i> Search Reports
<span class="spinner-border spinner-border-sm spinner-inline" id="searchSpinner" role="status" aria-hidden="true"></span>
</button>
</div>
</form>
</div>
</div>
<div id="customerBox" class="customer-box">
<div class="row g-3">
<div class="col-md-4">
<div class="customer-label">Customer Code</div>
<div class="customer-value" id="selectedCustomerCode">-</div>
</div>
<div class="col-md-8">
<div class="customer-label">Customer Name</div>
<div class="customer-value" id="selectedCustomerName">-</div>
</div>
</div>
</div>
<div id="resultContainer" style="display:none;">
<div class="card radius-10 compact-card">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<h6 class="mb-0">
Reports
<span class="badge bg-light text-dark ms-1" id="reportCountBadge">0</span>
</h6>
<button type="button" id="toggleJsonBtn" class="btn btn-sm btn-outline-secondary">
<i class="bx bx-code-alt"></i> Show JSON
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover table-report align-middle mb-0" id="reportsTable">
<thead>
<tr>
<th>Report Number</th>
<th>Report ID</th>
<th>Report Date</th>
<th>Print Date</th>
<th>Version</th>
<th>Status</th>
<th class="text-center">PDF</th>
</tr>
</thead>
<tbody id="reportsTableBody"></tbody>
</table>
</div>
<pre id="jsonPreview" class="json-preview mt-3"></pre>
</div>
</div>
</div>
<div id="emptyState" class="empty-state">
<i class="bx bx-file-find" style="font-size:34px;"></i>
<div class="mt-2 fw-semibold">No reports loaded</div>
<div class="small">Select a customer, choose the limit and click Search Reports.</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>
<?php include('jsinclude.php'); ?>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script src="assets/plugins/datatable/js/jquery.dataTables.min.js"></script>
<script src="assets/plugins/datatable/js/dataTables.bootstrap5.min.js"></script>
<script>
$(document).ready(function() {
let lastJsonResponse = null;
let loadedCustomers = [];
let reportsDataTable = null;
function escapeHtml(value) {
if (value === null || value === undefined) {
return '';
}
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function formatDate(value) {
if (!value) {
return '-';
}
const date = new Date(value);
if (isNaN(date.getTime())) {
return value;
}
return date.toLocaleString('it-IT', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
function setLoading(isLoading) {
$('#btnSearchReports').prop('disabled', isLoading);
$('#searchSpinner').toggle(isLoading);
}
function resetResults() {
lastJsonResponse = null;
if (reportsDataTable !== null) {
reportsDataTable.destroy();
reportsDataTable = null;
}
$('#resultContainer').hide();
$('#emptyState').show();
$('#reportsTableBody').html('');
$('#reportCountBadge').text('0');
$('#jsonPreview').hide().text('');
$('#toggleJsonBtn').html('<i class="bx bx-code-alt"></i> Show JSON');
}
function loadCustomers() {
$.ajax({
url: 'get_clienti.php',
method: 'GET',
dataType: 'json',
success: function(response) {
const select = $('#idCliente');
select.empty();
const customers = response.value || response || [];
loadedCustomers = Array.isArray(customers) ? customers : [];
select.append('<option value="">Select customer...</option>');
loadedCustomers.forEach(function(customer) {
const idCliente = customer.IdCliente || '';
const codiceCliente = customer.CodiceCliente || '';
const nominativo = customer.Nominativo || '';
const label = codiceCliente ?
codiceCliente + ' - ' + nominativo :
nominativo;
select.append(
'<option value="' + escapeHtml(idCliente) + '" ' +
'data-code="' + escapeHtml(codiceCliente) + '" ' +
'data-name="' + escapeHtml(nominativo) + '">' +
escapeHtml(label) +
'</option>'
);
});
if ($.fn.select2) {
select.select2({
width: '100%',
placeholder: 'Search customer...',
allowClear: true,
minimumInputLength: 0,
matcher: function(params, data) {
if ($.trim(params.term) === '') {
return data;
}
if (typeof data.text === 'undefined') {
return null;
}
const term = params.term.toLowerCase();
const text = data.text.toLowerCase();
if (text.indexOf(term) > -1) {
return data;
}
return null;
},
sorter: function(data) {
const term = $('.select2-search__field').val();
if (!term) {
return data;
}
const search = term.toLowerCase();
return data.sort(function(a, b) {
const aText = (a.text || '').toLowerCase();
const bText = (b.text || '').toLowerCase();
const aStarts = aText.startsWith(search);
const bStarts = bText.startsWith(search);
if (aStarts && !bStarts) return -1;
if (!aStarts && bStarts) return 1;
return aText.localeCompare(bText);
});
}
});
}
},
error: function(xhr) {
$('#idCliente').html('<option value="">Error loading customers</option>');
let message = 'Unable to load customers.';
if (xhr.responseJSON && xhr.responseJSON.error) {
message = xhr.responseJSON.error;
}
Swal.fire({
title: 'Customer loading error',
text: message,
icon: 'error',
confirmButtonText: 'OK'
});
}
});
}
function updateSelectedCustomerBox() {
const selectedOption = $('#idCliente option:selected');
const customerCode = selectedOption.data('code') || '-';
const customerName = selectedOption.data('name') || '-';
if (!$('#idCliente').val()) {
$('#customerBox').hide();
$('#selectedCustomerCode').text('-');
$('#selectedCustomerName').text('-');
return;
}
$('#selectedCustomerCode').text(customerCode);
$('#selectedCustomerName').text(customerName);
$('#customerBox').show();
}
function renderStatus(isSigned) {
if (isSigned === true) {
return '<span class="status-pill status-pill-success"><i class="bx bx-check-circle"></i> Signed</span>';
}
if (isSigned === false) {
return '<span class="status-pill status-pill-warning"><i class="bx bx-time"></i> Not signed</span>';
}
return '-';
}
function renderPdfCell(pdfFiles) {
if (!Array.isArray(pdfFiles) || pdfFiles.length === 0) {
return '<span class="no-pdf">No PDF</span>';
}
const firstPdf = pdfFiles[0];
if (!firstPdf.download_url) {
return '<span class="no-pdf">No PDF</span>';
}
return `
<a href="${escapeHtml(firstPdf.download_url)}"
target="_blank"
class="pdf-icon-link"
title="${escapeHtml(firstPdf.file_name || 'Download PDF')}">
<i class="bx bxs-file-pdf"></i>
</a>
`;
}
function initReportsDataTable() {
if (reportsDataTable !== null) {
reportsDataTable.destroy();
reportsDataTable = null;
}
reportsDataTable = $('#reportsTable').DataTable({
paging: false,
searching: false,
info: false,
ordering: true,
order: [
[2, 'desc']
],
autoWidth: false,
responsive: true,
columnDefs: [{
targets: 6,
orderable: false
}],
language: {
emptyTable: 'No reports found',
zeroRecords: 'No matching reports found'
}
});
}
function renderReports(response) {
lastJsonResponse = response;
const reports = response.reports || [];
const tbody = $('#reportsTableBody');
tbody.empty();
$('#reportCountBadge').text(reports.length);
if (reports.length > 0) {
reports.forEach(function(report) {
const reportDateOrder = report.data || '';
const printDateOrder = report.data_stampa || '';
const statusOrder = report.firmato === true ? 1 : 0;
const row = `
<tr>
<td>
<strong>${escapeHtml(report.codice_rapporto || '-')}</strong>
</td>
<td>${escapeHtml(report.id_rapporto || '-')}</td>
<td data-order="${escapeHtml(reportDateOrder)}">${escapeHtml(formatDate(report.data))}</td>
<td data-order="${escapeHtml(printDateOrder)}">${escapeHtml(formatDate(report.data_stampa))}</td>
<td>${escapeHtml(report.versione !== null && report.versione !== undefined ? report.versione : '-')}</td>
<td data-order="${statusOrder}">${renderStatus(report.firmato)}</td>
<td class="text-center">${renderPdfCell(report.pdf_files)}</td>
</tr>
`;
tbody.append(row);
});
}
$('#jsonPreview').text(JSON.stringify(response, null, 4));
$('#emptyState').hide();
$('#resultContainer').show();
setTimeout(function() {
initReportsDataTable();
}, 50);
}
$('#idCliente').on('change', function() {
updateSelectedCustomerBox();
resetResults();
});
$('#customerReportsForm').on('submit', function(event) {
event.preventDefault();
const idCliente = $('#idCliente').val();
const limit = $('#limitReports').val();
const signedStatus = $('#signedStatus').val();
if (!idCliente) {
Swal.fire({
title: 'Missing customer',
text: 'Please select a customer.',
icon: 'warning',
confirmButtonText: 'OK'
});
return;
}
resetResults();
updateSelectedCustomerBox();
setLoading(true);
$.ajax({
url: 'get_rapporti_cliente.php',
method: 'GET',
dataType: 'json',
data: {
id_cliente: idCliente,
limit: limit,
signed_status: signedStatus
},
success: function(response) {
if (!response || response.success !== true) {
Swal.fire({
title: 'No data',
text: response && response.error ? response.error : 'No reports were returned.',
icon: 'warning',
confirmButtonText: 'OK'
});
return;
}
renderReports(response);
},
error: function(xhr) {
let message = 'Unexpected error while loading reports.';
if (xhr.responseJSON && xhr.responseJSON.error) {
message = xhr.responseJSON.error;
} else if (xhr.responseText) {
message = xhr.responseText.substring(0, 500);
}
Swal.fire({
title: 'Error',
text: message,
icon: 'error',
confirmButtonText: 'OK'
});
},
complete: function() {
setLoading(false);
}
});
});
$('#toggleJsonBtn').on('click', function() {
const jsonPreview = $('#jsonPreview');
const isVisible = jsonPreview.is(':visible');
jsonPreview.toggle(!isVisible);
if (isVisible) {
$(this).html('<i class="bx bx-code-alt"></i> Show JSON');
} else {
$(this).html('<i class="bx bx-hide"></i> Hide JSON');
}
});
loadCustomers();
});
</script>
</body>
</html>
+580
View File
@@ -0,0 +1,580 @@
<?php include('include/headscript.php'); ?>
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--favicon-->
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>TRF-Project - Test Report Lookup</title>
<style>
.compact-card .card-body {
padding: 1rem;
}
.lookup-wrapper {
max-width: 1100px;
margin: 0 auto;
}
.lookup-title {
font-size: 18px;
font-weight: 700;
}
.lookup-subtitle {
font-size: 13px;
color: #6c757d;
}
.result-section-title {
font-size: 14px;
font-weight: 700;
margin-bottom: 10px;
color: #344767;
}
.info-box {
border: 1px solid #e9ecef;
border-radius: 10px;
padding: 14px;
background: #fff;
height: 100%;
}
.info-label {
font-size: 11px;
text-transform: uppercase;
color: #6c757d;
font-weight: 700;
margin-bottom: 3px;
}
.info-value {
font-size: 14px;
color: #212529;
font-weight: 600;
word-break: break-word;
}
.status-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
}
.status-pill-success {
background: #e8fff1;
color: #198754;
}
.status-pill-warning {
background: #fff3cd;
color: #b58100;
}
.pdf-card {
border: 1px solid #f1d1d1;
background: #fffafa;
border-radius: 10px;
padding: 14px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.pdf-icon {
width: 46px;
height: 46px;
border-radius: 12px;
background: #dc3545;
color: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 24px;
flex-shrink: 0;
}
.pdf-file-name {
font-weight: 700;
font-size: 14px;
margin-bottom: 2px;
}
.pdf-meta {
font-size: 12px;
color: #6c757d;
}
.json-preview {
display: none;
background: #111827;
color: #e5e7eb;
border-radius: 10px;
padding: 14px;
font-size: 12px;
max-height: 420px;
overflow: auto;
white-space: pre-wrap;
}
.empty-state {
border: 1px dashed #ced4da;
border-radius: 10px;
padding: 24px;
text-align: center;
color: #6c757d;
background: #fafafa;
}
.spinner-inline {
display: none;
margin-left: 8px;
}
.btn-download-pdf {
white-space: nowrap;
}
@media (max-width: 768px) {
.pdf-card {
flex-direction: column;
align-items: flex-start;
}
.btn-download-pdf {
width: 100%;
}
}
</style>
</head>
<body>
<!--wrapper-->
<div class="wrapper">
<!--sidebar wrapper -->
<?php include('include/navbar.php'); ?>
<!--end sidebar wrapper -->
<!--start header -->
<?php include('include/topbar.php'); ?>
<!--end header -->
<!--start page wrapper -->
<div class="page-wrapper">
<div class="page-content">
<?php include('top_stat_widget.php'); ?>
<div class="lookup-wrapper">
<div class="card radius-10 compact-card">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<div>
<div class="lookup-title">Test Report Lookup</div>
<div class="lookup-subtitle">
Search a test report from VisualLims by report number and download the PDF if available.
</div>
</div>
</div>
</div>
<div class="card-body">
<form id="reportSearchForm" class="row g-3 align-items-end">
<div class="col-md-8">
<label for="codiceRapporto" class="form-label fw-semibold">
Report Number
</label>
<input type="text"
class="form-control"
id="codiceRapporto"
name="codiceRapporto"
placeholder="Example: 2621521"
autocomplete="off">
</div>
<div class="col-md-4">
<button type="submit" id="btnSearchReport" class="btn btn-primary w-100">
<i class="bx bx-search"></i> Proceed
<span class="spinner-border spinner-border-sm spinner-inline" id="searchSpinner" role="status" aria-hidden="true"></span>
</button>
</div>
</form>
</div>
</div>
<div id="resultContainer" style="display:none;">
<div class="card radius-10 compact-card">
<div class="card-header">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<h6 class="mb-0">Report Data</h6>
<button type="button" id="toggleJsonBtn" class="btn btn-sm btn-outline-secondary">
<i class="bx bx-code-alt"></i> Show JSON
</button>
</div>
</div>
<div class="card-body">
<div class="result-section-title">General Information</div>
<div class="row g-3 mb-4">
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Report Number</div>
<div class="info-value" id="resCodiceRapporto">-</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Report ID</div>
<div class="info-value" id="resIdRapporto">-</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Customer Code</div>
<div class="info-value" id="resCodiceCliente">-</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Customer Name</div>
<div class="info-value" id="resNominativoCliente">-</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Report Date</div>
<div class="info-value" id="resDataRapporto">-</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Print Date</div>
<div class="info-value" id="resDataStampa">-</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Version</div>
<div class="info-value" id="resVersione">-</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<div class="info-label">Signed</div>
<div class="info-value" id="resFirmato">-</div>
</div>
</div>
<div class="col-md-6">
<div class="info-box">
<div class="info-label">Detail Endpoint</div>
<div class="info-value small" id="resDetailEndpoint">-</div>
</div>
</div>
</div>
<div class="result-section-title">Available PDF Files</div>
<div id="pdfFilesContainer" class="mb-4"></div>
<pre id="jsonPreview" class="json-preview"></pre>
</div>
</div>
</div>
<div id="emptyState" class="empty-state">
<i class="bx bx-file-find" style="font-size:34px;"></i>
<div class="mt-2 fw-semibold">No report loaded</div>
<div class="small">Enter a report number and click Proceed.</div>
</div>
</div>
</div>
</div>
<!--end page wrapper -->
<!--start overlay-->
<div class="overlay toggle-icon"></div>
<!--end overlay-->
<!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top">
<i class='bx bxs-up-arrow-alt'></i>
</a>
<!--End Back To Top Button-->
<?php include('include/footer.php'); ?>
</div>
<!--end wrapper-->
<?php include('jsinclude.php'); ?>
<script>
$(document).ready(function() {
let lastJsonResponse = null;
function escapeHtml(value) {
if (value === null || value === undefined) {
return '';
}
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function formatDate(value) {
if (!value) {
return '-';
}
const date = new Date(value);
if (isNaN(date.getTime())) {
return value;
}
return date.toLocaleString('it-IT', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
function setLoading(isLoading) {
$('#btnSearchReport').prop('disabled', isLoading);
$('#searchSpinner').toggle(isLoading);
}
function resetResult() {
lastJsonResponse = null;
$('#resultContainer').hide();
$('#emptyState').show();
$('#jsonPreview').hide().text('');
$('#toggleJsonBtn').html('<i class="bx bx-code-alt"></i> Show JSON');
$('#resCodiceRapporto').text('-');
$('#resIdRapporto').text('-');
$('#resCodiceCliente').text('-');
$('#resNominativoCliente').text('-');
$('#resDataRapporto').text('-');
$('#resDataStampa').text('-');
$('#resVersione').text('-');
$('#resFirmato').text('-');
$('#resDetailEndpoint').text('-');
$('#pdfFilesContainer').html('');
}
function renderPdfFiles(pdfFiles) {
const container = $('#pdfFilesContainer');
container.empty();
if (!Array.isArray(pdfFiles) || pdfFiles.length === 0) {
container.html(`
<div class="empty-state">
<i class="bx bx-file-blank" style="font-size:28px;"></i>
<div class="mt-2 fw-semibold">No PDF available</div>
<div class="small">No report PDF file was returned by VisualLims.</div>
</div>
`);
return;
}
pdfFiles.forEach(function(file) {
const idRapportoFile = file.id_rapporto_file || '';
const fileName = file.file_name || 'report.pdf';
const categoria = file.categoria || '-';
const tipoRapporto = file.tipo_rapporto || '-';
const downloadUrl = 'download_rapporto_pdf.php?id_rapporto_file=' + encodeURIComponent(idRapportoFile);
const html = `
<div class="pdf-card mb-2">
<div class="d-flex align-items-center gap-3">
<div class="pdf-icon">
<i class="bx bxs-file-pdf"></i>
</div>
<div>
<div class="pdf-file-name">${escapeHtml(fileName)}</div>
<div class="pdf-meta">
ID File: ${escapeHtml(idRapportoFile)}
&nbsp;|&nbsp;
Category: ${escapeHtml(categoria)}
&nbsp;|&nbsp;
Type: ${escapeHtml(tipoRapporto)}
</div>
</div>
</div>
<a href="${downloadUrl}" target="_blank" class="btn btn-danger btn-sm btn-download-pdf">
<i class="bx bx-download"></i> Download PDF
</a>
</div>
`;
container.append(html);
});
}
function renderResult(response) {
lastJsonResponse = response;
const base = response.rapporto_base || {};
const data = response.data || {};
const cliente = response.cliente || data.Cliente || {};
$('#resCodiceRapporto').text(response.codice_rapporto || base.CodiceRapporto || '-');
$('#resIdRapporto').text(response.id_rapporto || base.IdRapporto || '-');
$('#resCodiceCliente').text(cliente.CodiceCliente || '-');
$('#resNominativoCliente').text(cliente.Nominativo || '-');
$('#resDataRapporto').text(formatDate(base.Data || data.Data));
$('#resDataStampa').text(formatDate(base.DataStampa || data.DataStampa));
$('#resVersione').text(base.Versione !== undefined ? base.Versione : (data.Versione !== undefined ? data.Versione : '-'));
$('#resDetailEndpoint').text(response.detail_endpoint || '-');
const isSigned = base.Firmato === true || data.Firmato === true;
if (isSigned) {
$('#resFirmato').html('<span class="status-pill status-pill-success"><i class="bx bx-check-circle"></i> Signed</span>');
} else {
$('#resFirmato').html('<span class="status-pill status-pill-warning"><i class="bx bx-time"></i> Not signed</span>');
}
renderPdfFiles(response.pdf_files || []);
$('#jsonPreview').text(JSON.stringify(response, null, 4));
$('#emptyState').hide();
$('#resultContainer').show();
}
$('#reportSearchForm').on('submit', function(event) {
event.preventDefault();
const codiceRapporto = $('#codiceRapporto').val().trim();
if (!codiceRapporto) {
Swal.fire({
title: 'Missing report number',
text: 'Please enter a report number.',
icon: 'warning',
confirmButtonText: 'OK'
});
return;
}
resetResult();
setLoading(true);
$.ajax({
url: 'get_rapporto_prova.php',
method: 'GET',
dataType: 'json',
data: {
codice: codiceRapporto,
step: 'files'
},
success: function(response) {
if (!response || response.success !== true) {
Swal.fire({
title: 'Report not found',
text: response && response.message ? response.message : 'No report was found for this number.',
icon: 'warning',
confirmButtonText: 'OK'
});
return;
}
renderResult(response);
},
error: function(xhr) {
let message = 'Unexpected error while loading the report.';
if (xhr.responseJSON && xhr.responseJSON.error) {
message = xhr.responseJSON.error;
} else if (xhr.responseJSON && xhr.responseJSON.message) {
message = xhr.responseJSON.message;
} else if (xhr.responseText) {
message = xhr.responseText.substring(0, 500);
}
Swal.fire({
title: 'Error',
text: message,
icon: 'error',
confirmButtonText: 'OK'
});
},
complete: function() {
setLoading(false);
}
});
});
$('#toggleJsonBtn').on('click', function() {
const jsonPreview = $('#jsonPreview');
const isVisible = jsonPreview.is(':visible');
jsonPreview.toggle(!isVisible);
if (isVisible) {
$(this).html('<i class="bx bx-code-alt"></i> Show JSON');
} else {
$(this).html('<i class="bx bx-hide"></i> Hide JSON');
}
});
const urlParams = new URLSearchParams(window.location.search);
const prefillCodice = urlParams.get('codice');
if (prefillCodice) {
$('#codiceRapporto').val(prefillCodice);
$('#reportSearchForm').trigger('submit');
}
});
</script>
</body>
</html>
+2 -2
View File
@@ -730,8 +730,8 @@
{
"IdSchemaCustomFields": 177,
"ConteggioClienti": 0,
"Nome": "Phoebe philo ACC",
"Descrizione": "(scarpe, borse, cinture, occhiali, gioielleria)\r\n"
"Nome": "Phoebe Philo ",
"Descrizione": "\r\n\r\n"
},
{
"IdSchemaCustomFields": 178,
+298 -192
View File
@@ -78,6 +78,51 @@
color: #198754;
}
.badge-source-pdf {
background-color: #fff3cd;
color: #b58100;
}
.type-filter-bar {
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 12px;
}
.type-filter-btn {
border: 0;
border-radius: 999px;
padding: 7px 14px;
font-size: 13px;
font-weight: 700;
color: #fff;
opacity: 0.35;
transition: all 0.15s ease-in-out;
}
.type-filter-btn.active {
opacity: 1;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
}
.type-filter-btn[data-type="XLS"] {
background-color: #0d6efd;
}
.type-filter-btn[data-type="API"] {
background-color: #198754;
}
.type-filter-btn[data-type="PDF"] {
background-color: #b58100;
}
.type-filter-btn:hover {
transform: translateY(-1px);
}
#xlsTemplatesTable {
font-size: 13px;
}
@@ -136,79 +181,92 @@
</div>
<div class="card-body">
<div class="type-filter-bar">
<span class="text-muted fw-semibold me-1">Filter by type:</span>
<button type="button" class="type-filter-btn active" data-type="XLS">
XLS
</button>
<button type="button" class="type-filter-btn active" data-type="API">
JSON/API
</button>
<button type="button" class="type-filter-btn active" data-type="PDF">
PDF
</button>
</div>
<div class="table-responsive">
<table id="xlsTemplatesTable" class="table table-striped table-bordered table-sm w-100">
<table id="xlsTemplatesTable" class="table table-striped table-bordered align-middle w-100">
<thead>
<tr>
<th><?= htmlspecialchars($action, ENT_QUOTES, 'UTF-8'); ?></th>
<th><?= htmlspecialchars($nametemplate, ENT_QUOTES, 'UTF-8'); ?></th>
<th>Actions</th>
<th>Template Name</th>
<th>Type</th>
<th>Row</th>
<th>Col</th>
<th><?= htmlspecialchars($desctemplate, ENT_QUOTES, 'UTF-8'); ?></th>
<th>Description</th>
<th>Client</th>
<th>Button</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<!-- DataTables will populate this section automatically -->
</tbody>
<tbody></tbody>
</table>
</div>
</div>
</div>
<!--end page wrapper -->
<!--start overlay-->
<div class="overlay toggle-icon"></div>
<!--end overlay-->
<!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<!--End Back To Top Button-->
<?php include('include/footer.php'); ?>
</div>
</div>
<!--end page wrapper -->
<!--end wrapper-->
<!--start overlay-->
<div class="overlay toggle-icon"></div>
<!--end overlay-->
<?php include('jsinclude.php'); ?>
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
<!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<!--End Back To Top Button-->
<script>
$(document).ready(function() {
<?php include('include/footer.php'); ?>
</div>
<!--end wrapper-->
const urlParams = new URLSearchParams(window.location.search);
<?php include('jsinclude.php'); ?>
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
if (urlParams.get('cloned') === '1') {
Swal.fire({
title: "Template cloned",
text: "The template was cloned successfully.",
icon: "success",
confirmButtonText: "OK"
});
<script>
$(document).ready(function() {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('cloned') === '1') {
Swal.fire({
title: "Template cloned",
text: "The template was cloned successfully.",
icon: "success",
confirmButtonText: "OK"
});
const cleanUrl = window.location.pathname;
window.history.replaceState({}, document.title, cleanUrl);
}
$('#xlsTemplatesTable').DataTable({
processing: true,
serverSide: false,
ajax: 'load_templates.php',
pageLength: 50,
autoWidth: false,
columns: [{
data: 'id',
orderable: false,
searchable: false,
title: "Actions",
className: "table-actions text-center",
render: function(data, type, row) {
return `
const cleanUrl = window.location.pathname;
window.history.replaceState({}, document.title, cleanUrl);
}
const templatesTable = $('#xlsTemplatesTable').DataTable({
processing: true,
serverSide: false,
ajax: 'load_templates.php',
pageLength: 50,
autoWidth: false,
columns: [{
data: 'id',
orderable: false,
searchable: false,
title: "Actions",
className: "table-actions text-center",
render: function(data, type, row) {
return `
<div class="d-flex justify-content-center gap-1">
<a href="edit_template_xls.php?id=${data}" class="btn btn-sm btn-primary" title="Edit">
<i class="bx bx-edit-alt"></i>
@@ -227,121 +285,169 @@
</button>
</div>
`;
}
},
{
data: 'name',
title: "Template Name",
className: "name-cell"
},
{
data: 'source_type',
title: "Type",
className: "text-center",
render: function(data, type, row) {
const sourceType = (data || 'XLS').toUpperCase();
if (type === 'display') {
if (sourceType === 'API') {
return '<span class="badge-source badge-source-api">API</span>';
}
return '<span class="badge-source badge-source-xls">XLS</span>';
}
},
{
data: 'name',
title: "Template Name",
className: "name-cell"
},
{
data: 'source_type',
title: "Type",
className: "text-center",
render: function(data, type, row) {
let sourceType = (data || 'XLS').toUpperCase();
return sourceType;
}
},
{
data: 'header_row',
title: "Row",
className: "text-center",
defaultContent: ''
},
{
data: 'start_column',
title: "Col",
className: "text-center",
defaultContent: ''
},
{
data: 'description',
title: "Description",
className: "description-cell",
defaultContent: 'No description'
},
{
data: null,
title: "Client",
className: "client-cell",
render: function(data, type, row) {
const clientName = row.clientname || "No client";
const clientId = row.idclient || "N/A";
return `${clientName} <small class="text-muted">(ID: ${clientId})</small>`;
}
},
{
data: 'button_label',
title: "Button",
className: "button-cell",
defaultContent: 'Click Me'
},
{
data: 'status',
title: "Status",
orderable: false,
searchable: false,
className: "text-center",
render: function(status, type, row) {
let checked = (status === "active") ? "checked" : "";
return `
// Treat JSON as API group for dashboard filter
if (sourceType === 'JSON') {
sourceType = 'API';
}
if (type === 'display') {
if (sourceType === 'API') {
return '<span class="badge-source badge-source-api">JSON/API</span>';
}
if (sourceType === 'PDF') {
return '<span class="badge-source badge-source-pdf">PDF</span>';
}
return '<span class="badge-source badge-source-xls">XLS</span>';
}
return sourceType;
}
},
{
data: 'header_row',
title: "Row",
className: "text-center",
defaultContent: ''
},
{
data: 'start_column',
title: "Col",
className: "text-center",
defaultContent: ''
},
{
data: 'description',
title: "Description",
className: "description-cell",
defaultContent: 'No description'
},
{
data: null,
title: "Client",
className: "client-cell",
render: function(data, type, row) {
const clientName = row.clientname || "No client";
const clientId = row.idclient || "N/A";
return `${clientName} <small class="text-muted">(ID: ${clientId})</small>`;
}
},
{
data: 'button_label',
title: "Button",
className: "button-cell",
defaultContent: 'Click Me'
},
{
data: 'status',
title: "Status",
orderable: false,
searchable: false,
className: "text-center",
render: function(status, type, row) {
let checked = (status === "active") ? "checked" : "";
return `
<label class="switch">
<input type="checkbox" class="toggle-status" data-id="${row.id}" ${checked}>
<span class="slider round"></span>
</label>
`;
}
}
],
dom: '<"card-header border-bottom p-3"<"d-flex align-items-center"<"card-title mb-0 flex-grow-1"f>>>rt<"card-footer border-top p-3"<"d-flex align-items-center"<"me-auto"l><"d-flex gap-2"ip>>>',
lengthMenu: [10, 25, 50, 100],
order: [
[1, 'asc']
],
language: {
search: "Cerca:",
lengthMenu: "Mostra _MENU_ elementi",
info: "Visualizzando da _START_ a _END_ di _TOTAL_ elementi",
paginate: {
first: "<?= isset($langdatatables['paginate_first']) ? $langdatatables['paginate_first'] : 'Primo' ?>",
last: "<?= isset($langdatatables['paginate_last']) ? $langdatatables['paginate_last'] : 'Ultimo' ?>",
next: "<?= isset($langdatatables['paginate_next']) ? $langdatatables['paginate_next'] : 'Successivo' ?>",
previous: "<?= isset($langdatatables['paginate_previous']) ? $langdatatables['paginate_previous'] : 'Precedente' ?>"
}
}
}
],
dom: '<"card-header border-bottom p-3"<"d-flex align-items-center"<"card-title mb-0 flex-grow-1"f>>>rt<"card-footer border-top p-3"<"d-flex align-items-center"<"me-auto"l><"d-flex gap-2"ip>>>',
lengthMenu: [10, 25, 50, 100],
order: [
[1, 'asc']
],
language: {
search: "Cerca:",
lengthMenu: "Mostra _MENU_ elementi",
info: "Visualizzando da _START_ a _END_ di _TOTAL_ elementi",
paginate: {
first: "<?= isset($langdatatables['paginate_first']) ? $langdatatables['paginate_first'] : 'Primo' ?>",
last: "<?= isset($langdatatables['paginate_last']) ? $langdatatables['paginate_last'] : 'Ultimo' ?>",
next: "<?= isset($langdatatables['paginate_next']) ? $langdatatables['paginate_next'] : 'Successivo' ?>",
previous: "<?= isset($langdatatables['paginate_previous']) ? $langdatatables['paginate_previous'] : 'Precedente' ?>"
}
}
});
});
});
const activeSourceTypes = {
XLS: true,
API: true,
PDF: true
};
function confirmDelete(id) {
Swal.fire({
title: "Are you sure?",
text: "This action cannot be undone!",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#d33",
cancelButtonColor: "#3085d6",
confirmButtonText: "Yes, delete it!",
cancelButtonText: "Cancel"
}).then((result) => {
if (result.isConfirmed) {
window.location.href = `delete_template_xls.php?id=${id}`;
}
});
}
$.fn.dataTable.ext.search.push(function(settings, data, dataIndex) {
if (settings.nTable.id !== 'xlsTemplatesTable') {
return true;
}
function confirmClone(id, templateName) {
Swal.fire({
title: "Clone template?",
html: `
const api = new $.fn.dataTable.Api(settings);
const rowData = api.row(dataIndex).data();
let sourceType = ((rowData && rowData.source_type) ? rowData.source_type : 'XLS').toUpperCase();
if (sourceType === 'JSON') {
sourceType = 'API';
}
return activeSourceTypes[sourceType] === true;
});
$('.type-filter-btn').on('click', function() {
const type = $(this).data('type');
activeSourceTypes[type] = !activeSourceTypes[type];
$(this).toggleClass('active', activeSourceTypes[type]);
const hasAtLeastOneActive = Object.values(activeSourceTypes).some(Boolean);
if (!hasAtLeastOneActive) {
activeSourceTypes[type] = true;
$(this).addClass('active');
}
$('#xlsTemplatesTable').DataTable().draw();
});
});
function confirmDelete(id) {
Swal.fire({
title: "Are you sure?",
text: "This action cannot be undone!",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#d33",
cancelButtonColor: "#3085d6",
confirmButtonText: "Yes, delete it!",
cancelButtonText: "Cancel"
}).then((result) => {
if (result.isConfirmed) {
window.location.href = `delete_template_xls.php?id=${id}`;
}
});
}
function confirmClone(id, templateName) {
Swal.fire({
title: "Clone template?",
html: `
<div class="text-start">
<p class="mb-2">You are about to clone this template:</p>
<strong>${templateName}</strong>
@@ -350,45 +456,45 @@
</p>
</div>
`,
icon: "question",
showCancelButton: true,
confirmButtonColor: "#ffc107",
cancelButtonColor: "#6c757d",
confirmButtonText: "Yes, clone it",
cancelButtonText: "Cancel"
}).then((result) => {
if (result.isConfirmed) {
window.location.href = `clone_template.php?id=${id}`;
icon: "question",
showCancelButton: true,
confirmButtonColor: "#ffc107",
cancelButtonColor: "#6c757d",
confirmButtonText: "Yes, clone it",
cancelButtonText: "Cancel"
}).then((result) => {
if (result.isConfirmed) {
window.location.href = `clone_template.php?id=${id}`;
}
});
}
});
}
$(document).on("change", ".toggle-status", function() {
let templateId = $(this).data("id");
let newStatus = $(this).is(":checked") ? "active" : "inactive";
$(document).on("change", ".toggle-status", function() {
let templateId = $(this).data("id");
let newStatus = $(this).is(":checked") ? "active" : "inactive";
$.ajax({
url: "update_template_status.php",
type: "POST",
dataType: "json",
data: {
id: templateId,
status: newStatus
},
success: function(response) {
if (response.success) {
console.log("Status updated successfully.");
} else {
console.error("Error updating status:", response.message);
alert("Error updating status: " + response.message);
}
},
error: function(xhr) {
console.error("AJAX error:", xhr.responseText);
}
});
});
</script>
$.ajax({
url: "update_template_status.php",
type: "POST",
dataType: "json",
data: {
id: templateId,
status: newStatus
},
success: function(response) {
if (response.success) {
console.log("Status updated successfully.");
} else {
console.error("Error updating status:", response.message);
alert("Error updating status: " + response.message);
}
},
error: function(xhr) {
console.error("AJAX error:", xhr.responseText);
}
});
});
</script>
</body>
+77 -26
View File
@@ -3,33 +3,84 @@ header('Content-Type: application/json');
require_once(__DIR__ . '/include/headscript.php');
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$input = json_decode(file_get_contents('php://input'), true);
$input = json_decode(file_get_contents('php://input'), true);
$id = (int)($input['id'] ?? 0);
$field = (string)($input['field'] ?? '');
$value = $input['value'] ?? null;
$id = (int)($input['id'] ?? 0);
$field = (string)($input['field'] ?? '');
$value = $input['value'] ?? null;
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid id']);
exit;
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid id']);
exit;
}
$allowed = [
'default_value',
'default_source',
'json_node',
'is_visible_import',
'is_required'
];
if (!in_array($field, $allowed, true)) {
echo json_encode(['success' => false, 'message' => 'Invalid field: ' . $field]);
exit;
}
if ($field === 'is_visible_import' || $field === 'is_required') {
$value = ((int)$value === 1) ? 1 : 0;
}
if ($field === 'default_source') {
$value = (string)$value;
if (!in_array($value, ['manual', 'json'], true)) {
echo json_encode(['success' => false, 'message' => 'Invalid default_source']);
exit;
}
// If the user goes back to manual, clear the JSON node
if ($value === 'manual') {
$sql = "
UPDATE template_fixed_mapping
SET default_source = :val, json_node = NULL
WHERE id = :id
";
$stmt = $pdo->prepare($sql);
$ok = $stmt->execute([
':val' => $value,
':id' => $id
]);
echo json_encode(['success' => (bool)$ok]);
exit;
}
}
if ($field === 'json_node') {
$value = trim((string)$value);
if ($value === '') {
$value = null;
}
}
$sql = "UPDATE template_fixed_mapping SET {$field} = :val WHERE id = :id";
$stmt = $pdo->prepare($sql);
$ok = $stmt->execute([
':val' => $value,
':id' => $id
]);
echo json_encode(['success' => (bool)$ok]);
} catch (Throwable $e) {
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
$allowed = ['default_value', 'is_visible_import', 'is_required'];
if (!in_array($field, $allowed, true)) {
echo json_encode(['success' => false, 'message' => 'Invalid field']);
exit;
}
if ($field === 'is_visible_import' || $field === 'is_required') {
$value = ((int)$value === 1) ? 1 : 0;
}
$sql = "UPDATE template_fixed_mapping SET {$field} = :val WHERE id = :id";
$stmt = $pdo->prepare($sql);
$ok = $stmt->execute([':val' => $value, ':id' => $id]);
echo json_encode(['success' => (bool)$ok]);