added fixed fields

This commit is contained in:
2026-01-30 12:07:43 +01:00
parent 8838edf3a1
commit 4e4cae1df8
4 changed files with 598 additions and 14 deletions
+412 -1
View File
@@ -74,6 +74,25 @@ $slugMapping = [];
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$slugMapping[$row['mysql_column_name']] = $row['user_friendly_slug'];
}
// ---------------- FIXED FIELDS (from template_fixed_mapping) ----------------
$fixedStmt = $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 = ? AND is_visible_import = 1
ORDER BY id
");
$fixedStmt->execute([$template_id]);
$fixedFields = $fixedStmt->fetchAll(PDO::FETCH_ASSOC);
// helper default (DATE can use 'today' if you already use it elsewhere)
function fixedDefaultValue(array $f): string
{
$v = $f['default_value'] ?? '';
if ($f['data_type'] === 'DATE' && $v === 'today') return date('Y-m-d');
return (string)$v;
}
?>
<!doctype html>
@@ -456,6 +475,21 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 31px;
}
.api-fixed-select+.select2-container .select2-selection__rendered {
color: #333;
}
.api-fixed-select option[value=""] {
color: #999;
font-style: italic;
}
.api-fixed-select.required-input:invalid,
.api-fixed-select[required]:not([value]):not([data-select2-id]) {
background-color: #f8d7da !important;
border-color: #dc3545 !important;
}
</style>
<title>Edit Imported Data - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
</head>
@@ -577,6 +611,68 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
echo "<div class='grid-cell' style='flex: 0 0 150px;'></div>";
echo "<div class='grid-cell' style='flex: 0 0 150px;'></div>";
echo "<div class='grid-cell' style='flex: 0 0 150px;'></div>";
// ---------------- FIXED FIELDS TOP (propagate) ----------------
if (!empty($fixedFields)) {
foreach ($fixedFields as $fx => $f) {
$key = $f['fixed_field_key']; // datadb column
$val = fixedDefaultValue($f);
$isRequired = ((int)$f['is_required'] === 1);
$inputClass = 'manual-input' . ($isRequired ? ' required-input' : '');
$topRequiredClass = ($isRequired && ($val === '' || $val === null)) ? 'missing-required' : '';
echo "<div class='grid-cell {$topRequiredClass}' style='flex: 0 0 180px;'>";
// Forza DATE anche se per errore nel DB è diversa
$isDate = ($f['data_type'] === 'DATE' || $key === 'ConsegnaRichiesta');
if ($isDate) {
echo "<input type='text'
class='custom-field date-picker {$inputClass} fixed-top'
data-column='fixed_{$fx}'
data-fixed-key='" . htmlspecialchars($key, ENT_QUOTES) . "'
value='" . htmlspecialchars((string)$val, ENT_QUOTES) . "'
" . ($isRequired ? "required" : "") . ">";
} else {
$isApiField = in_array($key, [
'MoltiplicatorePrezzo',
'ClienteResponsabile',
'AnagraficaCertestObject',
'AnagraficaCertestService'
], true);
if ($isApiField) {
$placeholder = ($key === 'ClienteResponsabile') ? 'Seleziona cliente prima...' : 'Seleziona...';
echo "<select
class='custom-field dropdown-select api-fixed-select {$inputClass} fixed-top'
data-column='fixed_{$fx}'
data-fixed-key='" . htmlspecialchars($key, ENT_QUOTES) . "'
" . ($isRequired ? "required" : "") . ">
<option value=''>{$placeholder}</option>
</select>";
} else {
echo "<input type='number'
class='custom-field {$inputClass} fixed-top'
data-column='fixed_{$fx}'
data-fixed-key='" . htmlspecialchars($key, ENT_QUOTES) . "'
value='" . htmlspecialchars((string)$val, ENT_QUOTES) . "'
" . ($isRequired ? "required" : "") . ">";
}
}
// UNA SOLA freccia
echo "<button type='button' class='propagate-btn' data-column='fixed_{$fx}'><i class='fas fa-arrow-down'></i></button>";
echo "</div>";
}
}
?>
</div>
@@ -619,6 +715,22 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$headerIndex++;
echo "<div class='grid-header' data-index='$headerIndex' style='flex: 0 0 150px; position: relative;'>" . ($slugMapping['importdate'] ?? 'importdate') . "<div class='resizer'></div></div>";
?>
<?php
// ---------------- FIXED FIELDS HEADERS ----------------
$headerIndex++; // IMPORTANT: advance index after importdate
if (!empty($fixedFields)) {
foreach ($fixedFields as $f) {
$key = $f['fixed_field_key'];
$label = $slugMapping[$key] ?? $key; // if slug exists use it, else key
echo "<div class='grid-header' data-index='$headerIndex' style='flex: 0 0 180px; position: relative;'>"
. htmlspecialchars($label) .
"<div class='resizer'></div></div>";
$headerIndex++;
}
}
?>
</div>
<?php foreach ($importedData as $index => $row): ?>
@@ -768,6 +880,56 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
<span><?= htmlspecialchars($row['importdate']) ?></span>
<input type="hidden" name="rows[<?= $index ?>][importdate]" value="<?= htmlspecialchars($row['importdate']) ?>">
</div>
<?php
// ---------------- FIXED FIELDS CELLS ----------------
$cellIndex++; // IMPORTANT: move to next data-index after importdate cell
if (!empty($fixedFields)) {
foreach ($fixedFields as $f) {
$key = $f['fixed_field_key']; // datadb column name
$val = $row[$key] ?? '';
if ($val === '' || $val === null) {
$val = fixedDefaultValue($f);
}
$requiredClass = ((int)$f['is_required'] === 1 && ($val === '' || $val === null)) ? 'missing-required' : '';
$inputClass = 'manual-input';
if ((int)$f['is_required'] === 1) $inputClass .= ' required-input';
echo "<div class='grid-cell editable-cell $requiredClass'
data-col='" . htmlspecialchars($key, ENT_QUOTES) . "'
data-row='$index'
data-index='$cellIndex'
style='flex: 0 0 180px;'>";
if ($f['data_type'] === 'DATE') {
echo "<input type='text'
name='rows[$index][$key]'
value='" . htmlspecialchars((string)$val, ENT_QUOTES) . "'
class='cell-input date-picker $inputClass fixed-input'
data-fixed-key='" . htmlspecialchars($key, ENT_QUOTES) . "' "
. (((int)$f['is_required'] === 1) ? "required" : "")
. ">";
} else { // INT → diventa select se è uno dei campi API
$isApiField = in_array($key, ['MoltiplicatorePrezzo', 'ClienteResponsabile', 'AnagraficaCertestObject', 'AnagraficaCertestService']);
$selectClass = $isApiField ? 'api-fixed-select' : '';
echo "<select
name='rows[$index][$key]'
class='cell-input $inputClass fixed-input $selectClass'
data-fixed-key='" . htmlspecialchars($key, ENT_QUOTES) . "'
data-current-value='" . htmlspecialchars((string)$val, ENT_QUOTES) . "'
" . (((int)$f['is_required'] === 1) ? "required" : "") . ">
<option value=''>Caricamento...</option>
</select>";
}
echo "</div>";
$cellIndex++;
}
}
?>
</div>
<?php endforeach; ?>
</div>
@@ -910,6 +1072,20 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
if (idclientSelect) {
formData.append('idclient', idclientSelect.value);
}
// ---- FIXED FIELDS (NEW) ----
const fixedInputs = row.querySelectorAll(`input[name^="rows[${rowIndex}]["], select[name^="rows[${rowIndex}]["]`);
fixedInputs.forEach(inp => {
// prendo solo quelli che NON sono details e NON idclient/status/importdate/filename/importreferencecode ecc.
// filtro tramite classe fixed-input (che abbiamo messo)
if (!inp.classList.contains('fixed-input')) return;
const m = inp.name.match(/rows\[\d+\]\[([^\]]+)\]/);
if (m && m[1]) {
formData.append(m[1], inp.value);
}
});
formData.append('iddatadb', iddatadb);
fetch('save_edited_row.php', {
@@ -982,6 +1158,16 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
if (idclientSelect) {
formData.append('idclient', idclientSelect.value);
}
// ---- FIXED FIELDS (NEW) ----
const fixedInputs = row.querySelectorAll(`input[name^="rows[${rowIndex}]["], select[name^="rows[${rowIndex}]["]`);
fixedInputs.forEach(inp => {
if (!inp.classList.contains('fixed-input')) return;
const m = inp.name.match(/rows\[\d+\]\[([^\]]+)\]/);
if (m && m[1]) {
formData.append(m[1], inp.value);
}
});
formData.append('iddatadb', iddatadb);
try {
@@ -1093,6 +1279,10 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
populateClientDropdowns();
clientLoadingStatus.textContent = "Clienti caricati.";
// ✅ force refresh of header dependent fixed fields
$('#clientSelect').trigger('change');
$('.grid-top .api-fixed-select[data-fixed-key="ClienteResponsabile"]').trigger('fixed:reload');
} catch (error) {
clientLoadingStatus.textContent = "Errore nel caricamento.";
console.error("Errore nel caricamento dei client:", error);
@@ -1251,7 +1441,7 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
const dropdownData = {};
async function populateDropdowns() {
const dropdowns = document.querySelectorAll('.dropdown-select:not(.client-select)');
const dropdowns = document.querySelectorAll('.dropdown-select:not(.client-select):not(.api-fixed-select)');
if (dropdowns.length === 0) {
return;
}
@@ -1564,6 +1754,227 @@ foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
});
});
</script>
<script>
// Dati globali caricati una volta sola
let fixedFieldDataCache = {}; // { 'MoltiplicatorePrezzo': [...], 'ClienteResponsabile_4202': [...], ... }
document.addEventListener("DOMContentLoaded", function() {
const apiFields = {
'MoltiplicatorePrezzo': {
endpoint: 'MoltiplicatorePrezzo',
idKey: 'IdMoltiplicatorePrezzo',
textKey: 'Descrizione'
},
'AnagraficaCertestObject': {
endpoint: 'AnagraficaCertestObject',
idKey: 'IdAnagrafica',
textKey: 'NomeAnagrafica'
},
'AnagraficaCertestService': {
endpoint: 'AnagraficaCertestService',
idKey: 'IdAnagrafica',
textKey: 'NomeAnagrafica'
},
'ClienteResponsabile': {
endpoint: 'ClienteResponsabile',
idKey: 'IdClienteResponsabile',
textKey: 'Nominativo',
dependsOn: 'idclient',
getParams: (clientId) => ({
id_cliente: clientId
})
}
};
// ✅ NEW: returns clientId for both row selects and header (grid-top)
function getClientIdForSelect($select) {
const $row = $select.closest('.grid-row');
// If the select is inside a row, read the row client dropdown
if ($row.length) {
return $row.find('select[name$="[idclient]"]').val() || '';
}
// Otherwise it's in the header (grid-top): read the top client select
const topClientId = $('#clientSelect').val();
return topClientId || '';
}
// Funzione per caricare i dati di un campo (una volta sola)
async function loadFixedFieldData(fieldKey, clientId = null) {
const config = apiFields[fieldKey];
if (!config) return [];
const cacheKey = fieldKey + (clientId ? '_' + clientId : '');
if (fixedFieldDataCache[cacheKey]) {
return fixedFieldDataCache[cacheKey];
}
let urlParams = {
field: config.endpoint
};
if (config.dependsOn && clientId) {
Object.assign(urlParams, config.getParams(clientId));
}
try {
const response = await fetch("get_fixed_field_data.php?" + new URLSearchParams(urlParams));
if (!response.ok) throw new Error('HTTP ' + response.status);
const data = await response.json();
let items = [];
if (fieldKey === 'ClienteResponsabile') {
items = data.Responsabili || [];
} else {
items = data.value || data.d?.results || data || [];
}
let results = items.map(item => ({
id: item[config.idKey],
text: (item.Codice ? item.Codice + ' - ' : '') + (item[config.textKey] || 'Senza nome')
}));
fixedFieldDataCache[cacheKey] = results;
return results;
} catch (err) {
console.error('Errore caricamento ' + fieldKey + ':', err);
return [];
}
}
// Carica tutti i campi NON dipendenti all'apertura pagina
async function preloadNonDependentFields() {
for (const [fieldKey, config] of Object.entries(apiFields)) {
if (!config.dependsOn) {
await loadFixedFieldData(fieldKey);
}
}
}
// Inizializza tutte le select
$('.api-fixed-select').each(function() {
const $select = $(this);
const fieldKey = $select.data('fixed-key');
const currentVal = $select.data('current-value') || '';
const config = apiFields[fieldKey];
if (!config) return;
$select.select2({
placeholder: config.dependsOn ? "Seleziona cliente prima..." : "Seleziona...",
allowClear: true,
width: '100%',
data: [], // inizialmente vuota, la riempiamo dopo
minimumInputLength: 0
});
// Funzione per popolare la select con dati già caricati o caricandoli
async function populateSelect() {
let results = [];
if (config.dependsOn) {
const clientId = getClientIdForSelect($select);
if (!clientId) {
// ✅ reset select completely if no client
$select.empty().append(new Option('Seleziona cliente prima...', '', true, true)).trigger('change');
return;
}
let rawData = await fetch("get_fixed_field_data.php?" + new URLSearchParams({
field: config.endpoint,
...config.getParams(clientId)
})).then(r => r.json());
let items = [];
if (fieldKey === 'ClienteResponsabile') {
items = rawData.Responsabili || [];
} else {
items = rawData.value || [];
}
results = items.map(item => ({
id: item[config.idKey],
text: (item.Codice ? item.Codice + ' - ' : '') + (item[config.textKey] || 'Senza nome')
}));
} else {
results = await loadFixedFieldData(fieldKey);
}
$select.select2('destroy').empty().select2({
data: [{
id: '',
text: 'Seleziona...'
}, ...results],
placeholder: "Seleziona...",
allowClear: true,
width: '100%'
});
// Imposta valore iniziale
if (currentVal) {
const found = results.find(r => String(r.id) === String(currentVal));
if (found) {
$select.val(currentVal).trigger('change');
}
}
}
// ✅ NEW: allow external reload (used by header client change)
$select.on('fixed:reload', function() {
populateSelect();
});
// Per campi dipendenti: popola quando cambia cliente (riga o header)
if (config.dependsOn) {
const $row = $select.closest('.grid-row');
if ($row.length) {
// ✅ ROW: bind on row client select
$row.find('select[name$="[idclient]"]').on('change', populateSelect);
// Popola iniziale se cliente già selezionato nella riga
if ($row.find('select[name$="[idclient]"]').val()) {
populateSelect();
}
} else {
// ✅ HEADER (grid-top): bind on top client select
$('#clientSelect').on('change', populateSelect);
// Popola iniziale se cliente già selezionato nellheader
if ($('#clientSelect').val()) {
populateSelect();
}
}
} else {
// Campi non dipendenti: popola subito
populateSelect();
}
});
// ✅ NEW: when top client changes, reload only the header ClienteResponsabile select
$('#clientSelect').on('change', function() {
$('.grid-top .api-fixed-select[data-fixed-key="ClienteResponsabile"]').trigger('fixed:reload');
});
// Precarica i campi indipendenti all'avvio
preloadNonDependentFields();
// Propaga anche per i fixed select
$('.propagate-btn[data-column^="fixed_"]').on('click', function() {
const column = $(this).data('column');
const $topSelect = $(this).siblings('select.api-fixed-select');
if (!$topSelect.length) return;
const value = $topSelect.val();
const fieldKey = $topSelect.data('fixed-key');
// Propaga solo se il valore è valido
if (value) {
$(`.api-fixed-select[data-fixed-key="${fieldKey}"]`).each(function() {
$(this).val(value).trigger('change');
});
}
});
});
</script>
<!-- Modale di conferma per l'esportazione -->
<div class="modal fade" id="exportConfirmModal" tabindex="-1" aria-labelledby="exportConfirmModalLabel" aria-hidden="true">
<div class="modal-dialog">