added additional field in parts template e layout

This commit is contained in:
Claudio 2026-03-02 14:37:20 +01:00
parent 9af0df3cca
commit c9122774b1
15 changed files with 741 additions and 196 deletions

File diff suppressed because one or more lines are too long

View File

@ -312,3 +312,5 @@
2026-02-25 15:23:12 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-26 15:01:32 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-02-28 21:01:27 [AnagraficaCertestService] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}
2026-03-01 19:11:58 [AnagraficaCertestObject] Autenticazione fallita: HTTP 400, Errore cURL: , Risposta: {"title":"Bad Request","status":400,"detail":"Cannot persist the object. It was modified or deleted (purged) by another application.","instance":"POST /api/authentication/authenticate","errorCode":"96bfc1252b"}

View File

@ -0,0 +1,38 @@
<?php
require_once __DIR__ . '/include/headscript.php'; // o il tuo bootstrap standard
header('Content-Type: application/json');
try {
if (!isset($_GET['iddatadb']) || !is_numeric($_GET['iddatadb'])) {
echo json_encode(['success' => true, 'field' => null]);
exit;
}
$iddatadb = (int)$_GET['iddatadb'];
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
// 1) prendo templateid da datadb
// 2) cerco max 1 record in template_mapping con is_visible_parts=1
$sql = "
SELECT tm.field_id, tm.field_label, tm.data_type, tm.has_list
FROM datadb d
JOIN template_mapping tm ON tm.template_id = d.templateid
WHERE d.iddatadb = ?
AND tm.is_visible_parts = 1
ORDER BY tm.id ASC
LIMIT 1
";
$stmt = $pdo->prepare($sql);
$stmt->execute([$iddatadb]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'field' => $row ? $row : null
]);
} catch (Throwable $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}

View File

@ -14,11 +14,57 @@ if (!$iddatadb) {
}
try {
$stmt = $pdo->prepare("SELECT id, iddatadb, part_number, part_description, idmatrice, note, dateexpiry FROM identification_parts WHERE iddatadb = :iddatadb ORDER BY part_number ASC");
$stmt->execute([':iddatadb' => $iddatadb]);
// 1) prendo templateid da datadb
$stmtTpl = $pdo->prepare("SELECT templateid FROM datadb WHERE iddatadb = :iddatadb LIMIT 1");
$stmtTpl->execute([':iddatadb' => $iddatadb]);
$templateid = $stmtTpl->fetchColumn();
// 2) prendo (max 1) field_id visibile in parts
$extraFieldId = null;
if ($templateid) {
$stmtEF = $pdo->prepare("SELECT field_id FROM template_mapping WHERE template_id = :templateid AND is_visible_parts = 1 ORDER BY id ASC LIMIT 1");
$stmtEF->execute([':templateid' => $templateid]);
$extraFieldId = $stmtEF->fetchColumn();
if ($extraFieldId !== false) $extraFieldId = (int)$extraFieldId;
else $extraFieldId = null;
}
// 3) carico parts + join su tabella figlia
if ($extraFieldId) {
$stmt = $pdo->prepare("
SELECT
p.id, p.iddatadb, p.part_number, p.part_description, p.idmatrice, p.note, p.dateexpiry,
cf.value_id AS extra_value_id,
cf.value_text AS extra_value_text
FROM identification_parts p
LEFT JOIN identification_parts_customfields cf
ON cf.part_id = p.id AND cf.field_id = :extraFieldId
WHERE p.iddatadb = :iddatadb
ORDER BY p.part_number ASC
");
$stmt->execute([
':iddatadb' => $iddatadb,
':extraFieldId' => $extraFieldId
]);
} else {
$stmt = $pdo->prepare("
SELECT id, iddatadb, part_number, part_description, idmatrice, note, dateexpiry,
NULL AS extra_value_id, NULL AS extra_value_text
FROM identification_parts
WHERE iddatadb = :iddatadb
ORDER BY part_number ASC
");
$stmt->execute([':iddatadb' => $iddatadb]);
}
$parts = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'parts' => $parts]);
echo json_encode([
'success' => true,
'parts' => $parts,
'extra_field_id' => $extraFieldId,
'debug_sql' => ($extraFieldId ? 'WITH_CF_JOIN' : 'NO_CF')
]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'message' => 'Errore nel caricamento: ' . $e->getMessage()]);
}

View File

@ -0,0 +1,40 @@
CAMPIONE #0
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 95833,
"Matrice": 8413,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "aaa"
}'
RESPONSE:
{
"IdCampione": 14445,
"Commessa": 95833,
"Matrice": 8413
}
---
CAMPIONE #1
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/Campione' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Commessa": 95833,
"Matrice": 3879,
"SottoMatrice": null,
"SchemaCustomField": 82,
"NoteWeb": "bbb"
}'
RESPONSE:
{
"IdCampione": 15750,
"Commessa": 95833,
"Matrice": 3879
}
---

