diff --git a/public/userarea/clone_parts_to_visible.php b/public/userarea/clone_parts_to_visible.php new file mode 100644 index 0000000..b3fa6a0 --- /dev/null +++ b/public/userarea/clone_parts_to_visible.php @@ -0,0 +1,131 @@ +getConnection(); +$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +$data = json_decode(file_get_contents('php://input'), true); + +$sourceIddatadb = isset($data['source_iddatadb']) ? (int)$data['source_iddatadb'] : 0; +$targetList = $data['target_iddatadb_list'] ?? []; + +$targetIds = array_values(array_unique(array_filter(array_map('intval', (array)$targetList), function ($v) use ($sourceIddatadb) { + return $v > 0 && $v !== $sourceIddatadb; +}))); + +if ($sourceIddatadb <= 0 || empty($targetIds)) { + echo json_encode([ + 'success' => false, + 'message' => 'Missing source or target records' + ]); + exit; +} + +try { + $pdo->beginTransaction(); + + // 1. Load source parts + $stmtParts = $pdo->prepare(" + SELECT id, part_number, part_description, mix, idmatrice, note, dateexpiry + FROM identification_parts + WHERE iddatadb = ? + ORDER BY part_number ASC, id ASC + "); + $stmtParts->execute([$sourceIddatadb]); + $sourceParts = $stmtParts->fetchAll(PDO::FETCH_ASSOC); + + if (empty($sourceParts)) { + $pdo->rollBack(); + echo json_encode([ + 'success' => false, + 'message' => 'No parts found for source record' + ]); + exit; + } + + // 2. Prepare statements + $stmtInsertPart = $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()) + "); + + $stmtLoadCF = $pdo->prepare(" + SELECT field_id, value_id, value_text + FROM identification_parts_customfields + WHERE part_id = ? + ORDER BY id ASC + "); + + $stmtInsertCF = $pdo->prepare(" + INSERT INTO identification_parts_customfields + (part_id, field_id, value_id, value_text, created_at, updated_at) + VALUES + (:part_id, :field_id, :value_id, :value_text, NOW(), NOW()) + "); + + $details = []; + $totalClonedParts = 0; + + // 3. Clone source parts to each target record + foreach ($targetIds as $targetIddatadb) { + $clonedCountForTarget = 0; + + foreach ($sourceParts as $part) { + $stmtInsertPart->execute([ + ':iddatadb' => $targetIddatadb, + ':part_number' => $part['part_number'], + ':part_description' => $part['part_description'], + ':mix' => $part['mix'] ?? 'N', + ':idmatrice' => $part['idmatrice'] !== '' ? $part['idmatrice'] : null, + ':note' => $part['note'] ?? null, + ':dateexpiry' => $part['dateexpiry'] ?? null, + ]); + + $newPartId = (int)$pdo->lastInsertId(); + + // Load source custom fields for this part + $stmtLoadCF->execute([(int)$part['id']]); + $customFields = $stmtLoadCF->fetchAll(PDO::FETCH_ASSOC); + + foreach ($customFields as $cf) { + $stmtInsertCF->execute([ + ':part_id' => $newPartId, + ':field_id' => (int)$cf['field_id'], + ':value_id' => ($cf['value_id'] !== null && $cf['value_id'] !== '') ? (int)$cf['value_id'] : null, + ':value_text' => $cf['value_text'] !== '' ? $cf['value_text'] : null, + ]); + } + + $clonedCountForTarget++; + $totalClonedParts++; + } + + $details[] = [ + 'target_iddatadb' => $targetIddatadb, + 'cloned_parts' => $clonedCountForTarget + ]; + } + + $pdo->commit(); + + echo json_encode([ + 'success' => true, + 'source_iddatadb' => $sourceIddatadb, + 'cloned_targets' => count($targetIds), + 'total_cloned_parts' => $totalClonedParts, + 'details' => $details + ]); +} catch (Throwable $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + + echo json_encode([ + 'success' => false, + 'message' => 'Clone failed: ' . $e->getMessage() + ]); +} diff --git a/public/userarea/modal_partsTable.php b/public/userarea/modal_partsTable.php index 886d860..cb26f7a 100644 --- a/public/userarea/modal_partsTable.php +++ b/public/userarea/modal_partsTable.php @@ -26,7 +26,9 @@ - + diff --git a/public/userarea/modals_gridData.js b/public/userarea/modals_gridData.js index 5960ec5..387edde 100644 --- a/public/userarea/modals_gridData.js +++ b/public/userarea/modals_gridData.js @@ -2,93 +2,111 @@ * modals_gridData.js — Photos, Parts, Tested Component handlers for gridData pages */ (function () { - 'use strict'; + "use strict"; // ── Photos — use photos.js loadPopupContent (exported to window) ── - $(document).on('click', '.photos-btn', function () { - const iddatadb = $(this).data('iddatadb') || null; - const idquotations = $(this).data('idquotations') || null; - const modal = document.getElementById('photosModal'); + $(document).on("click", ".photos-btn", function () { + const iddatadb = $(this).data("iddatadb") || null; + const idquotations = $(this).data("idquotations") || null; + const modal = document.getElementById("photosModal"); if (!modal) return; - modal.style.display = 'block'; + modal.style.display = "block"; - if (typeof window.loadPopupContent === 'function') { + if (typeof window.loadPopupContent === "function") { window.loadPopupContent(iddatadb, idquotations); } }); // Close photos modal - $(document).on('click', '.close-btn', function () { - const modal = document.getElementById('photosModal'); - if (modal) modal.style.display = 'none'; + $(document).on("click", ".close-btn", function () { + const modal = document.getElementById("photosModal"); + if (modal) modal.style.display = "none"; }); // Close on backdrop click - $(document).on('click', '#photosModal', function (e) { + $(document).on("click", "#photosModal", function (e) { if (e.target === this) { - this.style.display = 'none'; + this.style.display = "none"; } }); // ── Parts (matching import_edit2.php behavior) ───────────────────── - $(document).on('click', '.parts-btn', function () { - const iddatadb = $(this).data('iddatadb') || null; - const idquotations = $(this).data('idquotations') || null; + $(document).on("click", ".parts-btn", function () { + const iddatadb = $(this).data("iddatadb") || null; + const idquotations = $(this).data("idquotations") || null; $.ajax({ - url: 'modal_partsTable.php', - method: 'GET', + url: "modal_partsTable.php", + method: "GET", data: { iddatadb: iddatadb }, success: function (response) { - $('#partsModalContainer').html(response); - const modalElement = document.getElementById('partsModal'); + $("#partsModalContainer").html(response); + const modalElement = document.getElementById("partsModal"); if (!modalElement) return; - $("#trfHeader").text(iddatadb || idquotations || ''); - $("#partsModal").data("iddatadb", iddatadb).data("idquotations", idquotations); + $("#trfHeader").text(iddatadb || idquotations || ""); + + const visibleIddatadbList = Array.isArray(window.gridData) + ? window.gridData + .map((r) => parseInt(r.iddatadb, 10)) + .filter((v) => !isNaN(v) && v > 0) + : []; + + $("#partsModal") + .data("iddatadb", iddatadb) + .data("idquotations", idquotations) + .data("visible-iddatadb-list", visibleIddatadbList); let modal = bootstrap.Modal.getInstance(modalElement); - if (!modal) modal = new bootstrap.Modal(modalElement, { backdrop: true, keyboard: true, focus: true }); + if (!modal) + modal = new bootstrap.Modal(modalElement, { + backdrop: true, + keyboard: true, + focus: true, + }); modal.show(); - if (typeof window.loadParts === 'function') { + if (typeof window.loadParts === "function") { window.loadParts(iddatadb, idquotations); } }, error: function (xhr, status, error) { - console.error('Error loading parts:', error); - } + console.error("Error loading parts:", error); + }, }); }); - $(document).on('hidden.bs.modal', '#partsModal', function () { - const modalElement = document.getElementById('partsModal'); + $(document).on("hidden.bs.modal", "#partsModal", function () { + const modalElement = document.getElementById("partsModal"); if (modalElement) { const modal = bootstrap.Modal.getInstance(modalElement); if (modal) modal.dispose(); } - $('#partsModalContainer').empty(); - $('.modal-backdrop').remove(); - $('body').removeClass('modal-open').css('padding-right', ''); + $("#partsModalContainer").empty(); + $(".modal-backdrop").remove(); + $("body").removeClass("modal-open").css("padding-right", ""); }); // ── Tested Component quick add ─────────────────────────────────────── - $(document).on('click', '.add-part-btn', async function () { - const iddatadb = $(this).data('iddatadb') || null; - const rowIndex = parseInt($(this).data('row')); + $(document).on("click", ".add-part-btn", async function () { + const iddatadb = $(this).data("iddatadb") || null; + const rowIndex = parseInt($(this).data("row")); const row = window.gridData?.[rowIndex]; const id = iddatadb || (row ? row.iddatadb : null); if (!id) return; - const $cell = $(this).closest('.grid-cell, div'); - const $input = $cell.find('input'); - const raw = ($input.val() || '').trim(); - const parts = raw.split('|').map(s => s.trim()).filter(s => s.length > 0); + const $cell = $(this).closest(".grid-cell, div"); + const $input = $cell.find("input"); + const raw = ($input.val() || "").trim(); + const parts = raw + .split("|") + .map((s) => s.trim()) + .filter((s) => s.length > 0); const uniqueParts = [...new Set(parts)]; if (!uniqueParts.length) { - alert('Insert a description first.'); + alert("Insert a description first."); $input.focus(); return; } @@ -96,14 +114,17 @@ try { for (const p of uniqueParts) { const formData = new FormData(); - formData.append('iddatadb', id); - formData.append('part_description', p); - await fetch('add_part_quick.php', { method: 'POST', body: formData }); + formData.append("iddatadb", id); + formData.append("part_description", p); + await fetch("add_part_quick.php", { + method: "POST", + body: formData, + }); } alert(`Added ${uniqueParts.length} part(s).`); - $input.val(''); + $input.val(""); } catch (e) { - alert('Error: ' + e.message); + alert("Error: " + e.message); } }); @@ -111,44 +132,47 @@ let deleteIddatadb = null; let deleteRowIndex = null; - $(document).on('click', '.delete-btn', function () { - deleteIddatadb = $(this).data('iddatadb'); - deleteRowIndex = parseInt($(this).data('row')); - const modalEl = document.getElementById('deleteConfirmModal'); + $(document).on("click", ".delete-btn", function () { + deleteIddatadb = $(this).data("iddatadb"); + deleteRowIndex = parseInt($(this).data("row")); + const modalEl = document.getElementById("deleteConfirmModal"); if (modalEl) { - document.getElementById('deleteIddatadbText').textContent = deleteIddatadb; + document.getElementById("deleteIddatadbText").textContent = + deleteIddatadb; new bootstrap.Modal(modalEl).show(); } }); - $(document).on('click', '#deleteConfirmBtn', async function () { - const modalEl = document.getElementById('deleteConfirmModal'); + $(document).on("click", "#deleteConfirmBtn", async function () { + const modalEl = document.getElementById("deleteConfirmModal"); const modal = bootstrap.Modal.getInstance(modalEl); if (modal) modal.hide(); if (!deleteIddatadb) return; try { - const resp = await fetch('delete_record.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ id: deleteIddatadb }) + const resp = await fetch("delete_record.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id: deleteIddatadb }), }); const result = await resp.json(); if (result.success) { // Remove from gridData - const idx = window.gridData.findIndex(r => r.iddatadb === deleteIddatadb); + const idx = window.gridData.findIndex( + (r) => r.iddatadb === deleteIddatadb, + ); if (idx >= 0) window.gridData.splice(idx, 1); // Re-render const gr = window.gridRenderer; if (gr) gr.renderVisibleRows(); } else { - alert('Error: ' + result.message); + alert("Error: " + result.message); } } catch (e) { - alert('Error: ' + e.message); + alert("Error: " + e.message); } deleteIddatadb = null; @@ -156,20 +180,26 @@ }); // ── Add new row ────────────────────────────────────────────────────── - $(document).on('click', '#addRowBtn', async function () { + $(document).on("click", "#addRowBtn", async function () { const btn = this; btn.disabled = true; try { const templateId = window.gridMeta?.templateId; - if (!templateId) { alert('Template ID missing'); return; } + if (!templateId) { + alert("Template ID missing"); + return; + } const urlParams = new URLSearchParams(window.location.search); - const importref = urlParams.get('importref') || ''; - const resp = await fetch('add_record.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ template_id: templateId, importreferencecode: importref }) + const importref = urlParams.get("importref") || ""; + const resp = await fetch("add_record.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + template_id: templateId, + importreferencecode: importref, + }), }); const result = await resp.json(); @@ -177,17 +207,20 @@ // Build new row object const newRow = { iddatadb: result.iddatadb, - status: 'i', - idclient: window.gridMeta?.defaultIdclient || '', + status: "i", + idclient: window.gridMeta?.defaultIdclient || "", cliente_fornitore_id: null, commessaweb: null, - user_name: result.user_name || '', - importreferencecode: result.importreferencecode || '', - filename_import: '', - importdate: new Date().toISOString().slice(0, 19).replace('T', ' '), + user_name: result.user_name || "", + importreferencecode: result.importreferencecode || "", + filename_import: "", + importdate: new Date() + .toISOString() + .slice(0, 19) + .replace("T", " "), fixedFields: {}, details: {}, - mainFieldValue: '', + mainFieldValue: "", _dirty: false, }; @@ -199,20 +232,27 @@ if (gr) gr.renderVisibleRows(); // Highlight new row briefly - const newGridRow = document.querySelector(`.grid-row[data-id="${result.iddatadb}"]`); + const newGridRow = document.querySelector( + `.grid-row[data-id="${result.iddatadb}"]`, + ); if (newGridRow) { - newGridRow.classList.add('row-just-created'); - newGridRow.scrollIntoView({ behavior: 'smooth', block: 'center' }); - setTimeout(() => newGridRow.classList.remove('row-just-created'), 4000); + newGridRow.classList.add("row-just-created"); + newGridRow.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + setTimeout( + () => newGridRow.classList.remove("row-just-created"), + 4000, + ); } } else { - alert('Error: ' + (result.message || 'Unknown error')); + alert("Error: " + (result.message || "Unknown error")); } } catch (e) { - alert('Error: ' + e.message); + alert("Error: " + e.message); } finally { btn.disabled = false; } }); - })(); diff --git a/public/userarea/partsTable.js b/public/userarea/partsTable.js index a15b002..b8b148a 100644 --- a/public/userarea/partsTable.js +++ b/public/userarea/partsTable.js @@ -1595,7 +1595,11 @@ $(document).ready(function () { dataType: "json", delay: 150, data: function (params) { - return { q: params.term || "", limit: 20, macro: selectedMacro || "" }; + return { + q: params.term || "", + limit: 20, + macro: selectedMacro || "", + }; }, processResults: function (data) { return { results: data.results || [] }; @@ -1654,7 +1658,11 @@ $(document).ready(function () { dataType: "json", delay: 150, data: function (params) { - return { q: params.term || "", limit: 20, macro: selectedMacro || "" }; + return { + q: params.term || "", + limit: 20, + macro: selectedMacro || "", + }; }, processResults: function (data) { return { results: data.results || [] }; @@ -1800,7 +1808,9 @@ $(document).ready(function () { $("#partsTableBody .part-matrice").each(function () { const $target = $(this); if (!$target.find(`option[value="${globalVal}"]`).length) { - $target.append(new Option(globalText, globalVal, true, true)); + $target.append( + new Option(globalText, globalVal, true, true), + ); } $target.val(globalVal).trigger("change"); }); @@ -1969,7 +1979,109 @@ $(document).ready(function () { ".add-row-global, .add-mix-global, .add-mix-row, .remove-row, .propagate-matrice-btn, .propagate-all-btn, .note-btn", markUnsaved, ); + $(document).on("click", "#clonePartsBtn", function (e) { + e.preventDefault(); + const sourceIddatadb = + parseInt($("#partsModal").data("iddatadb"), 10) || null; + const visibleListRaw = + $("#partsModal").data("visible-iddatadb-list") || []; + + if (!sourceIddatadb) { + const errorMsg = $( + '', + ); + $("#partsModal .modal-body").prepend(errorMsg); + setTimeout(() => { + errorMsg.fadeOut(500, function () { + $(this).remove(); + }); + }, 5000); + return; + } + + const targetIds = (Array.isArray(visibleListRaw) ? visibleListRaw : []) + .map((v) => parseInt(v, 10)) + .filter((v) => !isNaN(v) && v > 0 && v !== sourceIddatadb); + + if (!targetIds.length) { + const errorMsg = $( + '', + ); + $("#partsModal .modal-body").prepend(errorMsg); + setTimeout(() => { + errorMsg.fadeOut(500, function () { + $(this).remove(); + }); + }, 5000); + return; + } + + if ( + !confirm( + `Confermi il clone delle parti del record ${sourceIddatadb} negli altri ${targetIds.length} record visibili?`, + ) + ) { + return; + } + + const $btn = $(this); + const originalHtml = $btn.html(); + $btn.prop("disabled", true).html( + ' Clonazione...', + ); + + $.ajax({ + url: "clone_parts_to_visible.php", + method: "POST", + contentType: "application/json", + dataType: "json", + data: JSON.stringify({ + source_iddatadb: sourceIddatadb, + target_iddatadb_list: targetIds, + }), + success: function (response) { + $btn.prop("disabled", false).html(originalHtml); + + if (response.success) { + const successMsg = $( + ``, + ); + $("#partsModal .modal-body").prepend(successMsg); + setTimeout(() => { + successMsg.fadeOut(500, function () { + $(this).remove(); + }); + }, 5000); + } else { + const errorMsg = $( + ``, + ); + $("#partsModal .modal-body").prepend(errorMsg); + setTimeout(() => { + errorMsg.fadeOut(500, function () { + $(this).remove(); + }); + }, 5000); + } + }, + error: function (xhr, status, error) { + $btn.prop("disabled", false).html(originalHtml); + + const errorMsg = $( + ``, + ); + $("#partsModal .modal-body").prepend(errorMsg); + setTimeout(() => { + errorMsg.fadeOut(500, function () { + $(this).remove(); + }); + }, 5000); + }, + }); + }); // Esporta la funzione loadParts per essere usata da import_Edit2.php window.loadParts = loadParts;