/* ChefCode – MVP Controller PATCH 1.2.2 — Stable (Inventory deduction + Production fix) */ // Global utility functions function normName(s) { return String(s || '') .toLowerCase() .normalize('NFD').replace(/[\u0300-\u036f]/g,'') .replace(/[^a-z0-9\s]/g,' ') .replace(/\s+/g,' ') .trim(); } function normUnit(u) { u = String(u || '').trim().toLowerCase(); if (u === 'l') u = 'lt'; if (u === 'gr') u = 'g'; if (u === 'pz.' || u === 'pcs' || u === 'pc') u = 'pz'; return u; } function convertFactor(from, to) { from = normUnit(from); to = normUnit(to); if (from === to) return 1; if (from === 'kg' && to === 'g') return 1000; if (from === 'g' && to === 'kg') return 1/1000; if (from === 'lt' && to === 'ml') return 1000; if (from === 'ml' && to === 'lt') return 1/1000; // MVP: pz <-> bt 1:1 if ((from === 'pz' && to === 'bt') || (from === 'bt' && to === 'pz')) return 1; return null; // non convertibili } // Global function for adding/merging inventory items window.addOrMergeInventoryItem = function({ name, unit, quantity, category, price, lot_number, expiry_date }) { const nName = normName(name); const pCents = Math.round((Number(price) || 0) * 100); // HACCP: Items with different lot numbers or expiry dates MUST be kept separate for traceability const idx = window.STATE.inventory.findIndex(it => normName(it.name) === nName && Math.round((Number(it.price) || 0) * 100) === pCents && (it.lot_number || '') === (lot_number || '') && (it.expiry_date || '') === (expiry_date || '') ); if (idx >= 0) { const row = window.STATE.inventory[idx]; const fromU = normUnit(unit || row.unit); const toU = normUnit(row.unit || fromU); const f = convertFactor(fromU, toU); if (f === null) { // unità non compatibili: NON fondere, crea una nuova riga window.STATE.inventory.push({ name, unit, quantity, category: category || row.category || 'Other', price, lot_number, expiry_date }); } else { row.quantity = (Number(row.quantity) || 0) + (Number(quantity) || 0) * f; // manteniamo categoria e unit della riga esistente } } else { // nuova riga con HACCP traceability window.STATE.inventory.push({ name, unit, quantity, category, price, lot_number, expiry_date }); } }; document.addEventListener('DOMContentLoaded', () => { // ===== AI TOOLBAR FUNCTIONALITY ===== // =================================================================== // AI ASSISTANT TOOLBAR // =================================================================== // All AI functionality (voice, text, commands) is now handled by ai-assistant.js // - Voice button opens the AI chat with voice recognition // - Send button sends commands through AI chat // - No more browser prompt()/alert() - all conversational UI // - See ai-assistant.js for full implementation // =================================================================== // Upload button (connects to OCR) const aiUploadBtn = document.getElementById('ai-upload-btn'); if (aiUploadBtn) { aiUploadBtn.addEventListener('click', () => { // Open OCR modal if available if (window.ocrModal) { window.ocrModal.openModal(); } else { alert('📤 Upload functionality - Coming soon!'); } }); } // ---------- Helpers ---------- const el = (id) => document.getElementById(id); const q = (sel) => document.querySelector(sel); const qa = (sel) => Array.from(document.querySelectorAll(sel)); function safe(fn){ try { return fn(); } catch(e){ console.warn(e); return undefined; } } // ---------- Storage ---------- window.STATE = { inventory: [], // [{name, unit, quantity, category, price}] recipes: {}, // { "Carbonara": { items:[{name, qty, unit}] } } tasks: [], // [{id, recipe, quantity, assignedTo, status}] nextTaskId: 1 }; // Remove load/save/localStorage. Always sync with backend. window.updateInventoryToBackend = async function() { try { const apiKey = window.CHEFCODE_CONFIG?.API_KEY || ''; console.log('🔄 Syncing to backend...', { recipes: Object.keys(window.STATE.recipes || {}).length, inventory: (window.STATE.inventory || []).length, tasks: (window.STATE.tasks || []).length }); const syncData = { inventory: window.STATE.inventory || [], recipes: window.STATE.recipes || {}, tasks: window.STATE.tasks || [] }; const response = await fetch('http://localhost:8000/api/sync-data', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey }, body: JSON.stringify(syncData) }); if (!response.ok) { const errorText = await response.text(); console.error('❌ Sync failed:', response.status, errorText); throw new Error(`Sync failed: ${response.status} - ${errorText}`); } console.log('✅ Sync successful!'); // Removed fetchInventoryFromBackend() to prevent race condition // Frontend already has the latest STATE, no need to fetch again } catch (err) { console.error('❌ Backend sync failed:', err.message); alert(`Failed to save to database: ${err.message}\n\nYour changes may not be saved!`); throw err; // Re-throw so callers know sync failed } } async function fetchInventoryFromBackend() { try { const dataRes = await fetch('http://localhost:8000/api/data'); if (!dataRes.ok) { throw new Error(`Backend returned ${dataRes.status}: ${dataRes.statusText}`); } const latest = await dataRes.json(); // Validate response structure if (!latest || typeof latest !== 'object') { throw new Error('Invalid response format from backend'); } window.STATE = { inventory: Array.isArray(latest.inventory) ? latest.inventory : [], recipes: latest.recipes || {}, tasks: Array.isArray(latest.tasks) ? latest.tasks : [], nextTaskId: window.STATE.nextTaskId || 1 }; // Debug: Log recipes loaded console.log(`✅ Data loaded from backend: ${Object.keys(window.STATE.recipes).length} recipes`); if (Object.keys(window.STATE.recipes).length > 0) { console.log('Recipes:', Object.keys(window.STATE.recipes)); } // Synchronize production tasks with STATE.tasks if (Array.isArray(window.STATE.tasks)) { window.productionTasks = window.STATE.tasks; } window.renderInventory(); } catch (err) { console.error('⚠️ Backend fetch failed:', err.message); // Keep existing STATE if fetch fails - don't clear user's data } } // ---------- Selectors ---------- const chefcodeLogoBtn = el('chefcode-logo-btn'); const stepSelectionPage = el('step-selection-page'); const inputDetailPage = el('input-detail-page'); const inputPagesContainer = el('input-pages-container'); const bigStepButtons = qa('.big-step-button[data-step]'); // Production tab panels (for visibility control) const prodPanels = [ ...Array.from(qa('#production-content .production-tabs')), ...Array.from(qa('#production-content .production-tasks-tabbed')) ]; // Account const accountButton = q('.account-button'); const accountDropdownContent= q('.account-menu .dropdown-content'); // Goods In – Camera/OCR (sim) const cameraViewfinder = q('.camera-viewfinder'); const cameraOutput = q('.camera-output'); // Goods In – Voice (sim + process) const microphoneBtn = el('microphone-btn'); const micLabel = el('mic-label'); const voiceStatus = el('voice-status'); const voiceRecognizedText = el('voice-recognized-text'); const recognizedTextContent = el('recognized-text-content'); const processVoiceBtn = el('process-voice-btn'); // Goods In – Manual input const manualEntryForm = el('manual-entry-form'); const inventoryTableBody= el('inventory-table-body'); const inventoryTotalVal = el('inventory-total-value'); // Inventory – search/filter/expand const inventorySearch = el('inventory-search'); const categoryFilter = el('inventory-category-filter'); const expandTableBtn = el('expand-table-btn'); const inventoryTableCtr = q('#inventory-page-content .inventory-table-container'); // Recipe setup const ingredientSelect = el('ingredient-select'); const ingredientQty = el('ingredient-qty'); const ingredientUnit = el('ingredient-unit'); const addIngredientBtn = el('add-ingredient-btn'); const recipeIngredientsList = el('recipe-ingredients-list'); const saveRecipeBtn = el('save-recipe-btn'); const recipeNameInput = el('recipe-name'); // Production const recipeSelectProd = el('recipe-select-prod'); const productionQty = el('production-qty'); const assignTo = el('assign-to'); const initialStatusSelect = el('initial-status'); const addTaskBtn = el('add-task-btn'); const todoTasksContainer = el('todo-tasks'); const inprogressTasksContainer= el('inprogress-tasks'); const completedTasksList = el('completed-tasks-list'); // === Production state bootstrap (safe) === // garantisci che l'array esista sempre e che l'id parta da >0 window.productionTasks = Array.isArray(window.productionTasks) ? window.productionTasks : []; window.taskIdCounter = typeof window.taskIdCounter === 'number' ? window.taskIdCounter : 0; // runtime temp for recipe building let currentRecipeIngredients = []; let RECIPES = {}; // mappa: { [recipeName]: { items:[{name, quantity, unit}] } } // --- Helpers per unità/nome e parsing --- const normUnit = (u) => { u = String(u || '').trim().toLowerCase(); if (u === 'l') u = 'lt'; if (u === 'gr') u = 'g'; if (u === 'pz.' || u === 'pcs' || u === 'pc') u = 'pz'; return u; }; const convertFactor = (from, to) => { from = normUnit(from); to = normUnit(to); if (from === to) return 1; if (from === 'kg' && to === 'g') return 1000; if (from === 'g' && to === 'kg') return 1/1000; if (from === 'lt' && to === 'ml') return 1000; if (from === 'ml' && to === 'lt') return 1/1000; // MVP: pz <-> bt 1:1 if ((from === 'pz' && to === 'bt') || (from === 'bt' && to === 'pz')) return 1; return null; // non convertibili }; const normName = (s) => String(s || '') .toLowerCase() .normalize('NFD').replace(/[\u0300-\u036f]/g,'') .replace(/[^a-z0-9\s]/g,' ') .replace(/\s+/g,' ') .trim(); const parseNumber = (t) => { if (!t) return 0; t = String(t).replace('€','').replace(/\s/g,''); // togli separa-migliaia e usa il punto come decimale t = t.replace(/\./g,'').replace(',', '.'); const n = parseFloat(t); return isNaN(n) ? 0 : n; }; // ==== Merge Inventory: stesso nome + stesso prezzo => somma quantità ==== // Usa le funzioni già presenti: normName, normUnit, convertFactor let isRecording = false; // ---------- Routing ---------- function normalizeToken(s){ return String(s||'').toLowerCase().replace(/[^a-z0-9]/g,''); } function findPageIdForStep(stepToken){ const token = normalizeToken(stepToken); // Prova id esatto "-content" const direct = `${stepToken}-content`; if (el(direct)) return direct; // Cerca qualunque .input-page che contenga il token “normalizzato” const pages = qa('.input-page'); for (const page of pages){ const pid = page.id || ''; if (normalizeToken(pid).includes(token)) return pid; } return null; } function showPage(pageId){ if (!stepSelectionPage || !inputDetailPage || !inputPagesContainer) return; qa('.input-page').forEach(p => p.classList.remove('active')); stepSelectionPage.classList.remove('active'); inputDetailPage.classList.remove('active'); const target = el(pageId); if (target){ target.classList.add('active'); inputDetailPage.classList.add('active'); } else { stepSelectionPage.classList.add('active'); } // Mostra i tab solo se sei nella pagina production prodPanels.forEach(el => { el.style.display = (pageId === 'production-content') ? '' : 'none'; }); // Render recipe catalogue when showing that page if (pageId === 'recipe-catalogue-content' && window.renderRecipeCatalogue) { window.renderRecipeCatalogue(); } } bigStepButtons.forEach(btn => { btn.addEventListener('click', () => { const step = btn.getAttribute('data-step'); // es: goodsin / goods-in const pid = findPageIdForStep(step || ''); if (pid) showPage(pid); else showPage('step-selection-page'); /* ==== PATCH LAYOUT-4x2 — pulizia stili inline sul Back (append-only) ==== */ (function enforceHomeGridOnBack(){ const home = document.getElementById('step-selection-page'); if (!home) return; const grid = home.querySelector('.step-buttons-grid'); if (!grid) return; function cleanInline() { // rimuove qualsiasi style inline che possa stringere i riquadri grid.removeAttribute('style'); if (grid.style) { grid.style.gridTemplateColumns = ''; grid.style.gridTemplateRows = ''; grid.style.gap = ''; } } // Dopo qualunque click su un back-button, quando la home è visibile ripulisci document.addEventListener('click', (e) => { const back = e.target.closest('.back-button'); if (!back) return; const targetId = back.dataset.backTarget || ''; setTimeout(() => { if (targetId === 'step-selection-page' || home.classList.contains('active')) { cleanInline(); // il CSS sopra fa il resto (4x2 responsive) } }, 0); }, true); // Safety net: se la home diventa active per altri motivi const mo = new MutationObserver(() => { if (home.classList.contains('active')) cleanInline(); }); mo.observe(home, { attributes: true, attributeFilter: ['class'] }); })(); /* === PATCH 1.1.6 — Forza il centro della dashboard al ritorno (append-only) === */ (function centerHomeGridOnActivate(){ const home = document.getElementById('step-selection-page'); const grid = home ? home.querySelector('.step-buttons-grid') : null; if (!home || !grid) return; function centerNow(){ // nessuna misura fissa: centratura a contenuto (resta responsive) grid.style.width = 'fit-content'; grid.style.marginLeft = 'auto'; grid.style.marginRight = 'auto'; grid.style.justifyContent = 'center'; } // Quando premi "Back" e torni alla home, centra document.addEventListener('click', (e) => { const back = e.target.closest('.back-button'); if (!back) return; setTimeout(() => { if (home.classList.contains('active')) centerNow(); }, 0); }, true); // Safety net: qualsiasi volta la home diventa active, centra const mo = new MutationObserver(() => { if (home.classList.contains('active')) centerNow(); }); mo.observe(home, { attributes: true, attributeFilter: ['class'] }); })(); }); }); if (chefcodeLogoBtn){ chefcodeLogoBtn.addEventListener('click', () => showPage('step-selection-page')); } // Account menu if (accountButton && accountDropdownContent){ accountButton.addEventListener('click', () => { accountDropdownContent.style.display = accountDropdownContent.style.display === 'block' ? 'none' : 'block'; }); document.addEventListener('click', (e) => { if (!accountButton.contains(e.target) && !accountDropdownContent.contains(e.target)) { accountDropdownContent.style.display = 'none'; } /* ============ PATCH 1.1.3 — Goods In click fix + Back grid 2x4 ============ */ /* SOLO aggiunte, nessuna modifica al tuo codice esistente */ // 1) Goods In: cattura in modo robusto i click sui 3 pulsanti interni (function rebindGoodsInButtons(){ const goodsInContent = document.getElementById('goods-in-content'); if (!goodsInContent) return; // Usiamo capture=true per intercettare il click anche se ci sono figli (icona/span) goodsInContent.addEventListener('click', (e) => { const btn = e.target.closest('.big-step-button[data-action]'); if (!btn) return; const action = btn.dataset.action; if (!action) return; // Skip invoice-photo action - handled by OCR modal if (action === 'invoice-photo') { return; // Let the OCR modal handle this action } // Mappa azione -> id pagina interna (sono gli ID che hai già in index.html) const map = { 'voice-input' : 'voice-input-page-content', 'manual-input' : 'manual-input-content' }; const targetId = map[action]; if (targetId && typeof showPage === 'function') { e.preventDefault(); showPage(targetId); } }, true); })(); // 2) Back: quando torni alla dashboard, forziamo la griglia 2×4 come all’inizio (function fixBackGrid(){ // intercettiamo TUTTI i back-button già presenti in pagina document.addEventListener('click', (e) => { const back = e.target.closest('.back-button'); if (!back) return; // Lasciamo che il tuo handler faccia showPage(...). Poi sistemiamo la griglia. setTimeout(() => { const targetId = back.dataset.backTarget || ''; // Se torni alla home, rimetti 4 colonne fisse (2 righe x 4) if (targetId === 'step-selection-page' || document.getElementById('step-selection-page')?.classList.contains('active')) { const grid = document.querySelector('#step-selection-page .step-buttons-grid'); if (grid) grid.style.gridTemplateColumns = 'repeat(4, 1fr)'; // 2 file da 4 come da origine } }, 0); }, true); })(); }); } // ---------- Camera/OCR (sim) ---------- function renderCameraIdle(){ if (!cameraViewfinder) return; cameraViewfinder.innerHTML = `
`; } if (cameraViewfinder){ renderCameraIdle(); let capturedFile = null; cameraViewfinder.addEventListener('click', (e) => { if (e.target.closest('#take-photo-btn')) { // Create a real file input for image selection const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'image/*'; fileInput.style.display = 'none'; fileInput.onchange = (ev) => { const file = ev.target.files[0]; if (file) { capturedFile = file; const reader = new FileReader(); reader.onload = function(evt) { cameraViewfinder.innerHTML = `
Invoice
`; }; reader.readAsDataURL(file); } }; document.body.appendChild(fileInput); fileInput.click(); document.body.removeChild(fileInput); } if (e.target.closest('#retake-photo-btn')) { capturedFile = null; renderCameraIdle(); } if (e.target.closest('#confirm-photo-btn') && cameraOutput) { if (!capturedFile) { alert('No image selected. Please take a photo.'); return; } // Show loading cameraOutput.innerHTML = `