View File

@ -0,0 +1,9 @@
curl --location --request GET 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(95833)?$expand=CommesseCustomFields($expand=CustomField)' \
--header 'Authorization: Bearer ••••••'
RESPONSE:
{
"IdCommessa": 95833,
"CodiceCommessa": "SIM-95833",
"CommesseCustomFields": []
}

View File

@ -0,0 +1,9 @@
curl --location --request GET 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(95833)?$expand=CommesseCustomFields($expand=CustomField)' \
--header 'Authorization: Bearer ••••••'
RESPONSE:
{
"IdCommessa": 95833,
"CodiceCommessa": "SIM-95833",
"CommesseCustomFields": []
}

View File

@ -0,0 +1,10 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb(95833)/ImportaCommessa' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{}'
RESPONSE:
{
"simulated": true,
"endpoint": "CommessaWeb(95833)\/ImportaCommessa"
}

View File

@ -0,0 +1,15 @@
Photos for CommessaWeb 95833 (iddatadb=1237):
Total photos found: 1
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/AllegatoCommessaWeb' \
--header 'Authorization: Bearer ••••••' \
--form 'IdCommessa=95833' \
--form 'file=@C:\xampp\htdocs\trf_certest\public\photostrf\1237-20260228192737-Blue and White Simple Daily Vlogger YouTube Banner (15) (1) (1).png'
RESPONSE:
{
"simulated": true,
"file": "1237-20260228192737-Blue and White Simple Daily Vlogger YouTube Banner (15) (1) (1).png"
}
---

View File

@ -0,0 +1,22 @@
curl --location --request POST 'https://93.43.5.102/limsapi/api/odata/CommessaWeb' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ••••••' \
--data '{
"Cliente": 3378,
"SchemaCustomField": 82,
"Richiedente": "Test Web Import",
"Descrizione": "TEST CommessaWeb",
"ClienteResponsabile": 3,
"MoltiplicatorePrezzo": 1,
"AnagraficaCertestObject": 2,
"AnagraficaCertestService": 1,
"ClienteFornitore": null
}'
RESPONSE:
{
"IdCommessa": 95833,
"CodiceCommessa": "SIM-95833",
"Richiedente": "Test Web Import",
"Descrizione": "TEST CommessaWeb"
}

View File

@ -913,12 +913,22 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
cb.checked = cb.dataset.originalChecked === 'true';
});
});
} else if (event.target.classList.contains('visible-import-checkbox')) {
} else if (event.target.classList.contains('visible-parts-checkbox')) {
const checkbox = event.target;
const mappingId = checkbox.dataset.mappingId;
const value = checkbox.checked ? 1 : 0;
fetch('update_visible_import.php', {
// salva stato per rollback
const prevChecked = checkbox.checked;
// ✅ UI: se sto mettendo a 1, tolgo la spunta a tutti gli altri SUBITO
if (value === 1) {
document.querySelectorAll('.visible-parts-checkbox').forEach(cb => {
if (cb !== checkbox) cb.checked = false;
});
}
fetch('update_visible_parts.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@ -929,16 +939,25 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
value: value
})
})
.then(response => response.json())
.then(r => r.json())
.then(data => {
if (!data.success) {
console.error("❌ Error updating is_visible_import:", data.message);
checkbox.checked = !checkbox.checked;
console.error("❌ Error updating is_visible_parts:", data.message);
// rollback UI
checkbox.checked = !prevChecked;
// se avevo tolto le spunte agli altri, ricarico per riallineare la UI al DB
// (semplice e safe)
location.reload();
}
})
.catch(error => {
console.error("❌ Fetch error:", error);
checkbox.checked = !checkbox.checked;
// rollback UI
checkbox.checked = !prevChecked;
location.reload();
});
} else if (event.target.classList.contains('visible-parts-checkbox')) {
const checkbox = event.target;

View File

@ -1,5 +1,5 @@
<div class="modal fade" id="partsModal" tabindex="-1" aria-labelledby="partsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl" style="max-width: 80% !important;">
<div class="modal-dialog modal-xl" style="max-width: 95vw !important;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="partsModalLabel">Parti per TRF: <span id="trfHeader"></span></h5>
@ -15,10 +15,10 @@
<button type="button" class="btn btn-info btn-sm" id="renumberPartsBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Rinumera Parti</button>
<button type="button" class="btn btn-secondary btn-sm ms-2" id="toggleVoiceBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;"><i class="fas fa-microphone"></i> Voce</button>
<button type="button" class="btn btn-info btn-sm ms-2 d-none" id="quotationeBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Add Quotation</button>
<button type="button" class="btn btn-primary btn-sm ms-2" id="showHideImageBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
<i class="fas fa-eye-slash" style="font-size: 0.8rem;"></i>
<i class="fas fa-image ms-1" style="font-size: 0.8rem;"></i>
</button>
<button type="button" class="btn btn-primary btn-sm ms-2" id="showHideImageBtn" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">
<i class="fas fa-eye-slash" style="font-size: 0.8rem;"></i>
<i class="fas fa-image ms-1" style="font-size: 0.8rem;"></i>
</button>
</div>
</div>
<!-- Seconda riga: +, M, MacroMatrice, Matrice globale, Propaga -->
@ -34,7 +34,7 @@
<table class="table table-striped table-sm" id="partsTable">
<thead>
<tr>
<th style="width: 80px;">Numero</th>
<th style="width: 55px;">Num</th>
<th>Descrizione</th>
<th style="width: 200px;">Matrice</th>
<th style="width: 150px;">
@ -51,7 +51,7 @@
</thead>
<tbody id="partsTableBody">
<tr data-part-id="new">
<td><input type="number" class="form-control form-control-sm part-number" value="1" style="width: 80px;"></td>
<td><input type="number" class="form-control form-control-sm part-number" value="1" style="width: 55px;"></td>
<td><input type="text" class="form-control form-control-sm part-description" placeholder="Inserisci descrizione"></td>
<td>
<div style="display: flex; align-items: center;">
@ -151,11 +151,11 @@
<div class="modal-dialog modal-md">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addQuotationModalLabel">Choose a quotation</h5>
<h5 class="modal-title" id="addQuotationModalLabel">Choose a quotation</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<select id="addQuotationSelect" class="form-control form-control-sm" style="width: 100% !important; min-width: 100% !important"></select>
<select id="addQuotationSelect" class="form-control form-control-sm" style="width: 100% !important; min-width: 100% !important"></select>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Annulla</button>
@ -180,13 +180,13 @@
max-width: 100% !important
}
#addQuotationModal {
z-index: 1060 !important
}
#addQuotationModal {
z-index: 1060 !important
}
#addQuotationModal .modal-backdrop {
z-index: 1055 !important
}
#addQuotationModal .modal-backdrop {
z-index: 1055 !important
}
/* Tabelle */
#partsTable tr {
@ -287,9 +287,13 @@
/* Colonna Descrizione (2ª colonna) = 420px */
#partsTable th:nth-child(2),
#partsTable td:nth-child(2) {
width: 350 !important;
min-width: 350px !important;
max-width: 350px !important;
width: 300px !important;
min-width: 300px !important;
max-width: 300px !important;
}
#partsTable .part-number {
text-align: center;
}
#partsTable td:nth-child(2) .part-description {

