Autosave: 20260220-172714

This commit is contained in:
Flatlogic Bot 2026-02-20 17:27:14 +00:00
parent b9b815f6ba
commit fdce191a3f
4 changed files with 384 additions and 64 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 KiB

View File

@ -0,0 +1,2 @@
-- Agrega un indice unico a la combinacion de fecha y provincia para asegurar que ON DUPLICATE KEY UPDATE funcione.
ALTER TABLE `liquidaciones_provincia` ADD UNIQUE `fecha_provincia_unique`(`fecha`, `provincia`);

View File

@ -42,14 +42,21 @@ foreach ($data as $row) {
z-index: 10; z-index: 10;
font-weight: 700; font-weight: 700;
} }
.table td[contenteditable="true"]:focus { .editable-cell {
background-color: #fff3cd; cursor: pointer;
}
.editable-cell input {
width: 100%;
box-sizing: border-box;
border: 1px solid #007bff;
font: inherit;
padding: 0.75rem;
} }
</style> </style>
<div class="container-fluid mt-4"> <div class="container-fluid mt-4">
<h2>Liquidaciones Provincia</h2> <h2>Liquidaciones Provincia</h2>
<p>Tabla de liquidaciones por provincia para el mes seleccionado.</p> <p>Haz clic en una celda de "Monto" o "Estado" para editarla. Presiona "Enter" o haz clic fuera para guardar.</p>
<form method="GET" action="liquidaciones_provincia.php" class="form-inline mb-4"> <form method="GET" action="liquidaciones_provincia.php" class="form-inline mb-4">
<div class="form-group mr-2"> <div class="form-group mr-2">
@ -84,7 +91,7 @@ foreach ($data as $row) {
<?php endforeach; ?> <?php endforeach; ?>
</tr> </tr>
</thead> </thead>
<tbody> <tbody id="liquidaciones-tbody">
<?php for ($day = 1; $day <= $days_in_month; $day++): ?> <?php for ($day = 1; $day <= $days_in_month; $day++): ?>
<?php $date = date('Y-m-d', strtotime("$year-$month-$day")); ?> <?php $date = date('Y-m-d', strtotime("$year-$month-$day")); ?>
<tr data-date="<?php echo $date; ?>"> <tr data-date="<?php echo $date; ?>">
@ -94,8 +101,8 @@ foreach ($data as $row) {
$monto = isset($liquidaciones[$date][$provincia]['monto']) ? number_format($liquidaciones[$date][$provincia]['monto'], 2, '.', '') : '0.00'; $monto = isset($liquidaciones[$date][$provincia]['monto']) ? number_format($liquidaciones[$date][$provincia]['monto'], 2, '.', '') : '0.00';
$estado = isset($liquidaciones[$date][$provincia]['estado']) ? htmlspecialchars($liquidaciones[$date][$provincia]['estado']) : ''; $estado = isset($liquidaciones[$date][$provincia]['estado']) ? htmlspecialchars($liquidaciones[$date][$provincia]['estado']) : '';
?> ?>
<td class="editable-cell" contenteditable="true" data-provincia="<?php echo htmlspecialchars($provincia); ?>" data-column="monto"><?php echo $monto; ?></td> <td class="editable-cell" data-provincia="<?php echo htmlspecialchars($provincia); ?>" data-column="monto"><?php echo $monto; ?></td>
<td class="editable-cell" contenteditable="true" data-provincia="<?php echo htmlspecialchars($provincia); ?>" data-column="estado"><?php echo $estado; ?></td> <td class="editable-cell" data-provincia="<?php echo htmlspecialchars($provincia); ?>" data-column="estado"><?php echo $estado; ?></td>
<?php endforeach; ?> <?php endforeach; ?>
</tr> </tr>
<?php endfor; ?> <?php endfor; ?>
@ -115,7 +122,8 @@ foreach ($data as $row) {
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const table = document.querySelector('.table'); const tableBody = document.getElementById('liquidaciones-tbody');
let activeInput = null;
function calculateTotals() { function calculateTotals() {
const totals = {}; const totals = {};
@ -123,7 +131,7 @@ document.addEventListener('DOMContentLoaded', function() {
totals['<?php echo $provincia; ?>'] = 0; totals['<?php echo $provincia; ?>'] = 0;
<?php endforeach; ?> <?php endforeach; ?>
table.querySelectorAll('tbody tr').forEach(row => { tableBody.querySelectorAll('tr').forEach(row => {
<?php foreach ($provincias as $provincia): ?> <?php foreach ($provincias as $provincia): ?>
const cell = row.querySelector(`td[data-provincia="<?php echo $provincia; ?>"][data-column="monto"]`); const cell = row.querySelector(`td[data-provincia="<?php echo $provincia; ?>"][data-column="monto"]`);
if (cell) { if (cell) {
@ -134,77 +142,116 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
<?php foreach ($provincias as $provincia): ?> <?php foreach ($provincias as $provincia): ?>
const totalCell = table.querySelector(`tfoot th[data-total-column="<?php echo $provincia; ?>"]`); const totalCell = document.querySelector(`tfoot th[data-total-column="<?php echo $provincia; ?>"]`);
if (totalCell) { if (totalCell) {
totalCell.textContent = totals['<?php echo $provincia; ?>'].toFixed(2); totalCell.textContent = totals['<?php echo $provincia; ?>'].toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
} }
<?php endforeach; ?> <?php endforeach; ?>
} }
// Initial calculation
calculateTotals(); calculateTotals();
table.addEventListener('keydown', function(e) { function turnCellIntoInput(cell) {
if (e.key === 'Enter' && e.target.classList.contains('editable-cell')) { if (cell.querySelector('input')) return; // Already an input
e.preventDefault();
e.target.blur(); // Trigger blur to save
}
});
table.addEventListener('blur', function(e) { const originalValue = cell.textContent.trim();
if (e.target.classList.contains('editable-cell')) { cell.setAttribute('data-original-value', originalValue);
const cell = e.target; cell.innerHTML = '';
const row = cell.closest('tr');
const fecha = row.dataset.date;
const provincia = cell.dataset.provincia;
const columna = cell.dataset.column;
let valor = cell.textContent.trim();
if (columna === 'monto') { const input = document.createElement('input');
let numValue = parseFloat(valor.replace(/,/g, '')); input.type = cell.dataset.column === 'monto' ? 'number' : 'text';
if (isNaN(numValue)) { input.className = 'form-control form-control-sm';
numValue = 0; input.value = originalValue;
}
valor = numValue; cell.appendChild(input);
// Format back to 2 decimal places for display input.focus();
cell.textContent = numValue.toFixed(2); activeInput = input;
input.addEventListener('blur', () => saveChanges(input));
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
saveChanges(input);
} else if (e.key === 'Escape') {
const original = cell.getAttribute('data-original-value');
cell.innerHTML = original;
activeInput = null;
} }
});
const data = { fecha, provincia, columna, valor }; }
fetch('save_liquidaciones_provincia.php', { function saveChanges(input) {
method: 'POST', if (!input) return;
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data) const cell = input.parentElement;
}) const originalValue = cell.getAttribute('data-original-value');
.then(response => response.json()) const newValue = input.value.trim();
.then(result => {
if (result.success) { activeInput = null;
if (columna === 'monto') { cell.innerHTML = newValue; // Optimistic update
calculateTotals();
}
} else {
console.error('Failed to save:', result.message);
// alert('Error al guardar el dato.'); // Optional: notify user
}
})
.catch(error => console.error('Error:', error));
}
}, true); // Use capturing to ensure blur event is handled reliably
table.addEventListener('focus', function(e) { if (newValue === originalValue) {
if (e.target.classList.contains('editable-cell')) { return; // No change
// Select all text in cell on focus
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(e.target);
selection.removeAllRanges();
selection.addRange(range);
} }
}, true);
const row = cell.closest('tr');
const fecha = row.dataset.date;
const provincia = cell.dataset.provincia;
const columna = cell.dataset.column;
let finalValue = newValue;
if (columna === 'monto') {
let numValue = parseFloat(newValue.replace(/,/g, ''));
if (isNaN(numValue)) numValue = 0;
finalValue = numValue.toFixed(2);
cell.innerHTML = finalValue; // Show formatted value
}
const formData = new URLSearchParams();
formData.append('fecha', fecha);
formData.append('provincia', provincia);
formData.append('columna', columna);
formData.append('valor', finalValue);
fetch('save_liquidaciones_provincia.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: formData
})
.then(response => response.json())
.then(result => {
if (result.success) {
if (columna === 'monto') calculateTotals();
} else {
alert('Error al guardar: ' + (result.message || 'Error desconocido'));
cell.innerHTML = originalValue; // Revert on failure
if (columna === 'monto') calculateTotals();
}
})
.catch(error => {
alert('Error de conexión. No se pudieron guardar los cambios.');
cell.innerHTML = originalValue; // Revert on failure
if (columna === 'monto') calculateTotals();
});
}
// --- Event Delegation ---
tableBody.addEventListener('click', function(e) {
const targetCell = e.target.closest('.editable-cell');
if (!targetCell) return;
// If we are clicking a cell but another one is being edited, save the other one first.
if (activeInput && activeInput.parentElement !== targetCell) {
saveChanges(activeInput);
}
// Now, turn the clicked cell into an input.
turnCellIntoInput(targetCell);
});
}); });
</script> </script>
<?php <?php
require_once 'layout_footer.php'; require_once 'layout_footer.php';
?> ?>

View File

@ -0,0 +1,271 @@
<?php
require_once 'layout_header.php';
require_once 'db/config.php';
// --- Data Fetching (identical to old file) ---
$month = isset($_GET['month']) ? $_GET['month'] : date('m');
$year = isset($_GET['year']) ? $_GET['year'] : date('Y');
$days_in_month = cal_days_in_month(CAL_GREGORIAN, $month, $year);
$provincias = ['LIMA', 'AREQUIPA', 'TRUJILLO', 'ICA', 'CUSCO', 'CAJAMARCA', 'CHICLAYO', 'PIURA'];
$pdo = db();
$stmt = $pdo->prepare("SELECT fecha, provincia, monto, estado FROM liquidaciones_provincia WHERE MONTH(fecha) = ? AND YEAR(fecha) = ?");
$stmt->execute([$month, $year]);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
$liquidaciones = [];
foreach ($data as $row) {
$liquidaciones[$row['fecha']][$row['provincia']] = [
'monto' => $row['monto'],
'estado' => $row['estado']
];
}
?>
<!-- --- STYLES (simplified) --- -->
<style>
.table thead th {
background-color: #337ab7;
color: #ffffff;
position: sticky;
top: 0;
z-index: 10;
}
.table tfoot th {
position: sticky;
bottom: 0;
background-color: #f2f2f2;
z-index: 10;
font-weight: bold;
}
.editable-cell {
cursor: pointer;
}
/* Simple highlight for editable cells on hover */
.editable-cell:hover {
background-color: #f5f5f5;
}
/* Style for the input field when editing */
.editable-cell input {
width: 100%;
box-sizing: border-box;
border: 2px solid #007bff;
border-radius: 4px;
padding: 5px;
font-family: inherit;
font-size: inherit;
}
</style>
<!-- --- HTML Structure (identical to old file) --- -->
<div class="container-fluid mt-4">
<h2>Liquidaciones Provincia (Nueva Versión)</h2>
<p>Haz <strong>un solo clic</strong> en una celda de "Monto" o "Estado" para editarla. Presiona "Enter" o haz clic fuera para guardar.</p>
<form method="GET" action="liquidaciones_provincia_new.php" class="form-inline mb-4">
<div class="form-group mr-2">
<label for="month" class="mr-2">Mes:</label>
<select name="month" id="month" class="form-control">
<?php for ($m = 1; $m <= 12; $m++): ?>
<option value="<?php echo str_pad($m, 2, '0', STR_PAD_LEFT); ?>" <?php echo $m == $month ? 'selected' : ''; ?>>
<?php echo DateTime::createFromFormat('!m', $m)->format('F'); ?>
</option>
<?php endfor; ?>
</select>
</div>
<div class="form-group mr-2">
<label for="year" class="mr-2">Año:</label>
<select name="year" id="year" class="form-control">
<?php for ($y = date('Y'); $y >= date('Y') - 5; $y--): ?>
<option value="<?php echo $y; ?>" <?php echo $y == $year ? 'selected' : ''; ?>><?php echo $y; ?></option>
<?php endfor; ?>
</select>
</div>
<button type="submit" class="btn btn-primary">Filtrar</button>
</form>
<div class="table-responsive" style="max-height: 75vh;">
<table class="table table-bordered table-striped table-hover">
<thead>
<tr>
<th>Fecha</th>
<?php foreach ($provincias as $provincia): ?>
<th><?php echo htmlspecialchars($provincia); ?></th>
<th>ESTADO</th>
<?php endforeach; ?>
</tr>
</thead>
<tbody id="liquidaciones-tbody">
<?php for ($day = 1; $day <= $days_in_month; $day++): ?>
<?php $date = date('Y-m-d', strtotime("$year-$month-$day")); ?>
<tr data-date="<?php echo $date; ?>">
<td><?php echo $date; ?></td>
<?php foreach ($provincias as $provincia): ?>
<?php
$monto = isset($liquidaciones[$date][$provincia]['monto']) ? number_format($liquidaciones[$date][$provincia]['monto'], 2, '.', '') : '0.00';
$estado = isset($liquidaciones[$date][$provincia]['estado']) ? htmlspecialchars($liquidaciones[$date][$provincia]['estado']) : '';
?>
<td class="editable-cell" data-provincia="<?php echo htmlspecialchars($provincia); ?>" data-column="monto"><?php echo $monto; ?></td>
<td class="editable-cell" data-provincia="<?php echo htmlspecialchars($provincia); ?>" data-column="estado"><?php echo $estado; ?></td>
<?php endforeach; ?>
</tr>
<?php endfor; ?>
</tbody>
<tfoot>
<tr>
<th>TOTAL</th>
<?php foreach ($provincias as $provincia): ?>
<th data-total-column="<?php echo htmlspecialchars($provincia); ?>">0.00</th>
<th></th>
<?php endforeach; ?>
</tr>
</tfoot>
</table>
</div>
</div>
<!-- --- BRAND NEW JAVASCRIPT --- -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const tableBody = document.getElementById('liquidaciones-tbody');
let activeCell = null; // To keep track of the cell being edited
// --- 1. Function to Calculate Totals ---
function calculateTotals() {
const totals = {};
const provincias = <?php echo json_encode($provincias); ?>;
provincias.forEach(provincia => {
totals[provincia] = 0;
});
tableBody.querySelectorAll('tr').forEach(row => {
provincias.forEach(provincia => {
const cell = row.querySelector(`td[data-provincia="${provincia}"][data-column="monto"]`);
if (cell) {
const value = parseFloat(cell.textContent.replace(/,/g, '')) || 0;
totals[provincia] += value;
}
});
});
provincias.forEach(provincia => {
const totalCell = document.querySelector(`tfoot th[data-total-column="${provincia}"]`);
if (totalCell) {
totalCell.textContent = totals[provincia].toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
});
}
// --- 2. Function to Save Changes ---
function saveCell(input) {
const cell = input.parentElement;
const originalValue = cell.getAttribute('data-original-value');
const newValue = input.value.trim();
// Optimistically update the UI
let displayValue = newValue;
if (cell.dataset.column === 'monto') {
const numericValue = parseFloat(newValue.replace(/,/g, '')) || 0;
displayValue = numericValue.toFixed(2);
}
cell.textContent = displayValue;
activeCell = null;
// If value hasn't changed, don't bother saving
if (newValue === originalValue) {
calculateTotals(); // Recalculate just in case formatting was the only change
return;
}
// Prepare data for saving
const formData = new FormData();
formData.append('fecha', cell.closest('tr').dataset.date);
formData.append('provincia', cell.dataset.provincia);
formData.append('columna', cell.dataset.column);
formData.append('valor', newValue);
// Send to server
fetch('save_liquidaciones_provincia.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (!data.success) {
// On failure, revert to original value
cell.textContent = originalValue;
alert('Error al guardar: ' + (data.message || 'Error desconocido.'));
}
})
.catch(err => {
// On network error, revert
cell.textContent = originalValue;
alert('Error de conexión. No se pudo guardar el cambio.');
})
.finally(() => {
// Always recalculate totals after an edit attempt
calculateTotals();
});
}
// --- 3. Function to Make a Cell Editable ---
function editCell(cell) {
// If we are already editing this cell, do nothing
if (cell === activeCell) {
return;
}
// If we are editing a *different* cell, save it first
if (activeCell) {
saveCell(activeCell.querySelector('input'));
}
activeCell = cell;
const originalValue = cell.textContent.trim();
cell.setAttribute('data-original-value', originalValue);
cell.innerHTML = ''; // Clear the cell
const input = document.createElement('input');
input.type = cell.dataset.column === 'monto' ? 'number' : 'text';
if (cell.dataset.column === 'monto') {
input.step = '0.01';
}
input.value = originalValue;
cell.appendChild(input);
input.focus();
input.select();
// Event listeners for the input
input.addEventListener('blur', () => {
saveCell(input);
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault(); // Prevent form submission or line breaks
saveCell(input);
} else if (e.key === 'Escape') {
cell.textContent = originalValue; // Revert on escape
activeCell = null;
}
});
}
// --- 4. Main Event Listener (Event Delegation) ---
tableBody.addEventListener('click', function(e) {
// Find the clicked cell
const target = e.target;
if (target.classList.contains('editable-cell')) {
editCell(target);
}
});
// --- 5. Initial Calculation ---
calculateTotals();
});
</script>
<?php
require_once 'layout_footer.php';
?>