Processing invoice...

`; cameraOutput.style.display = ''; // Upload to backend const formData = new FormData(); formData.append('file', capturedFile); const apiKey = window.CHEFCODE_CONFIG?.API_KEY || ''; fetch('http://localhost:8000/api/ocr-invoice', { method: 'POST', headers: { 'X-API-Key': apiKey }, body: formData }) .then(res => res.json()) .then(data => { if (data.status === 'success' && Array.isArray(data.items)) { let added = []; data.items.forEach(item => { // OCR may extract HACCP fields; if not, they can be added manually later addOrMergeInventoryItem({ name: item.name, unit: item.unit, quantity: item.quantity, category: item.category || 'Other', price: item.price, lot_number: item.lot_number || '', expiry_date: item.expiry_date || '' }); added.push(`${item.name} (${item.quantity} ${item.unit} @ €${item.price})`); }); updateInventoryToBackend(); renderInventory(); cameraOutput.innerHTML = `

OCR Extraction Result

💡 Tip: Add lot numbers and expiry dates via Manual Input for HACCP compliance

`; } else { cameraOutput.innerHTML = `

OCR failed

${data.message || 'Could not extract items.'}
`; } }) .catch(err => { cameraOutput.innerHTML = `

OCR error

${err.message}
`; }); } }); } // ---------- Voice (sim + process) ---------- if (microphoneBtn){ microphoneBtn.addEventListener('click', () => { isRecording = !isRecording; if (isRecording){ if (voiceStatus) voiceStatus.textContent = 'Listening...'; if (micLabel) micLabel.textContent = 'Stop Recording'; setTimeout(() => { if (recognizedTextContent) recognizedTextContent.textContent = '"pomodori 20 chili 2 euro e 50"'; if (voiceRecognizedText) voiceRecognizedText.style.display = 'block'; }, 1200); } else { if (micLabel) micLabel.textContent = 'Start Recording'; if (voiceStatus) voiceStatus.textContent = 'Press the microphone to start speaking...'; if (voiceRecognizedText) voiceRecognizedText.style.display = 'none'; if (recognizedTextContent) recognizedTextContent.textContent = ''; } }); } function parseItalianGoods(text){ const t = (text||'').toLowerCase().replace(/"/g,' ').replace(/\s+/g,' ').trim(); // prezzo: "2 euro e 50" | "€2,50" let price = 0; const pm = t.match(/(\d+[.,]?\d*)\s*(?:€|euro)?(?:\s*e\s*(\d{1,2}))?/); if (pm){ const euros = parseFloat(pm[1].replace(',','.')); const cents = pm[2] ? parseInt(pm[2]) : 0; price = (isNaN(euros)?0:euros) + (isNaN(cents)?0:cents)/100; } const unitMap = { chili:'kg', chilo:'kg', chilogrammi:'kg', kg:'kg', grammi:'g', g:'g', litro:'l', litri:'l', lt:'l', l:'l', millilitri:'ml', ml:'ml', pezzi:'pz', pezzo:'pz', uova:'pz', pz:'pz', bt:'bt' }; const qm = t.match(/(\d+[.,]?\d*)\s*(kg|g|l|lt|ml|pz|bt|litro|litri|chili|chilo|chilogrammi|grammi|millilitri|pezzi|pezzo|uova)\b/); const qty = qm ? parseFloat(qm[1].replace(',','.')) : 1; const unit = qm ? (unitMap[qm[2]] || qm[2]) : 'pz'; const name = qm ? t.slice(0, t.indexOf(qm[0])).trim() : t; return { name: name || 'item', qty: isNaN(qty)?1:qty, unit, price: isNaN(price)?0:price }; } if (processVoiceBtn && recognizedTextContent){ processVoiceBtn.addEventListener('click', () => { const text = recognizedTextContent.textContent || ''; const { name, qty, unit, price } = parseItalianGoods(text); if (!name){ alert('Voice: nessun nome articolo rilevato'); return; } addOrMergeInventoryItem({ name, unit, quantity: qty, category: 'Other', price }); renderInventory(); // Update display immediately updateInventoryToBackend(); alert(`Voice→Inventory: ${name} — ${qty} ${unit} @ €${price.toFixed(2)}`); }); } // ---------- Inventory ---------- function rowToItem(tr){ const tds = tr?.querySelectorAll('td'); if (!tds || tds.length < 6) return null; const name = tds[0].textContent.trim(); const priceText = tds[1].textContent.replace('€','').trim(); const unit = tds[2].textContent.trim(); const quantityText = tds[3].textContent.trim(); const category = tds[4].textContent.trim(); const price = parseFloat(priceText.replace(',','.')) || 0; const quantity = parseFloat(quantityText.replace(',','.')) || 0; return { name, unit, quantity, category, price }; } window.renderInventory = function(){ if (!inventoryTableBody) return; // Validate STATE and inventory array exist if (!window.STATE || !Array.isArray(window.STATE.inventory)) { console.warn('⚠️ Invalid STATE or inventory array'); window.STATE = window.STATE || { inventory: [], recipes: {}, tasks: [], nextTaskId: 1 }; window.STATE.inventory = []; } inventoryTableBody.innerHTML = ''; let total = 0; const today = new Date(); today.setHours(0, 0, 0, 0); window.STATE.inventory.forEach(item => { // Validate item structure if (!item || typeof item !== 'object') return; const rowTotal = (item.price||0) * (item.quantity||0); total += rowTotal; const tr = document.createElement('tr'); // Calculate expiry alert level let expiryHTML = '-'; let expiryClass = ''; if (item.expiry_date) { const expiryDate = new Date(item.expiry_date); const daysUntilExpiry = Math.ceil((expiryDate - today) / (1000 * 60 * 60 * 24)); if (daysUntilExpiry < 2) { expiryClass = 'expiry-critical'; // Red expiryHTML = `${window.escapeHtml(item.expiry_date)} (${daysUntilExpiry}d)`; } else if (daysUntilExpiry < 7) { expiryClass = 'expiry-warning'; // Yellow expiryHTML = `${window.escapeHtml(item.expiry_date)} (${daysUntilExpiry}d)`; } else { expiryHTML = window.escapeHtml(item.expiry_date); } } tr.innerHTML = ` ${window.escapeHtml(item.name || '')} €${(item.price ?? 0).toFixed(2)} ${window.escapeHtml(item.unit || '-')} ${item.quantity ?? 0} ${window.escapeHtml(item.category || '-')} ${window.escapeHtml(item.lot_number || '-')} ${expiryHTML} €${rowTotal.toFixed(2)} `; inventoryTableBody.appendChild(tr); }); if (inventoryTotalVal) inventoryTotalVal.textContent = `€${total.toFixed(2)}`; populateIngredientSelect(); // tiene il select aggiornato } // Bootstrap inventory: always fetch from backend, do not import from DOM or localStorage if (inventoryTableBody){ fetchInventoryFromBackend(); } // Manual Input (sovrascrive submit per evitare doppie append) if (manualEntryForm){ manualEntryForm.addEventListener('submit', async (e) => { e.preventDefault(); const name = (el('item-name')?.value || '').trim(); const qty = parseFloat(el('item-quantity')?.value || '0'); const unit = el('item-unit')?.value || 'pz'; const price= parseFloat(el('item-price')?.value || '0'); const cat = el('item-category')?.value || 'Other'; const lotNumber = el('item-lot-number')?.value || ''; const expiryDate = el('item-expiry-date')?.value || ''; if (!name || isNaN(qty) || isNaN(price)){ alert('Inserisci nome, quantità e prezzo validi.'); return; } // Validate expiry date is not in the past (if provided) if (expiryDate) { const expiry = new Date(expiryDate); const today = new Date(); today.setHours(0, 0, 0, 0); if (expiry < today) { alert('Expiry date cannot be in the past!'); return; } } addOrMergeInventoryItem({ name, unit, quantity: qty, category: cat, price, lot_number: lotNumber, expiry_date: expiryDate }); renderInventory(); // Update display immediately await updateInventoryToBackend(); safe(()=>manualEntryForm.reset()); safe(()=>el('item-name').focus()); alert(`"${name}" aggiunto in inventario`); }); } // Search / Filter function applyInventoryFilters(){ if (!inventoryTableBody) return; const term = (inventorySearch?.value || '').toLowerCase(); const cat = (categoryFilter?.value || 'All'); let total = 0; qa('#inventory-table-body tr').forEach(row => { const name = row.children[0]?.textContent.toLowerCase() || ''; const rc = row.children[4]?.textContent || ''; const isAll = cat.toLowerCase() === 'all'; const ok = (!term || name.includes(term)) && (isAll || cat === '' || rc === cat); row.style.display = ok ? '' : 'none'; if (ok){ const tv = row.children[7]?.textContent.replace('€','').trim(); const val= parseFloat(tv?.replace('.','').replace(',','.')) || 0; total += val; } }); if (inventoryTotalVal) inventoryTotalVal.textContent = `€${total.toFixed(2)}`; } // Delete inventory item function - attach to window for global access window.deleteInventoryItem = async function(itemId) { if (!confirm('Are you sure you want to delete this item? This action cannot be undone.')) { return; } try { const apiKey = window.CHEFCODE_CONFIG?.API_KEY; if (!apiKey) { alert('API key not configured'); return; } const response = await fetch('http://localhost:8000/api/inventory/delete', { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey }, body: JSON.stringify({ id: itemId }) }); if (response.ok) { // Remove from local state window.STATE.inventory = window.STATE.inventory.filter(item => item.id !== itemId); window.renderInventory(); // No need for full sync after successful delete - local state is already updated console.log('Item deleted successfully'); } else { let errorMessage = 'Unknown error'; try { const error = await response.json(); errorMessage = error.detail || error.message || 'Unknown error'; } catch (e) { errorMessage = `Server error (${response.status}): ${response.statusText}`; } alert(`Failed to delete item: ${errorMessage}`); } } catch (error) { console.error('Delete error:', error); alert('Failed to delete item. Please try again.'); } }; if (inventorySearch) inventorySearch.addEventListener('input', applyInventoryFilters); if (categoryFilter) categoryFilter.addEventListener('change', applyInventoryFilters); if (expandTableBtn && inventoryTableCtr){ expandTableBtn.addEventListener('click', () => inventoryTableCtr.classList.toggle('expanded')); } // ---------- Recipes ---------- function renderIngredientsList(){ if (!recipeIngredientsList) return; recipeIngredientsList.innerHTML = ''; currentRecipeIngredients.forEach((ing, i) => { const li = document.createElement('li'); li.innerHTML = `${ing.name} - ${ing.quantity} ${ing.unit} `; recipeIngredientsList.appendChild(li); }); } function populateIngredientSelect(){ if (!ingredientSelect) return; const current = ingredientSelect.value; ingredientSelect.innerHTML = ``; const seen = new Set(); window.STATE.inventory.forEach(it => { if (seen.has(it.name)) return; seen.add(it.name); const opt = document.createElement('option'); opt.value = it.name; opt.textContent = it.name; ingredientSelect.appendChild(opt); }); if (current && seen.has(current)) ingredientSelect.value = current; updateRecipeSelects(); // mantiene in sync Produzione } if (addIngredientBtn){ addIngredientBtn.addEventListener('click', () => { const name = ingredientSelect?.value || ''; const qty = parseFloat(ingredientQty?.value || '0'); const unit = ingredientUnit?.value || 'g'; if (!name || !qty){ alert('Seleziona ingrediente e quantità.'); return; } currentRecipeIngredients.push({ name, quantity: qty, unit }); renderIngredientsList(); if (ingredientSelect) ingredientSelect.value = ''; if (ingredientQty) ingredientQty.value = ''; }); } if (recipeIngredientsList){ recipeIngredientsList.addEventListener('click', (e) => { const btn = e.target.closest('.remove-ingredient-btn'); if (!btn) return; const idx = parseInt(btn.dataset.index, 10); if (!isNaN(idx)) currentRecipeIngredients.splice(idx, 1); renderIngredientsList(); }); } function updateRecipeSelects(){ if (!recipeSelectProd) return; const current = recipeSelectProd.value; recipeSelectProd.innerHTML = ''; Object.keys(window.STATE.recipes).forEach(name => { const opt = document.createElement('option'); opt.value = name; opt.textContent = name; recipeSelectProd.appendChild(opt); }); if (window.STATE.recipes[current]) recipeSelectProd.value = current; } if (saveRecipeBtn) { saveRecipeBtn.addEventListener('click', async () => { const recipeName = (document.getElementById('recipe-name')?.value || '').trim(); if (!recipeName) { alert('Please enter a name for the recipe.'); return; } if (!currentRecipeIngredients.length) { alert('Please add at least one ingredient.'); return; } // Get yield data const yieldQty = parseFloat(el('recipe-yield-qty')?.value || 0); const yieldUnit = el('recipe-yield-unit')?.value || 'pz'; console.log(`💾 Saving recipe: ${recipeName} with yield:`, yieldQty > 0 ? `${yieldQty} ${yieldUnit}` : 'none'); // Salva nel motore usato dalla Production: STATE.recipes // e usa il campo "qty" (non "quantity") perché la deduzione legge ing.qty window.STATE.recipes[recipeName] = { items: currentRecipeIngredients.map(i => ({ name: i.name, qty: parseFloat(i.quantity) || 0, // quantità per 1 batch unit: i.unit })), // Save yield information yield: yieldQty > 0 ? { qty: yieldQty, unit: yieldUnit } : null }; try { await updateInventoryToBackend(); } catch (error) { console.error('Failed to save recipe:', error); return; // Don't continue if save failed } // Aggiorna il menu a tendina in Production updateRecipeSelects(); // Feedback let summary = `Recipe Saved: ${recipeName}\n\nIngredients:\n`; window.STATE.recipes[recipeName].items.forEach(ing => { summary += `- ${ing.name}: ${ing.qty} ${ing.unit}\n`; }); alert(summary); // Reset form ricetta currentRecipeIngredients = []; renderIngredientsList(); const rn = document.getElementById('recipe-name'); if (rn) rn.value = ''; const ri = document.getElementById('recipe-instructions'); if (ri) ri.value = ''; const yq = el('recipe-yield-qty'); if (yq) yq.value = ''; const yu = el('recipe-yield-unit'); if (yu) yu.value = 'pz'; if (ingredientSelect) ingredientSelect.value = ''; if (ingredientQty) ingredientQty.value = ''; }); } // Primo allineamento selects populateIngredientSelect(); updateRecipeSelects(); // ---------- Recipe Catalogue ---------- const recipeCatalogueBody = el('recipe-catalogue-body'); const recipeCatalogueEmpty = el('recipe-catalogue-empty'); const recipeSearchInput = el('recipe-search'); // Render recipe catalogue table window.renderRecipeCatalogue = function() { console.log('📖 Rendering recipe catalogue...'); if (!recipeCatalogueBody) { console.warn('⚠️ Recipe catalogue body element not found'); return; } recipeCatalogueBody.innerHTML = ''; const recipes = window.STATE.recipes || {}; const recipeNames = Object.keys(recipes); console.log(`Found ${recipeNames.length} recipes in STATE:`, recipeNames); // Show/hide empty state if (recipeCatalogueEmpty) { recipeCatalogueEmpty.style.display = recipeNames.length === 0 ? 'block' : 'none'; } if (recipeNames.length === 0) { console.log('No recipes to display'); return; } // Get search term const searchTerm = recipeSearchInput?.value.toLowerCase() || ''; recipeNames.forEach(recipeName => { // Apply search filter if (searchTerm && !recipeName.toLowerCase().includes(searchTerm)) { return; } const recipe = recipes[recipeName]; const ingredients = recipe.items || []; const ingredientCount = ingredients.length; // Build full ingredients list for display const ingredientsList = ingredients.map(ing => { const qty = ing.qty !== undefined && ing.qty !== null ? ing.qty : '?'; const unit = ing.unit !== undefined && ing.unit !== null ? ing.unit : ''; return `
  • ${window.escapeHtml(ing.name)} ${qty} ${unit}
  • `; }).join(''); // Get yield info const yieldInfo = recipe.yield ? `${recipe.yield.qty} ${recipe.yield.unit}` : 'Not specified'; // Create card element const card = document.createElement('div'); card.className = 'recipe-card'; card.innerHTML = `

    ${window.escapeHtml(recipeName)}

    ${ingredientCount} ingredient${ingredientCount !== 1 ? 's' : ''}
    Ingredients
      ${ingredientsList}
    Yield: ${window.escapeHtml(yieldInfo)}
    `; recipeCatalogueBody.appendChild(card); }); }; // Delete recipe async function deleteRecipe(recipeName) { if (!confirm(`Are you sure you want to delete the recipe "${recipeName}"?`)) { return; } console.log(`🗑️ Deleting recipe: ${recipeName}`); delete window.STATE.recipes[recipeName]; try { await updateInventoryToBackend(); renderRecipeCatalogue(); updateRecipeSelects(); // Update dropdowns in production alert(`Recipe "${recipeName}" has been deleted and saved to database.`); } catch (error) { console.error('Failed to delete recipe:', error); // Recipe already deleted from STATE, but sync failed } } // Edit recipe - navigate to add recipe page and populate form function editRecipe(recipeName) { const recipe = window.STATE.recipes[recipeName]; if (!recipe) return; // Store recipe name for editing window.editingRecipeName = recipeName; // Navigate to add recipe page const addRecipePage = el('add-recipe-content'); if (addRecipePage) { // Clear current state qa('.input-page').forEach(p => p.classList.remove('active')); el('step-selection-page')?.classList.remove('active'); el('input-detail-page')?.classList.add('active'); addRecipePage.classList.add('active'); // Populate form const recipeNameInput = el('recipe-name'); if (recipeNameInput) { recipeNameInput.value = recipeName; recipeNameInput.disabled = true; // Don't allow name change during edit } // Populate yield fields const yieldQtyInput = el('recipe-yield-qty'); const yieldUnitInput = el('recipe-yield-unit'); if (recipe.yield) { if (yieldQtyInput) yieldQtyInput.value = recipe.yield.qty || ''; if (yieldUnitInput) yieldUnitInput.value = recipe.yield.unit || 'pz'; } else { if (yieldQtyInput) yieldQtyInput.value = ''; if (yieldUnitInput) yieldUnitInput.value = 'pz'; } // Populate ingredients currentRecipeIngredients = recipe.items.map(ing => ({ name: ing.name, quantity: ing.qty, unit: ing.unit })); renderIngredientsList(); // Change button text if (saveRecipeBtn) { saveRecipeBtn.innerHTML = ' Update Recipe'; } alert(`Editing recipe: ${recipeName}\nModify ingredients and click "Update Recipe" to save changes.`); } } // Handle edit/delete button clicks if (recipeCatalogueBody) { recipeCatalogueBody.addEventListener('click', (e) => { const editBtn = e.target.closest('.edit-recipe-btn'); const deleteBtn = e.target.closest('.delete-recipe-btn'); if (editBtn) { const recipeName = editBtn.dataset.recipe; editRecipe(recipeName); } else if (deleteBtn) { const recipeName = deleteBtn.dataset.recipe; deleteRecipe(recipeName); } }); } // Search recipes if (recipeSearchInput) { recipeSearchInput.addEventListener('input', () => { renderRecipeCatalogue(); }); } // Update save recipe button to handle edit mode if (saveRecipeBtn) { const originalClickHandler = saveRecipeBtn.onclick; saveRecipeBtn.addEventListener('click', () => { // After saving, reset edit mode if (window.editingRecipeName) { const recipeNameInput = el('recipe-name'); if (recipeNameInput) { recipeNameInput.disabled = false; } delete window.editingRecipeName; if (saveRecipeBtn) { saveRecipeBtn.innerHTML = ' Save Recipe'; } } // Update catalogue if it's visible setTimeout(() => { renderRecipeCatalogue(); }, 100); }); } // ---------- Production ---------- const renderProductionTasks = () => { if (!todoTasksContainer || !completedTasksList) return; // Tab To Do todoTasksContainer.innerHTML = '

    To Do

    '; // Tab Completed completedTasksList.innerHTML = '

    Completed

    '; window.productionTasks.forEach(task => { const card = document.createElement('div'); card.className = 'task-card'; card.dataset.id = String(task.id); card.innerHTML = `
    ${task.recipe} (${task.quantity})

    Assegnato a: ${task.assignedTo || '—'}

    `; if (task.status === 'todo') { todoTasksContainer.appendChild(card); } else if (task.status === 'completed') { completedTasksList.appendChild(card); } }); }; // ==== Helpers per deduzione inventario (unità + nomi) ==== function ccNormUnit(u){ u = String(u || '').trim().toLowerCase(); if (u === 'l') u = 'lt'; if (u === 'gr') u = 'g'; if (u === 'pcs' || u === 'pc' || u === 'pz.') u = 'pz'; return u; } function ccConvertFactor(from, to){ from = ccNormUnit(from); to = ccNormUnit(to); if (from === to) return 1; if (from === 'kg' && to === 'g') return 1000; if (from === 'g' && to === 'kg') return 1/1000; if (from === 'lt' && to === 'ml') return 1000; if (from === 'ml' && to === 'lt') return 1/1000; // equivalenza “di comodo” se tratti bottle/pezzi come unità contabili if ((from === 'pz' && to === 'bt') || (from === 'bt' && to === 'pz')) return 1; return null; // incompatibili } function ccNormName(s){ return String(s || '').trim().toLowerCase(); } function ccFindInventoryItemByName(name){ const wanted = ccNormName(name); return window.STATE.inventory.find(it => ccNormName(it.name) === wanted) || null; } function consumeInventoryForTask(task){ const r = window.STATE.recipes[task.recipe]; if (!r){ alert(`Ricetta non trovata: ${task.recipe}`); return; } const batches = Number(task.quantity) || 1; // quante “unità ricetta” produci let changed = false; const skipped = []; r.items.forEach(ing => { const inv = ccFindInventoryItemByName(ing.name); if (!inv){ skipped.push(`${ing.name} (non in inventario)`); return; } const invU = ccNormUnit(inv.unit); const ingU = ccNormUnit(ing.unit || invU); const f = ccConvertFactor(ingU, invU); if (f === null){ skipped.push(`${ing.name} (${ingU}→${invU} incompatibile)`); return; } const perBatch = Number(ing.qty) || 0; const toConsume = perBatch * batches * f; inv.quantity = Math.max(0, (Number(inv.quantity) || 0) - toConsume); changed = true; }); if (changed){ updateInventoryToBackend(); renderInventory(); } if (skipped.length){ console.warn('Ingredienti non scalati:', skipped); } } function onTaskActionClick(e){ const btn = e.target.closest('.task-action-btn'); if (!btn) return; const card = btn.closest('.task-card'); if (!card) return; const id = Number(card.dataset.id); const task = window.STATE.tasks.find(t => t.id === id); if (!task) return; if (task.status === 'todo'){ task.status = 'inprogress'; } else if (task.status === 'inprogress'){ task.status = 'completed'; consumeInventoryForTask(task); } renderInventory(); // Update inventory display after consumption window.updateInventoryToBackend(); renderProductionTasks(); } if (addTaskBtn) { // evita comportamenti da "submit" nel caso in cui il bottone fosse dentro un form addTaskBtn.setAttribute('type','button'); addTaskBtn.addEventListener('click', (e) => { e.preventDefault(); const recipe = recipeSelectProd && recipeSelectProd.value; const quantity = productionQty && productionQty.value; const assignedToVal = assignTo && assignTo.value; const initialStatus = (initialStatusSelect && initialStatusSelect.value) || 'todo'; if (!recipe || !quantity) { alert('Please select a recipe and specify the quantity.'); return; } window.taskIdCounter += 1; const newTask = { id: window.taskIdCounter, recipe, quantity: Number(quantity), assignedTo: assignedToVal || '', status: (initialStatus === 'completed') ? 'completed' : 'todo' }; // Se l'utente ha scelto "Completed", scala subito l'inventario if (initialStatus === 'completed') { try { consumeInventoryForTask(newTask); } catch (e) { console.warn('consume-on-create failed', e); } } window.productionTasks.push(newTask); renderProductionTasks(); if (productionQty) productionQty.value = ''; if (recipeSelectProd) recipeSelectProd.value = ''; if (initialStatusSelect) initialStatusSelect.value = 'todo'; }); } // Gestione click su To Do: convalida task if (todoTasksContainer) { todoTasksContainer.addEventListener('click', function(event) { const btn = event.target.closest('button.task-action-btn'); if (!btn) return; const card = btn.closest('.task-card'); const taskId = parseInt(card.dataset.id, 10); const task = window.productionTasks.find(t => t.id === taskId); if (!task) return; // Deduzione ingredienti SOLO ora try { consumeInventoryForTask(task); } catch(e){} task.status = 'completed'; renderProductionTasks(); }); } // Tab switching logic if (typeof prodTabBtns !== 'undefined' && prodTabBtns.length) { prodTabBtns.forEach(btn => { btn.addEventListener('click', function() { prodTabBtns.forEach(b => b.classList.remove('active')); prodTabPanels.forEach(p => p.classList.remove('active')); btn.classList.add('active'); const tab = btn.getAttribute('data-tab'); if (tab === 'todo') { todoTasksContainer.classList.add('active'); } else if (tab === 'completed') { completedTasksList.classList.add('active'); } }); }); } // Inizializza tasks view e riallinea contatore try { const maxExisting = window.STATE.tasks.reduce((m, t) => { const id = Number(t?.id) || 0; return id > m ? id : m; }, 0); const current = Number(window.STATE.nextTaskId) || 1; window.STATE.nextTaskId = Math.max(current, maxExisting + 1); } catch (e) { console.warn('Reindex nextTaskId failed', e); window.STATE.nextTaskId = Number(window.STATE.nextTaskId) || 1; } renderProductionTasks(); // ---------- Back buttons ---------- qa('.back-button').forEach(btn => { btn.addEventListener('click', () => { const target = btn.dataset.backTarget || 'step-selection-page'; showPage(target); }); }); // ---------- Dev reset ---------- window.addEventListener('keydown', (e) => { if (e.ctrlKey && e.altKey && String(e.key).toLowerCase() === 'r'){ if (confirm('Reset ChefCode data?')){ localStorage.removeItem(STORAGE_KEY); location.reload(); } } }); }); /* ===== CHEFCODE PATCH BACK-RESTORE 1.0 — START ===== */ (function(){ const home = document.getElementById('step-selection-page'); const inputDetail = document.getElementById('input-detail-page'); const homeGrid = home ? home.querySelector('.step-buttons-grid') : null; if (!home || !inputDetail || !homeGrid) return; // Snapshot dello stato iniziale (quello giusto che vedi all'apertura) const ORIGINAL_CLASS = homeGrid.className; const HAD_STYLE = homeGrid.hasAttribute('style'); const ORIGINAL_STYLE = homeGrid.getAttribute('style'); function restoreHome(){ // 1) Mostra la home, nascondi area dettagli e qualsiasi sotto-pagina ancora attiva document.querySelectorAll('#input-pages-container .input-page.active') .forEach(p => p.classList.remove('active')); inputDetail.classList.remove('active'); home.classList.add('active'); // 2) Ripristina la griglia ESATTAMENTE come all'avvio homeGrid.className = ORIGINAL_CLASS; if (HAD_STYLE) { homeGrid.setAttribute('style', ORIGINAL_STYLE || ''); } else { homeGrid.removeAttribute('style'); } // 3) Pulisci eventuali proprietà inline appiccicate da patch vecchie if (homeGrid.style) { [ 'grid-template-columns','grid-template-rows','width', 'margin-left','margin-right','left','right','transform', 'justify-content','max-width' ].forEach(prop => homeGrid.style.removeProperty(prop)); } } // Intercetta TUTTI i "Back" e, dopo che i tuoi handler hanno girato, ripristina la home document.addEventListener('click', (e) => { const back = e.target.closest('.back-button'); if (!back) return; setTimeout(() => { const targetId = back.getAttribute('data-back-target') || ''; if (targetId === 'step-selection-page') restoreHome(); }, 0); }, true); /* ===== CHEFCODE PATCH HOME-RESTORE 1.0 — START ===== */ (function(){ const home = document.getElementById('step-selection-page'); const inputDetail = document.getElementById('input-detail-page'); const grid = home ? home.querySelector('.step-buttons-grid') : null; const homeBtn = document.getElementById('chefcode-logo-btn'); if (!home || !inputDetail || !grid || !homeBtn) return; // Prendiamo uno snapshot della griglia com'è all'apertura (stato "buono") const ORIGINAL_CLASS = grid.className; const HAD_STYLE = grid.hasAttribute('style'); const ORIGINAL_STYLE = grid.getAttribute('style'); function cleanGridToInitial(){ // Classi originali grid.className = ORIGINAL_CLASS; // Stile inline: se all’inizio non c’era, lo togliamo; altrimenti rimettiamo il valore originale if (HAD_STYLE) grid.setAttribute('style', ORIGINAL_STYLE || ''); else grid.removeAttribute('style'); // Rimuovi qualsiasi proprietà inline residua che possa decentrarla o rimpicciolirla if (grid.style){ [ 'grid-template-columns','grid-template-rows','width','max-width', 'margin-left','margin-right','left','right','transform','justify-content' ].forEach(p => grid.style.removeProperty(p)); } } function restoreHome(){ // Chiudi eventuali sotto-pagine attive document.querySelectorAll('#input-pages-container .input-page.active') .forEach(p => p.classList.remove('active')); inputDetail.classList.remove('active'); home.classList.add('active'); // Ripristina la griglia allo stato iniziale (come al primo load) cleanGridToInitial(); } // Quando clicchi la "casetta", lasciamo agire il tuo handler e poi ripristiniamo (come fatto per il Back) homeBtn.addEventListener('click', () => { setTimeout(restoreHome, 0); }, true); })(); // ===== OCR MODAL FUNCTIONALITY ===== class OCRModal { constructor() { this.modal = document.getElementById('ocr-modal'); this.currentStream = null; this.currentFile = null; this.ocrResults = null; this.currentScreen = 'selection'; this.initializeEventListeners(); } initializeEventListeners() { // Modal open/close document.addEventListener('click', (e) => { if (e.target.closest('[data-action="invoice-photo"]')) { e.preventDefault(); this.openModal(); } }); // Close modal document.getElementById('ocr-modal-close-btn').addEventListener('click', () => { this.closeModal(); }); // Close on overlay click this.modal.addEventListener('click', (e) => { if (e.target === this.modal) { this.closeModal(); } }); // Selection screen document.getElementById('camera-option').addEventListener('click', () => { this.showCameraScreen(); }); document.getElementById('upload-option').addEventListener('click', () => { this.showFileUpload(); }); // Camera screen document.getElementById('ocr-camera-back').addEventListener('click', () => { this.showSelectionScreen(); }); document.getElementById('ocr-camera-capture').addEventListener('click', () => { this.capturePhoto(); }); document.getElementById('ocr-camera-switch').addEventListener('click', () => { this.switchCamera(); }); // Preview screen document.getElementById('ocr-preview-back').addEventListener('click', () => { this.showCameraScreen(); }); document.getElementById('ocr-preview-process').addEventListener('click', () => { this.processInvoice(); }); // Results screen document.getElementById('ocr-results-back').addEventListener('click', () => { this.showSelectionScreen(); }); document.getElementById('ocr-results-confirm').addEventListener('click', () => { this.confirmAndAddToInventory(); }); // Success screen document.getElementById('ocr-success-close').addEventListener('click', () => { this.closeModal(); }); } openModal() { this.modal.style.display = 'flex'; setTimeout(() => { this.modal.classList.add('show'); }, 10); document.body.style.overflow = 'hidden'; this.showSelectionScreen(); } closeModal() { this.modal.classList.remove('show'); setTimeout(() => { this.modal.style.display = 'none'; document.body.style.overflow = ''; this.cleanup(); }, 300); } showScreen(screenId) { // Hide all screens document.querySelectorAll('.ocr-screen').forEach(screen => { screen.style.display = 'none'; }); // Show target screen document.getElementById(screenId).style.display = 'flex'; this.currentScreen = screenId; } showSelectionScreen() { this.showScreen('ocr-selection-screen'); this.cleanup(); } showCameraScreen() { this.showScreen('ocr-camera-screen'); this.initializeCamera(); } showPreviewScreen() { this.showScreen('ocr-preview-screen'); } showProcessingScreen() { this.showScreen('ocr-processing-screen'); } showResultsScreen() { this.showScreen('ocr-results-screen'); } showSuccessScreen() { this.showScreen('ocr-success-screen'); } async initializeCamera() { try { this.currentStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment', width: { ideal: 1280 }, height: { ideal: 720 } } }); const video = document.getElementById('ocr-camera-preview'); video.srcObject = this.currentStream; video.play(); } catch (error) { console.error('Camera access denied:', error); alert('Camera access is required to take photos. Please allow camera access and try again.'); this.showSelectionScreen(); } } switchCamera() { // Simple camera switch - in a real implementation, you'd cycle through available cameras if (this.currentStream) { this.currentStream.getTracks().forEach(track => track.stop()); } this.initializeCamera(); } capturePhoto() { const video = document.getElementById('ocr-camera-preview'); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; context.drawImage(video, 0, 0); canvas.toBlob((blob) => { this.currentFile = new File([blob], 'captured-photo.jpg', { type: 'image/jpeg' }); this.showPreviewScreen(); // Display preview const previewImg = document.getElementById('ocr-preview-image'); previewImg.src = URL.createObjectURL(blob); }, 'image/jpeg', 0.8); } showFileUpload() { const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*,.pdf'; input.style.display = 'none'; input.onchange = (e) => { const file = e.target.files[0]; if (file) { this.currentFile = file; this.showPreviewScreen(); // Display preview const previewImg = document.getElementById('ocr-preview-image'); if (file.type.startsWith('image/')) { previewImg.src = URL.createObjectURL(file); } else { previewImg.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2Y4ZjlmYSIvPjx0ZXh0IHg9IjUwIiB5PSI1MCIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjE0IiBmaWxsPSIjN2Y4YzhkIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iLjNlbSI+UERGIEZpbGU8L3RleHQ+PC9zdmc+'; } } }; document.body.appendChild(input); input.click(); document.body.removeChild(input); } async processInvoice() { if (!this.currentFile) { alert('No file selected'); return; } this.showProcessingScreen(); try { const formData = new FormData(); formData.append('file', this.currentFile); const apiKey = window.CHEFCODE_CONFIG?.API_KEY || ''; const response = await fetch('http://localhost:8000/api/ocr-invoice', { method: 'POST', headers: { 'X-API-Key': apiKey }, body: formData }); const data = await response.json(); // Handle service unavailable (503) - OCR not configured if (response.status === 503) { alert('⚠️ OCR Service Not Available\n\n' + 'The OCR feature requires Google Cloud credentials.\n\n' + 'Please use Manual Input instead or contact your administrator to configure:\n' + '• Google Cloud Project ID\n' + '• Document AI Processor\n' + '• Gemini API Key'); this.showSelectionScreen(); return; } if (!response.ok) { throw new Error(data.detail || `Server error: ${response.status}`); } if (data.status === 'success' && Array.isArray(data.items)) { this.ocrResults = data; this.displayResults(data); this.showResultsScreen(); } else { throw new Error(data.message || 'OCR processing failed'); } } catch (error) { console.error('OCR processing error:', error); alert(`OCR processing failed: ${error.message}`); this.showSelectionScreen(); } } displayResults(data) { // Update metadata with null checks const supplierElement = document.getElementById('ocr-supplier-name'); if (supplierElement) { supplierElement.textContent = `Supplier: ${data.supplier || 'Unknown'}`; } const dateElement = document.getElementById('ocr-invoice-date'); if (dateElement) { dateElement.textContent = `Date: ${data.date || 'Unknown'}`; } // Populate results table with EDITABLE cells const tbody = document.getElementById('ocr-results-tbody'); if (tbody && Array.isArray(data.items)) { tbody.innerHTML = ''; data.items.forEach((item, index) => { const row = document.createElement('tr'); row.dataset.index = index; row.innerHTML = ` `; tbody.appendChild(row); }); // Add event listeners to sync changes back to data tbody.querySelectorAll('.ocr-edit-input').forEach(input => { input.addEventListener('change', (e) => { const row = e.target.closest('tr'); const index = parseInt(row.dataset.index); const field = e.target.dataset.field; let value = e.target.value; // Convert numeric fields if (field === 'quantity' || field === 'price') { value = parseFloat(value) || 0; } // Update the data object if (this.ocrResults && this.ocrResults.items[index]) { this.ocrResults.items[index][field] = value; } }); }); } } async confirmAndAddToInventory() { if (!this.ocrResults || !this.ocrResults.items) { alert('No OCR results to add'); return; } try { // Add items to inventory let addedCount = 0; this.ocrResults.items.forEach(item => { window.addOrMergeInventoryItem({ name: item.name, unit: item.unit, quantity: item.quantity, category: item.category || 'Other', price: item.price, lot_number: item.lot_number || '', expiry_date: item.expiry_date || '' }); addedCount++; }); // Sync to backend await window.updateInventoryToBackend(); window.renderInventory(); this.showSuccessScreen(); } catch (error) { console.error('Error adding to inventory:', error); alert(`Failed to add items to inventory: ${error.message}`); } } cleanup() { // Stop camera stream if (this.currentStream) { this.currentStream.getTracks().forEach(track => track.stop()); this.currentStream = null; } // Clear file references this.currentFile = null; this.ocrResults = null; // Clear preview image const previewImg = document.getElementById('ocr-preview-image'); if (previewImg.src && previewImg.src.startsWith('blob:')) { URL.revokeObjectURL(previewImg.src); previewImg.src = ''; } } } // Global utility function for HTML escaping window.escapeHtml = function(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }; // Initialize OCR Modal const ocrModal = new OCRModal(); // QuickAddPopup removed - ready for new AI toolbar implementation // Make OCR modal globally accessible window.ocrModal = ocrModal; })(); window.CHEFCODE_RESET = () => { alert('ChefCode storage azzerato'); location.reload(); };