View File

@ -6,7 +6,9 @@ $(document).ready(function () {
let unsavedChanges = false;
let matrici = [];
let macroMatrici = [];
let quotations = [];
let quotations = [];
let partsExtraField = null; // {field_id, field_label} oppure null
let extraFieldOptions = []; // [{id,label}]
// --- ROW ID helpers: niente più cache impazzita di jQuery .data() ---
function getPartId($row) {
@ -29,6 +31,148 @@ $(document).ready(function () {
return id === "new" || id === "" || id === null ? null : id;
}
function loadPartsExtraField(iddatadb, done) {
partsExtraField = null;
if (!iddatadb) return done();
$.ajax({
url: "get_parts_extra_field.php",
method: "GET",
dataType: "json",
data: { iddatadb },
success: function (res) {
partsExtraField =
res && res.success && res.field ? res.field : null;
if (
partsExtraField &&
(partsExtraField.data_type || "").toLowerCase() ===
"sceltamultipla"
) {
loadExtraFieldOptions(
String(partsExtraField.field_id),
function () {
applyExtraFieldColumn();
done();
},
);
} else {
applyExtraFieldColumn();
done();
}
},
error: function () {
partsExtraField = null;
applyExtraFieldColumn();
done();
},
});
}
function loadExtraFieldOptions(fieldId, done) {
extraFieldOptions = [];
if (!fieldId) return done();
$.ajax({
url: "get_customfield_values.php",
method: "GET",
dataType: "json",
data: { field_ids: fieldId },
success: function (res) {
const arr = res && res[fieldId] ? res[fieldId] : [];
extraFieldOptions = (arr || []).map((x) => ({
id: x.IdCustomFieldsValue,
label: x.Valore,
}));
done();
},
error: function () {
extraFieldOptions = [];
done();
},
});
}
function buildExtraFieldCellHtml($row = null) {
if (!partsExtraField) return "";
const selectedValueId = $row ? $row.data("extra-value-id") || "" : "";
// SceltaMultipla -> select + hidden value id
if (
(partsExtraField.data_type || "").toLowerCase() === "sceltamultipla"
) {
const opts = [`<option value="">Select…</option>`]
.concat(
extraFieldOptions.map(
(o) => `<option value="${o.id}">${o.label}</option>`,
),
)
.join("");
return `
<td class="extra-field-td" style="width:200px;">
<input type="hidden" class="part-extra-field-id" value="${partsExtraField.field_id}">
<input type="hidden" class="part-extra-value-id" value="">
<select class="form-control form-control-sm part-extra-select" data-selected="${selectedValueId || ""}">${opts}</select>
</td>`;
}
// Testo -> input + hidden field_id
return `
<td class="extra-field-td" style="width:200px;">
<input type="hidden" class="part-extra-field-id" value="${partsExtraField.field_id}">
<input type="text" class="form-control form-control-sm part-extra-field" data-type="${partsExtraField.data_type || ""}">
</td>`;
}
$(document).on("change", ".part-extra-select", function () {
const valId = $(this).val() || "";
const $row = $(this).closest("tr");
// salva nello stato della riga (così sopravvive ai re-render)
$row.data("extra-value-id", valId);
$(this).closest("td").find(".part-extra-value-id").val(valId);
saveRow($row);
});
function applyExtraFieldColumn() {
const $theadRow = $("#partsTable thead tr");
// 1) rimuovi header extra
$theadRow.find("th.extra-field-th").remove();
// 2) rimuovi celle extra esistenti
$("#partsTableBody tr").each(function () {
$(this).find("td.extra-field-td").remove();
});
// 3) se non c'è campo extra -> fine
if (!partsExtraField) return;
// 4) inserisci header prima di "Azioni" (ultima colonna)
$theadRow
.find("th:last")
.before(
`<th class="extra-field-th" style="width:200px;">${partsExtraField.field_label}</th>`,
);
// 5) aggiungi cella a ogni riga già presente (una sola volta)
$("#partsTableBody tr").each(function () {
const $row = $(this);
$row.find("td:last").before(buildExtraFieldCellHtml($row));
// ripristina selezione se già presente nello stato riga
const selected = ($row.data("extra-value-id") || "").toString();
if (selected) {
$row.find(".part-extra-value-id").val(selected);
$row.find(".part-extra-select").val(selected);
}
});
}
function setPartId($row, id) {
if (!id) return;
// Sincronizza TUTTO: attributo + cache jQuery + nostra cache
@ -153,15 +297,19 @@ $(document).ready(function () {
matrici = data.value || [];
loadMacroMatrici();
initializeGlobalSelect2();
loadPhoto(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations, callback);
loadPartsExtraField(iddatadb, function () {
loadPhoto(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations, callback);
});
},
error: function (xhr, status, error) {
matrici = [];
loadMacroMatrici();
initializeGlobalSelect2();
loadPhoto(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations, callback);
loadPartsExtraField(iddatadb, function () {
loadPhoto(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations, callback);
});
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel caricamento delle matrici: ' +
error +
@ -180,12 +328,16 @@ $(document).ready(function () {
} else {
loadMacroMatrici();
initializeGlobalSelect2();
loadPhoto(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations, callback);
loadPartsExtraField(iddatadb, function () {
loadPhoto(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations, callback);
});
}
} else {
loadPhoto(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations, callback);
loadPartsExtraField(iddatadb, function () {
loadPhoto(iddatadb, idquotations);
loadExistingParts(iddatadb, idquotations, callback);
});
}
}
@ -514,6 +666,11 @@ $(document).ready(function () {
if (response.new_id) return response.new_id;
if (response.partId) return response.partId;
// ✅ RISPOSTA DI save_parts.php: { success:true, results:[{part_id:...}] }
if (Array.isArray(response.results) && response.results[0]) {
if (response.results[0].part_id) return response.results[0].part_id;
if (response.results[0].id) return response.results[0].id;
}
return null;
}
@ -527,6 +684,13 @@ $(document).ready(function () {
const iddatadb = $("#partsModal").data("iddatadb");
const idquotations = $("#partsModal").data("idquotations");
const isMix = partDescription.startsWith("Mix") ? "Y" : "N";
// EXTRA FIELD (0/1)
const extra_field_id = $row.find(".part-extra-field-id").val() || null;
const extra_value_id = $row.find(".part-extra-value-id").val() || null;
const extra_value_text = $row.find(".part-extra-field").length
? ($row.find(".part-extra-field").val() || "").trim()
: null;
let partId = getPartId($row);
if (partId === "new") partId = null; // difesa extra (non dovrebbe più servire, ma sicura)
const endpoint = idquotations
@ -559,8 +723,14 @@ $(document).ready(function () {
part_number: partNumber,
part_description: partDescription,
mix: isMix,
idmatrice: $row.find(".part-matrice").val() || null,
dateexpiry: dateexpiry || null,
note: note,
// extra custom field
extra_field_id: extra_field_id,
extra_value_id: extra_value_id,
extra_value_text: extra_value_text,
},
],
}),
@ -591,7 +761,8 @@ $(document).ready(function () {
$saveStatus.show();
setTimeout(() => $saveStatus.hide(), 2000);
if (!$("#quotationeBtn").hasClass('d-none')) $("#quotationeBtn").addClass("d-none");
if (!$("#quotationeBtn").hasClass("d-none"))
$("#quotationeBtn").addClass("d-none");
} else {
const errorMsg = $(
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio: ' +
@ -720,8 +891,9 @@ $(document).ready(function () {
<select class="part-matrice form-control form-control-sm" style="width: 150px;"></select>
</div>
</td>
<td><input type="date" class="form-control form-control-sm part-dateexpiry" style="width: 130px;"></td>
<td>
<td><input type="date" class="form-control form-control-sm part-dateexpiry" style="width: 130px;"></td>
${partsExtraField ? buildExtraFieldCellHtml(null) : ``}
<td>
<button type="button" class="btn btn-light btn-sm note-btn" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;" title="Aggiungi/Modifica nota"><i class="fas fa-file-alt"></i></button>
<button type="button" class="btn btn-warning btn-sm add-mix-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;">M+</button>
<button type="button" class="btn btn-danger btn-sm remove-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem; display: none;"><i class="fas fa-trash fa-xs"></i></button>
@ -1154,9 +1326,13 @@ $(document).ready(function () {
}
});
$(document).on("blur", ".part-description, .part-number", function () {
saveRow($(this).closest("tr"));
});
$(document).on(
"blur",
".part-description, .part-number, .part-extra-field",
function () {
saveRow($(this).closest("tr"));
},
);
function loadExistingParts(iddatadb, idquotations, callback = null) {
const endpoint = idquotations
@ -1208,8 +1384,9 @@ $(document).ready(function () {
<select class="part-matrice form-control form-control-sm" style="width: 150px;" ${idquotations && !part.idmatrice ? "disabled" : ""}></select>
</div>
</td>
<td><input type="date" class="form-control form-control-sm part-dateexpiry" value="${part.dateexpiry || ""}" style="width: 130px;"></td>
<td>
<td><input type="date" class="form-control form-control-sm part-dateexpiry" value="${part.dateexpiry || ""}" style="width: 130px;"></td>
${partsExtraField ? buildExtraFieldCellHtml() : ``}
<td>
<button type="button" class="btn btn-light btn-sm note-btn ${part.note ? "has-note" : ""}" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;" title="Aggiungi/Modifica nota"><i class="fas fa-file-alt"></i></button>
<button type="button" class="btn btn-warning btn-sm add-mix-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;">M+</button>
<button type="button" class="btn btn-danger btn-sm remove-row" style="padding: 0.1rem 0.3rem; font-size: 0.8rem;"><i class="fas fa-trash fa-xs"></i></button>
@ -1218,6 +1395,20 @@ $(document).ready(function () {
</td>
</tr>`;
$("#partsTableBody").append(newRow);
const $row = $(
`#partsTableBody tr[data-part-id="${part.id}"]`,
);
if (
part.extra_value_id !== undefined &&
part.extra_value_id !== null
) {
const vid = String(part.extra_value_id);
$row.data("extra-value-id", vid);
$row.find(".part-extra-value-id").val(vid);
$row.find(".part-extra-select").val(vid);
}
const $select = $("#partsTableBody").find(
`tr[data-part-id="${part.id}"] .part-matrice`,
);
@ -1236,11 +1427,11 @@ $(document).ready(function () {
});
} else {
addNewRow(1, false); // Riga iniziale normale
$("#quotationeBtn").removeClass("d-none");
$("#quotationeBtn").removeClass("d-none");
}
updateRowButtons();
if (callback) callback();
if (callback) callback();
},
error: function (xhr, status, error) {
const errorMsg = $(
@ -1345,7 +1536,7 @@ $(document).ready(function () {
partId,
currentValue,
selectedMacro,
true
true,
);
});
});
@ -1422,7 +1613,7 @@ $(document).ready(function () {
partId,
idmatrice,
selectedMacro = null,
fromFilter = false
fromFilter = false,
) {
if (typeof $.fn.select2 === "undefined") {
$select.replaceWith(
@ -1493,23 +1684,23 @@ $(document).ready(function () {
true,
);
if (!fromFilter) $select.append(option).trigger("change");
else $select.append(option);
if (!fromFilter) $select.append(option).trigger("change");
else $select.append(option);
partMatrice[partNumber] = matrice.IdMatrice;
} else {
// Aggiusta valore non valido
if (!fromFilter) $select.val(null).trigger("change");
if (!fromFilter) $select.val(null).trigger("change");
partMatrice[partNumber] = null;
}
} else {
$select.val(null).trigger("change", [{ skipHandler: true }]);
}
$select.val(null).trigger("change", [{ skipHandler: true }]);
}
$select.on("change", function (event, data) {
if (data && data?.skipHandler) return;
if (data && data?.skipHandler) return;
const idmatrice = $(this).val();
const $row = $(this).closest("tr");
const partId = $row.data("part-id");
@ -1794,121 +1985,140 @@ $(document).ready(function () {
// Esporta la funzione loadParts per essere usata da import_Edit2.php
window.loadParts = loadParts;
$(document).on("click", "#quotationeBtn", function () {
$("#addQuotationModal").modal("show");
$(document).on("click", "#quotationeBtn", function () {
$("#addQuotationModal").modal("show");
if (quotations.length > 0) {
reloadQuotations();
} else {
$.ajax({
url: "load_quotations.php",
method: "GET",
success: function (response) {
quotations = response?.quotations || [];
if (quotations.length > 0) {
reloadQuotations();
} else {
$.ajax({
url: "load_quotations.php",
method: "GET",
success: function (response) {
quotations = response?.quotations || [];
reloadQuotations();
},
error: function (xhr, status, error) {
console.error("Errore AJAX caricamento quotations:", error, xhr.responseText);
let message = `
reloadQuotations();
},
error: function (xhr, status, error) {
console.error(
"Errore AJAX caricamento quotations:",
error,
xhr.responseText,
);
let message = `
<div class="alert alert-danger temp-alert" role="alert">
Errore nel caricamento delle quotations: ${error} (${xhr.status})
</div>
`;
$("#partsModal .modal-body").prepend(errorMsg);
$("#partsModal .modal-body").prepend(errorMsg);
setTimeout(function () {
message.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
}
});
}
});
setTimeout(function () {
message.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
},
});
}
});
$(document).on("click", "#addQuotationBtn", function () {
const quotationId = $("#addQuotationSelect").val();
if (quotationId && confirm("Confermo di collegare la quotazione al campione?")) {
$("#addQuotationModal").modal("hide");
$(document).on("click", "#addQuotationBtn", function () {
const quotationId = $("#addQuotationSelect").val();
loadParts(null, quotationId, () => {
$("#quotationeBtn").addClass("d-none");
if (
quotationId &&
confirm("Confermo di collegare la quotazione al campione?")
) {
$("#addQuotationModal").modal("hide");
setTimeout(() => {
$("#partsTableBody tr").each(function () {
$(this).find(".save-loading").show();
});
loadParts(null, quotationId, () => {
$("#quotationeBtn").addClass("d-none");
let photoList = $('#photoSelector option').map(function () {
return this.value?.split('/')?.pop();
}).get();
setTimeout(() => {
$("#partsTableBody tr").each(function () {
$(this).find(".save-loading").show();
});
if (!photoList.length) {
let path = $('#samplePhoto').attr("src")?.split('/')?.pop();
if (path) photoList = [path];
}
let photoList = $("#photoSelector option")
.map(function () {
return this.value?.split("/")?.pop();
})
.get();
$.ajax({
url: "save_parts_photo_iddatadb.php",
method: "POST",
data: JSON.stringify({
iddatadb: $("#partsModal").data("iddatadb"),
photoList,
partIds: $('#partsTableBody tr').map(function () {
return $(this).data("part-id");
}).get(),
}),
success: function () {
$("#partsTableBody tr").each(function () {
let $row = $(this);
let $saveStatus = $row.find(".save-status");
let $saveLoading = $row.find(".save-loading");
if (!photoList.length) {
let path = $("#samplePhoto")
.attr("src")
?.split("/")
?.pop();
$saveLoading.hide();
$saveStatus.show();
if (path) photoList = [path];
}
setTimeout(() => $saveStatus.hide(), 2000);
});
}, error: function (xhr, status, error) {
let message = `
$.ajax({
url: "save_parts_photo_iddatadb.php",
method: "POST",
data: JSON.stringify({
iddatadb: $("#partsModal").data("iddatadb"),
photoList,
partIds: $("#partsTableBody tr")
.map(function () {
return $(this).data("part-id");
})
.get(),
}),
success: function () {
$("#partsTableBody tr").each(function () {
let $row = $(this);
let $saveStatus = $row.find(".save-status");
let $saveLoading = $row.find(".save-loading");
$saveLoading.hide();
$saveStatus.show();
setTimeout(() => $saveStatus.hide(), 2000);
});
},
error: function (xhr, status, error) {
let message = `
<div class="alert alert-danger temp-alert" role="alert">
Errore di salvataggio: ${error} (${xhr.status})
</div>
`;
$("#partsModal .modal-body").prepend(message);
$("#partsModal .modal-body").prepend(message);
setTimeout(function () {
message.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
}
});
}, 100);
});
}
});
setTimeout(function () {
message.fadeOut(500, function () {
$(this).remove();
});
}, 5000);
},
});
}, 100);
});
}
});
function reloadQuotations() {
if (quotations.length > 0) {
$("#addQuotationSelect").empty();
function reloadQuotations() {
if (quotations.length > 0) {
$("#addQuotationSelect").empty();
$("#addQuotationSelect").append("<option value=''>Seleziona Quotation</option>");
quotations.forEach(quotation => {
if (quotation?.description) {
$("#addQuotationSelect").append(`<option value='${quotation?.id}'>${quotation?.description}</option>`);
}
});
$("#addQuotationSelect").append(
"<option value=''>Seleziona Quotation</option>",
);
$("#addQuotationSelect").select2();
}
}
quotations.forEach((quotation) => {
if (quotation?.description) {
$("#addQuotationSelect").append(
`<option value='${quotation?.id}'>${quotation?.description}</option>`,
);
}
});
$("#addQuotationSelect").select2();
}
}
});
$(document).on("change", ".propagate-date-input", function () {
@ -2136,15 +2346,21 @@ $(document).on("click", ".save-common-note-btn", function () {
$(document).on("click", "#showHideImageBtn", function () {
let mainRow = $(this).closest(".parts-row");
let photoContainer = mainRow.find(".col-md-3");
let tableContainer = mainRow.find("#partsTable").closest("div[class*='col-md']");
let tableContainer = mainRow
.find("#partsTable")
.closest("div[class*='col-md']");
if (photoContainer.hasClass("d-none")) {
photoContainer.removeClass("d-none");
tableContainer.removeClass("col-md-12").addClass("col-md-9");
$(this).html("<i class='fas fa-eye-slash' style='font-size: 0.8rem;'></i><i class='fas fa-image ms-1' style='font-size: 0.8rem;'></i>");
$(this).html(
"<i class='fas fa-eye-slash' style='font-size: 0.8rem;'></i><i class='fas fa-image ms-1' style='font-size: 0.8rem;'></i>",
);
} else {
photoContainer.addClass("d-none");
tableContainer.removeClass("col-md-9").addClass("col-md-12");
$(this).html("<i class='fas fa-eye' style='font-size: 0.8rem;'></i><i class='fas fa-image ms-1' style='font-size: 0.8rem;'></i>");
$(this).html(
"<i class='fas fa-eye' style='font-size: 0.8rem;'></i><i class='fas fa-image ms-1' style='font-size: 0.8rem;'></i>",
);
}
});
});

View File

@ -4,6 +4,7 @@ include('include/headscript.php');
$dbHandler = DBHandlerSelect::getInstance();
$pdo = $dbHandler->getConnection();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$data = json_decode(file_get_contents('php://input'), true);
@ -19,6 +20,21 @@ try {
$pdo->beginTransaction();
$results = [];
// Custom fields statements (child table)
$stmtUpsertCF = $pdo->prepare("
INSERT INTO identification_parts_customfields (part_id, field_id, value_id, value_text)
VALUES (:part_id, :field_id, :value_id, :value_text)
ON DUPLICATE KEY UPDATE
value_id = VALUES(value_id),
value_text = VALUES(value_text),
updated_at = NOW()
");
$stmtDeleteCF = $pdo->prepare("
DELETE FROM identification_parts_customfields
WHERE part_id = :part_id AND field_id = :field_id
");
foreach ($parts as $part) {
$partId = $part['id'] ?? null;
$partNumber = $part['part_number'] ?? null;
@ -28,50 +44,123 @@ try {
$note = $part['note'] ?? null;
$dateexpiry = $part['dateexpiry'] ?? null;
if ($partDescription || $note || $dateexpiry) {
if ($partId) {
// UPDATE se la parte esiste
$stmt = $pdo->prepare("UPDATE identification_parts
SET part_number = :part_number,
part_description = :part_description,
mix = :mix,
idmatrice = :idmatrice,
note = :note,
dateexpiry = :dateexpiry,
updated_at = NOW()
WHERE id = :id");
$stmt->execute([
':id' => $partId,
':part_number' => $partNumber,
':part_description' => $partDescription,
':mix' => $mix,
':idmatrice' => $idmatrice,
':note' => $note,
':dateexpiry' => $dateexpiry,
]);
$results[] = ['part_id' => $partId, 'part_number' => $partNumber, 'message' => 'Parte aggiornata con successo'];
} else {
// INSERT per nuova parte
$stmt = $pdo->prepare("INSERT INTO identification_parts
(iddatadb, part_number, part_description, mix, idmatrice, note, dateexpiry, created_at, updated_at)
VALUES (:iddatadb, :part_number, :part_description, :mix, :idmatrice, :note, :dateexpiry, NOW(), NOW())");
$stmt->execute([
':iddatadb' => $iddatadb,
':part_number' => $partNumber,
':part_description' => $partDescription,
':mix' => $mix,
':idmatrice' => $idmatrice,
':note' => $note,
':dateexpiry' => $dateexpiry,
]);
$newId = $pdo->lastInsertId();
$results[] = ['part_id' => $newId, 'part_number' => $partNumber, 'message' => 'Parte salvata con successo'];
// Extra field (0/1)
$extraFieldId = $part['extra_field_id'] ?? null;
$extraValueId = $part['extra_value_id'] ?? null;
$extraValueText = $part['extra_value_text'] ?? null;
// Normalizza vuoti
if ($extraFieldId !== null && $extraFieldId !== '') $extraFieldId = (int)$extraFieldId;
else $extraFieldId = null;
if ($extraValueId !== null && $extraValueId !== '') $extraValueId = (int)$extraValueId;
else $extraValueId = null;
if ($extraValueText !== null) {
$extraValueText = trim((string)$extraValueText);
if ($extraValueText === '') $extraValueText = null;
}
if ($partId) {
// UPDATE se la parte esiste (sempre)
$stmt = $pdo->prepare("UPDATE identification_parts
SET part_number = :part_number,
part_description = :part_description,
mix = :mix,
idmatrice = :idmatrice,
note = :note,
dateexpiry = :dateexpiry,
updated_at = NOW()
WHERE id = :id");
$stmt->execute([
':id' => $partId,
':part_number' => $partNumber,
':part_description' => $partDescription,
':mix' => $mix,
':idmatrice' => $idmatrice,
':note' => $note,
':dateexpiry' => $dateexpiry,
]);
// Save extra custom field (if provided)
if ($extraFieldId !== null) {
if ($extraValueId === null && $extraValueText === null) {
$stmtDeleteCF->execute([
':part_id' => $partId,
':field_id' => $extraFieldId,
]);
} else {
$stmtUpsertCF->execute([
':part_id' => $partId,
':field_id' => $extraFieldId,
':value_id' => $extraValueId,
':value_text' => $extraValueText,
]);
}
}
$cf_row = null;
if ($extraFieldId !== null) {
$chk = $pdo->prepare("SELECT id, value_id, value_text FROM identification_parts_customfields WHERE part_id = ? AND field_id = ?");
$chk->execute([$partId, $extraFieldId]);
$cf_row = $chk->fetch(PDO::FETCH_ASSOC);
}
$results[] = [
'part_id' => $partId,
'part_number' => $partNumber,
'message' => 'Parte aggiornata con successo',
'cf_row' => $cf_row
];
} else if ($partDescription || $note || $dateexpiry) {
// INSERT per nuova parte (solo se ha contenuto)
$stmt = $pdo->prepare("INSERT INTO identification_parts
(iddatadb, part_number, part_description, mix, idmatrice, note, dateexpiry, created_at, updated_at)
VALUES (:iddatadb, :part_number, :part_description, :mix, :idmatrice, :note, :dateexpiry, NOW(), NOW())");
$stmt->execute([
':iddatadb' => $iddatadb,
':part_number' => $partNumber,
':part_description' => $partDescription,
':mix' => $mix,
':idmatrice' => $idmatrice,
':note' => $note,
':dateexpiry' => $dateexpiry,
]);
$newId = (int)$pdo->lastInsertId();
if ($extraFieldId !== null) {
if ($extraValueId === null && $extraValueText === null) {
$stmtDeleteCF->execute([
':part_id' => $newId,
':field_id' => $extraFieldId,
]);
} else {
$stmtUpsertCF->execute([
':part_id' => $newId,
':field_id' => $extraFieldId,
':value_id' => $extraValueId,
':value_text' => $extraValueText,
]);
}
}
$results[] = [
'part_id' => $newId,
'part_number' => $partNumber,
'message' => 'Parte salvata con successo'
];
}
}
$pdo->commit();
echo json_encode(['success' => true, 'results' => $results]);
echo json_encode([
'success' => true,
'results' => $results,
'debug_last' => [
'extra_field_id' => $extraFieldId ?? null,
'extra_value_id' => $extraValueId ?? null,
'extra_value_text' => $extraValueText ?? null,
'part_id' => $partId ?? ($newId ?? null),
]
]);
} catch (PDOException $e) {
$pdo->rollBack();
echo json_encode(['success' => false, 'message' => 'Errore nel salvataggio: ' . $e->getMessage()]);

View File

@ -21,11 +21,37 @@ $value = ($value === 1) ? 1 : 0;
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$stmt = $pdo->prepare("UPDATE template_mapping SET is_visible_parts = ? WHERE id = ? AND template_id = ?");
$result = $stmt->execute([$value, $mapping_id, $template_id]);
try {
$pdo->beginTransaction();
if ($result) {
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'message' => 'Failed to update is_visible_parts']);
if ($value === 1) {
// 1) Force ONLY ONE row visible_parts per template:
// set all to 0 for this template
$stmtReset = $pdo->prepare("
UPDATE template_mapping
SET is_visible_parts = 0
WHERE template_id = ?
");
$stmtReset->execute([$template_id]);
}
// 2) Set requested mapping to 1 or 0
$stmtSet = $pdo->prepare("
UPDATE template_mapping
SET is_visible_parts = ?
WHERE id = ? AND template_id = ?
");
$stmtSet->execute([$value, $mapping_id, $template_id]);
$pdo->commit();
echo json_encode([
'success' => true,
'template_id' => $template_id,
'mapping_id' => $mapping_id,
'value' => $value
]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}