1,2,3,4,5,6 points of milestone

This commit is contained in:
2026-03-13 00:30:19 +03:00
parent c516589483
commit 1b97bf4362
6 changed files with 1245 additions and 187 deletions
+351 -34
View File
@@ -347,6 +347,118 @@ function fixedDefaultValue(array $f): string
background-color: #e9ecef;
}
.grid-row.batch-exporting {
background: linear-gradient(90deg, #fff0f0 0%, #ffe0e0 50%, #fff0f0 100%) !important;
background-size: 200% 100% !important;
animation: batch-pulse 1.5s ease-in-out infinite;
position: relative;
z-index: 1;
}
.grid-row.batch-exporting .button-cell {
background: transparent !important;
}
@keyframes batch-pulse {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.batch-row-spinner {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #eb0b0b;
font-weight: 500;
}
.batch-row-spinner i {
font-size: 16px;
}
.grid-row.batch-disabled {
opacity: 0.5;
}
.grid-row.batch-disabled .action-btn {
pointer-events: none;
}
.grid-row.batch-row-error {
background: #fff3f3 !important;
border-left: 3px solid #dc3545;
}
.grid-row.batch-row-error .button-cell {
background: #fff3f3 !important;
}
.batch-error-msg {
color: #dc3545;
font-size: 10px;
line-height: 1.2;
display: block;
padding: 2px 0;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
}
.batch-error-msg:hover {
text-decoration: underline;
}
.grid-row.validation-row-error {
background: #fff3f3 !important;
border-left: 3px solid #dc3545;
}
.grid-cell.validation-error {
background-color: #f8d7da !important;
position: relative;
}
.input-validation-error,
.input-validation-error:focus {
border: 2px solid #dc3545 !important;
background-color: #fff5f5 !important;
box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.25) !important;
outline: none !important;
}
.validation-tooltip {
display: none;
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #dc3545;
color: #fff;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
white-space: nowrap;
z-index: 1050;
pointer-events: none;
}
.validation-tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: #dc3545;
}
.grid-cell.validation-error:hover .validation-tooltip {
display: block;
}
.grid-header,
.grid-cell {
flex: 1;
@@ -472,7 +584,10 @@ function fixedDefaultValue(array $f): string
.grid-top .save-all-cell {
flex: 0 0 210px;
align-items: flex-start;
display: flex;
align-items: center;
justify-content: center;
overflow: visible;
}
.grid-top .grid-cell input,
@@ -633,29 +748,51 @@ function fixedDefaultValue(array $f): string
transition: background-color 0.3s ease;
}
.save-all-btn {
background-color: #28a745;
.actions-dropdown .dropdown-toggle {
background-color: #6c757d;
color: white;
border: none;
border-radius: 5px;
padding: 8px 16px;
padding: 6px 14px;
cursor: pointer;
font-size: 14px;
font-size: 13px;
}
.save-all-btn:hover {
background-color: #218838;
.actions-dropdown .dropdown-toggle:hover,
.actions-dropdown .dropdown-toggle:focus {
background-color: #5a6268;
}
.actions-dropdown .dropdown-menu {
min-width: 160px;
font-size: 13px;
z-index: 1050;
}
.actions-dropdown .dropdown-item i {
width: 18px;
text-align: center;
margin-right: 6px;
}
#exportConfirmModal,
#exportResponseModal,
#exportUnsavedModal {
#exportUnsavedModal,
#exportBatchConfirmModal,
#exportBatchUnsavedModal,
#saveAllConfirmModal,
#saveAllResultModal {
z-index: 1300 !important;
}
#exportConfirmModal .modal-backdrop,
#exportResponseModal .modal-backdrop,
#exportUnsavedModal .modal-backdrop {
#exportUnsavedModal .modal-backdrop,
#exportBatchConfirmModal .modal-backdrop,
#exportBatchUnsavedModal .modal-backdrop,
#saveAllConfirmModal .modal-backdrop,
#saveAllResultModal .modal-backdrop {
z-index: 1299 !important;
}
@@ -745,21 +882,36 @@ function fixedDefaultValue(array $f): string
</div>
<div class="card radius-10">
<div class="card-header">
<div class="d-flex align-items-center">
<div id="unsavedChanges" style="display:none; color: red; font-weight: bold; margin:10px 0;">
⚠️ Unsaved changes detected! Please save before leaving this page.<br>
<div class="d-flex align-items-center" style="min-height: 42px; gap: 12px;">
<div class="dropdown actions-dropdown" style="flex-shrink: 0;">
<button class="dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-cogs"></i> Actions
</button>
<ul class="dropdown-menu dropdown-menu-end">
<?php if ((Auth::user()->hasRole('Admin'))) : ?>
<li><a class="dropdown-item export-all-lims-btn" href="#"><i class="fas fa-upload" style="color: #eb0b0b;"></i>Export All</a></li>
<?php endif; ?>
<li><a class="dropdown-item save-all-btn" href="#"><i class="fas fa-save" style="color: #28a745;"></i>Save All</a></li>
</ul>
</div>
<div id="unsavedChanges" style="display:none; color: red; font-weight: bold; font-size: 13px;">
⚠️ Unsaved changes detected!
<span id="changedRows" style="font-weight:normal; color:darkred;"></span>
</div>
<div id="batchExportBar" style="display:none; flex: 1; min-width: 0;">
<div class="d-flex align-items-center gap-2">
<i class="fas fa-spinner fa-spin" style="color: #eb0b0b;"></i>
<span id="batchExportStatus" style="font-size: 13px; font-weight: 500;">Esportazione...</span>
<button type="button" class="btn btn-outline-danger btn-sm" id="exportBatchCancelBtn" style="padding: 2px 10px; font-size: 12px;">Annulla</button>
</div>
</div>
</div>
</div>
<div class="card-body">
<form id="editForm">
<div class="grid-container">
<div class="grid-top">
<div class="grid-cell save-all-cell">
<button type="button" class="save-all-btn" title="Save All Rows"><i class="fas fa-save"></i> Save All</button>
</div>
<div class="grid-cell save-all-cell"></div>
<?php
$topColIndex = 1;
@@ -1025,7 +1177,8 @@ function fixedDefaultValue(array $f): string
<div class="grid-cell button-cell" style="flex: 0 0 210px; position: relative;">
<!-- commented only for admin roles -->
<?php if ((Auth::user()->hasRole('Admin'))) : ?>
<button type="button" class="export-lims-btn action-btn" data-row="<?= $index ?>" data-iddatadb="<?= $row['iddatadb'] ?>" title="Export to LIMS" style="background: #eb0b0bff; color: white; border: none; border-radius: 5px; cursor: pointer;"><i class="fas fa-upload"></i></button>
<?php $isExported = (($row['status'] ?? '') === 'l'); ?>
<button type="button" class="export-lims-btn action-btn" data-row="<?= $index ?>" data-iddatadb="<?= $row['iddatadb'] ?>" title="<?= $isExported ? 'Già esportato' : 'Export to LIMS' ?>" style="background: <?= $isExported ? '#ccc' : '#eb0b0b' ?>; color: white; border: none; border-radius: 5px; cursor: <?= $isExported ? 'not-allowed' : 'pointer' ?>; <?= $isExported ? 'opacity: 0.5;' : '' ?>"<?= $isExported ? ' disabled' : '' ?>><i class="fas fa-upload"></i></button>
<?php endif; ?>
<button type="button" class="save-btn action-btn" data-row="<?= $index ?>" title="Save" style="background: #28a745; color: white; border: none; border-radius: 5px; cursor: pointer;"><i class="fas fa-save"></i></button>
@@ -1349,6 +1502,19 @@ function fixedDefaultValue(array $f): string
// (se vuoi mantenere highlight cella gialla, lascia questa riga)
if (gridCell) gridCell.classList.add("cell-changed");
// Clear validation error on this field when user edits it
el.classList.remove("input-validation-error");
if (gridCell) {
gridCell.classList.remove("validation-error");
const tooltip = gridCell.querySelector(".validation-tooltip");
if (tooltip) tooltip.remove();
// If no more validation errors on the row, remove row highlight
const gridRow = gridCell.closest(".grid-row");
if (gridRow && !gridRow.querySelector(".validation-error")) {
gridRow.classList.remove("validation-row-error");
}
}
renderChangedRows();
});
});
@@ -1446,21 +1612,65 @@ function fixedDefaultValue(array $f): string
});
});
document.querySelector('.save-all-btn').addEventListener('click', async () => {
const rows = document.querySelectorAll('.grid-row');
let saveAllRunning = false;
document.querySelector('.save-all-btn').addEventListener('click', (e) => {
e.preventDefault();
if (saveAllRunning || window.batchRunning) return;
const confirmModal = new bootstrap.Modal(document.getElementById('saveAllConfirmModal'), { keyboard: false });
confirmModal.show();
});
document.getElementById('saveAllConfirmBtn').addEventListener('click', async () => {
bootstrap.Modal.getInstance(document.getElementById('saveAllConfirmModal')).hide();
saveAllRunning = true;
// Clear previous row errors
document.querySelectorAll('.grid-row.batch-row-error').forEach(r => {
r.classList.remove('batch-row-error');
const msg = r.querySelector('.batch-error-msg');
if (msg) msg.remove();
});
// Disable all row buttons
document.querySelectorAll('.grid-row[data-id]').forEach(r => r.classList.add('batch-disabled'));
const toggle = document.querySelector('.actions-dropdown .dropdown-toggle');
if (toggle) { toggle.disabled = true; toggle.style.opacity = '0.5'; toggle.style.pointerEvents = 'none'; }
// Show status bar
const bar = document.getElementById('batchExportBar');
const statusEl = document.getElementById('batchExportStatus');
const cancelBtn = document.getElementById('exportBatchCancelBtn');
bar.style.display = '';
cancelBtn.style.display = 'none';
const rows = document.querySelectorAll('.grid-row[data-id]');
const total = rows.length;
let processed = 0;
let successCount = 0;
let errorMessages = [];
for (const row of rows) {
const saveBtn = row.querySelector('.save-btn');
if (!saveBtn) {
continue;
}
if (!saveBtn) { continue; }
const rowIndex = saveBtn.dataset.row;
const iddatadb = row.getAttribute('data-id');
if (!rowIndex || !iddatadb) {
continue;
if (!rowIndex || !iddatadb) { continue; }
processed++;
statusEl.textContent = `Salvataggio ${processed} / ${total} (id: ${iddatadb})...`;
// Highlight current row
row.classList.remove('batch-disabled');
row.classList.add('batch-exporting');
const btnCell = row.querySelector('.button-cell');
if (btnCell) {
btnCell.querySelectorAll('.action-btn').forEach(b => { b.dataset.prevDisplay = b.style.display; b.style.display = 'none'; });
const spinner = document.createElement('span');
spinner.className = 'batch-row-spinner';
spinner.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Saving...';
btnCell.appendChild(spinner);
}
const formData = new FormData();
const inputs = row.querySelectorAll(`input[name^="rows[${rowIndex}][details]"], select[name^="rows[${rowIndex}][details]"]`);
@@ -1479,12 +1689,11 @@ function fixedDefaultValue(array $f): string
if (idclientSelect) {
formData.append('idclient', idclientSelect.value);
}
// ---- FIXED FIELDS (NEW) ----
// ---- FIXED FIELDS ----
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+\]\[([^\]]+)\]/);
// Map: fixed key (logical) -> datadb real column
const fixedAliasMap = {
ClienteResponsabile: 'cliente_responsabile_id',
MoltiplicatorePrezzo: 'moltiplicatore_prezzo_id',
@@ -1492,13 +1701,10 @@ function fixedDefaultValue(array $f): string
AnagraficaCertestService: 'anagrafica_certest_service_id',
ConsegnaRichiesta: 'consegna_richiesta'
};
if (m && m[1]) {
const logicalKey = m[1];
const realKey = fixedAliasMap[logicalKey] || logicalKey;
const realKey = fixedAliasMap[m[1]] || m[1];
formData.append(realKey, inp.value);
}
});
formData.append('iddatadb', iddatadb);
@@ -1524,24 +1730,59 @@ function fixedDefaultValue(array $f): string
changedRows.delete(rowIndex);
document.querySelectorAll(`.grid-cell[data-row="${rowIndex}"]`)
.forEach(cell => cell.classList.remove("cell-changed"));
} else {
errorMessages.push(`Riga ${parseInt(rowIndex) + 1}: ${data.message}`);
// Show error on the row
row.classList.add('batch-row-error');
if (btnCell) {
const errEl = document.createElement('div');
errEl.className = 'batch-error-msg';
errEl.textContent = '⚠ ' + (data.message || 'Errore');
btnCell.appendChild(errEl);
}
}
} catch (error) {
errorMessages.push(`Riga ${parseInt(rowIndex) + 1}: ${error.message}`);
row.classList.add('batch-row-error');
if (btnCell) {
const errEl = document.createElement('div');
errEl.className = 'batch-error-msg';
errEl.textContent = '⚠ ' + error.message;
btnCell.appendChild(errEl);
}
}
// Remove highlight from current row
row.classList.remove('batch-exporting');
row.classList.add('batch-disabled');
if (btnCell) {
const sp = btnCell.querySelector('.batch-row-spinner');
if (sp) sp.remove();
btnCell.querySelectorAll('.action-btn').forEach(b => { b.style.display = b.dataset.prevDisplay || ''; delete b.dataset.prevDisplay; });
}
}
// Finished
renderChangedRows();
hasChanges = changedRows.size > 0;
saveAllRunning = false;
bar.style.display = 'none';
cancelBtn.style.display = '';
// Re-enable all row buttons
document.querySelectorAll('.grid-row[data-id]').forEach(r => r.classList.remove('batch-disabled'));
if (toggle) { toggle.disabled = false; toggle.style.opacity = ''; toggle.style.pointerEvents = ''; }
const resultEl = document.getElementById('saveAllResultMessage');
const resultLabel = document.getElementById('saveAllResultModalLabel');
if (errorMessages.length === 0) {
alert(`Tutte le ${successCount} righe salvate con successo!`);
resultLabel.textContent = 'Salvataggio Completato';
resultEl.innerHTML = `<i class="fas fa-check-circle" style="color:#28a745;"></i> Tutte le <strong>${successCount}</strong> righe salvate con successo.`;
} else {
alert(`Salvate ${successCount} righe con successo.\nErrori:\n${errorMessages.join('\n')}`);
resultLabel.textContent = 'Salvataggio con Errori';
resultEl.innerHTML = `<i class="fas fa-exclamation-triangle" style="color:#ffc107;"></i> Salvate <strong>${successCount}</strong> righe con successo.<br><br><strong>Errori:</strong><br>` + errorMessages.join('<br>');
}
new bootstrap.Modal(document.getElementById('saveAllResultModal'), { keyboard: false }).show();
});
window.addEventListener("beforeunload", function(e) {
@@ -1553,7 +1794,7 @@ function fixedDefaultValue(array $f): string
});
// Gestisci la chiusura dei modali per rimuovere i backdrop
document.querySelectorAll('#exportConfirmModal, #exportResponseModal, #exportUnsavedModal').forEach(modal => {
document.querySelectorAll('#exportConfirmModal, #exportResponseModal, #exportUnsavedModal, #exportBatchConfirmModal, #exportBatchUnsavedModal, #saveAllConfirmModal, #saveAllResultModal').forEach(modal => {
modal.addEventListener('hidden.bs.modal', () => {
// Rimuovi tutti i backdrop residui
document.querySelectorAll('.modal-backdrop').forEach(backdrop => {
@@ -2744,6 +2985,82 @@ function fixedDefaultValue(array $f): string
</div>
</div>
<!-- Modal: batch export confirm -->
<div class="modal fade" id="exportBatchConfirmModal" tabindex="-1" aria-labelledby="exportBatchConfirmModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exportBatchConfirmModalLabel">Conferma Export All</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Verranno esportate <strong><span id="exportBatchCount"></span></strong> righe al LIMS, una alla volta dall'alto verso il basso.<br>
Potrai annullare il batch in qualsiasi momento.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Annulla</button>
<button type="button" class="btn btn-danger btn-sm" id="exportBatchConfirmBtn"><i class="fas fa-upload"></i> Avvia Export</button>
</div>
</div>
</div>
</div>
<!-- Modal: batch export unsaved changes -->
<div class="modal fade" id="exportBatchUnsavedModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modifiche non salvate</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
Ci sono modifiche non salvate. Salvare tutte le righe prima di procedere con l'esportazione?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Annulla</button>
<button type="button" class="btn btn-primary btn-sm" id="batchSaveAndExportBtn"><i class="fas fa-save"></i> Salva ed Esporta</button>
</div>
</div>
</div>
</div>
<!-- Modal: Save All confirm -->
<div class="modal fade" id="saveAllConfirmModal" tabindex="-1" aria-labelledby="saveAllConfirmModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="saveAllConfirmModalLabel">Conferma Salvataggio</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Confermi il salvataggio di tutte le righe?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Annulla</button>
<button type="button" class="btn btn-success btn-sm" id="saveAllConfirmBtn"><i class="fas fa-save"></i> Conferma</button>
</div>
</div>
</div>
</div>
<!-- Modal: Save All result -->
<div class="modal fade" id="saveAllResultModal" tabindex="-1" aria-labelledby="saveAllResultModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="saveAllResultModalLabel">Risultato Salvataggio</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p id="saveAllResultMessage"></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary btn-sm" data-bs-dismiss="modal">Chiudi</button>
</div>
</div>
</div>
</div>
<!-- Toast: row saved successfully -->
<div class="position-fixed bottom-0 end-0 p-3" style="z-index:9999">
<div id="saveSuccessToast" class="toast align-items-center text-bg-success border-0" role="alert" data-bs-delay="2500">