feat: pagina Log con diario operazioni

- Nuova sezione 'Log' nella bottom nav con icona 📒
- Mostra tutte le transazioni (entrate/uscite) raggruppate per data
- Ogni voce: icona 📥/📤, nome prodotto, marca, quantità, posizione, orario
- Bordo verde per aggiunte, rosso per uscite
- Paginazione con 'Carica altri...' (50 per pagina)
- Backend: aggiunto supporto offset a listTransactions
This commit is contained in:
dadaloop82
2026-03-10 18:20:31 +00:00
parent 77ed2d6964
commit 2abcec6fe5
4 changed files with 147 additions and 3 deletions
+5 -3
View File
@@ -532,7 +532,8 @@ function inventorySummary(PDO $db): void {
// ===== TRANSACTION FUNCTIONS ===== // ===== TRANSACTION FUNCTIONS =====
function listTransactions(PDO $db): void { function listTransactions(PDO $db): void {
$limit = $_GET['limit'] ?? 50; $limit = (int)($_GET['limit'] ?? 50);
$offset = (int)($_GET['offset'] ?? 0);
$productId = $_GET['product_id'] ?? ''; $productId = $_GET['product_id'] ?? '';
$query = " $query = "
@@ -545,8 +546,9 @@ function listTransactions(PDO $db): void {
$query .= " WHERE t.product_id = ?"; $query .= " WHERE t.product_id = ?";
$params[] = $productId; $params[] = $productId;
} }
$query .= " ORDER BY t.created_at DESC LIMIT ?"; $query .= " ORDER BY t.created_at DESC LIMIT ? OFFSET ?";
$params[] = (int)$limit; $params[] = $limit;
$params[] = $offset;
$stmt = $db->prepare($query); $stmt = $db->prepare($query);
$stmt->execute($params); $stmt->execute($params);
+63
View File
@@ -2094,6 +2094,69 @@ body {
line-height: 1.3; line-height: 1.3;
} }
/* ===== LOG PAGE ===== */
.log-list {
display: flex;
flex-direction: column;
gap: 2px;
padding: 0 var(--space-sm);
}
.log-date-header {
font-size: 0.78rem;
font-weight: 700;
color: var(--text-muted);
text-transform: capitalize;
padding: 12px 0 4px;
border-bottom: 1px solid var(--bg-light);
margin-bottom: 2px;
}
.log-entry {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: var(--radius-sm);
background: var(--bg-card);
}
.log-entry.log-in {
border-left: 3px solid var(--success);
}
.log-entry.log-out {
border-left: 3px solid var(--danger);
}
.log-icon {
font-size: 1.2rem;
flex-shrink: 0;
}
.log-info {
flex: 1;
min-width: 0;
}
.log-product {
font-size: 0.9rem;
line-height: 1.3;
}
.log-product em {
font-weight: 400;
color: var(--text-muted);
font-size: 0.82rem;
}
.log-detail {
font-size: 0.75rem;
color: var(--text-muted);
line-height: 1.3;
margin-top: 1px;
}
/* ===== AI IDENTIFICATION RESULTS ===== */ /* ===== AI IDENTIFICATION RESULTS ===== */
.ai-identified-card { .ai-identified-card {
background: var(--bg-light); background: var(--bg-light);
+64
View File
@@ -365,6 +365,7 @@ function showPage(pageId, param = null) {
case 'scan': initScanner(); clearQuickNameResults(); break; case 'scan': initScanner(); clearQuickNameResults(); break;
case 'products': loadAllProducts(); break; case 'products': loadAllProducts(); break;
case 'shopping': loadShoppingList(); break; case 'shopping': loadShoppingList(); break;
case 'log': loadLog(); break;
case 'ai': initAICamera(); break; case 'ai': initAICamera(); break;
} }
@@ -2626,6 +2627,69 @@ function showToast(message, type = '') {
}, 3000); }, 3000);
} }
// ===== LOG =====
let _logOffset = 0;
const LOG_PAGE_SIZE = 50;
async function loadLog(more = false) {
if (!more) {
_logOffset = 0;
document.getElementById('log-list').innerHTML = '<p style="text-align:center;color:var(--text-muted)">Caricamento...</p>';
}
try {
const result = await api(`transactions_list&limit=${LOG_PAGE_SIZE}&offset=${_logOffset}`);
const txns = result.transactions || [];
let html = '';
if (!more && txns.length === 0) {
html = '<p style="text-align:center;color:var(--text-muted)">Nessuna operazione registrata.</p>';
} else {
let lastDate = more ? '' : null;
txns.forEach(t => {
const dt = new Date(t.created_at + 'Z');
const dateStr = dt.toLocaleDateString('it-IT', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
const timeStr = dt.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' });
if (dateStr !== lastDate) {
html += `<div class="log-date-header">${dateStr}</div>`;
lastDate = dateStr;
}
const isIn = t.type === 'in';
const icon = isIn ? '📥' : '📤';
const typeLabel = isIn ? 'Aggiunto' : 'Usato';
const colorClass = isIn ? 'log-in' : 'log-out';
const brand = t.brand ? ` <em>(${t.brand})</em>` : '';
const loc = t.location || '';
const locLabels = { 'frigo': '🧊 Frigo', 'freezer': '❄️ Freezer', 'dispensa': '🗄️ Dispensa' };
const locStr = locLabels[loc] || ('📍 ' + loc);
html += `<div class="log-entry ${colorClass}">`;
html += `<span class="log-icon">${icon}</span>`;
html += `<div class="log-info">`;
html += `<div class="log-product"><strong>${t.name}</strong>${brand}</div>`;
html += `<div class="log-detail">${typeLabel} ${t.quantity} ${t.unit || ''} · ${locStr} · ${timeStr}</div>`;
html += `</div>`;
html += `</div>`;
});
}
if (more) {
document.getElementById('log-list').insertAdjacentHTML('beforeend', html);
} else {
document.getElementById('log-list').innerHTML = html;
}
_logOffset += txns.length;
document.getElementById('log-load-more').style.display = txns.length >= LOG_PAGE_SIZE ? '' : 'none';
} catch (err) {
console.error('Log load error:', err);
if (!more) document.getElementById('log-list').innerHTML = '<p style="text-align:center;color:var(--danger)">Errore nel caricamento log</p>';
}
}
// ===== RECIPE GENERATION ===== // ===== RECIPE GENERATION =====
function getMealType() { function getMealType() {
const hour = new Date().getHours(); const hour = new Date().getHours();
+15
View File
@@ -473,6 +473,17 @@
</div> </div>
</section> </section>
<!-- Log Page -->
<section id="page-log" class="page">
<div class="page-header">
<h2>📒 Log Operazioni</h2>
</div>
<div id="log-list" class="log-list"></div>
<button class="btn btn-secondary full-width mt-2" id="log-load-more" style="display:none" onclick="loadLog(true)">
Carica altri...
</button>
</section>
</main> </main>
<!-- Bottom Navigation --> <!-- Bottom Navigation -->
@@ -493,6 +504,10 @@
<span class="nav-icon">🛒</span> <span class="nav-icon">🛒</span>
<span class="nav-label">Spesa</span> <span class="nav-label">Spesa</span>
</button> </button>
<button class="nav-btn" onclick="showPage('log')" data-page="log">
<span class="nav-icon">📒</span>
<span class="nav-label">Log</span>
</button>
</nav> </nav>
<!-- Recipe Dialog --> <!-- Recipe Dialog -->