Filamento Manager FDM Cloud

Connessione al servizio in corso…

Usa l'account già creato in Supabase. La sessione resta memorizzata su questo dispositivo.

`);w.document.close(); } function renderAll(){renderDashboard();renderWarehouse();renderSpools();renderPrinters();renderJobOptions();renderJobs();renderCatalogs();} function resetSpool(){ $('spoolForm').reset();$('spoolId').value='';$('spoolFormTitle').textContent='Aggiungi una bobina';$('spoolMinimum').value=150;$('spoolDiameter').value='1.75';$('spoolProduct').value='';$('spoolLocation').value='deposito';} function resetPrinter(){ $('printerForm').reset();$('printerId').value='';$('printerFormTitle').textContent='Aggiungi una stampante';} function resetJob(){ $('jobForm').reset();$('jobDate').value=today();$('jobQuantity').value=1;updateJobPreview();} function updateJobPreview(){ const s=getSpool($('jobSpool').value),g=Number($('jobGrams').value)||0; $('jobCostPreview').textContent=euro(s?g*costPerGram(s):0); $('jobRemainingPreview').textContent=s?num(Number(s.remaining)-g)+' g':'—'; $('jobRemainingPreview').style.color=s&&g>Number(s.remaining)?'var(--red)':'inherit'; } $('nav').addEventListener('click',e=>{const b=e.target.closest('button[data-page]');if(!b)return;document.querySelectorAll('nav button').forEach(x=>x.classList.toggle('active',x===b));document.querySelectorAll('.page').forEach(x=>x.classList.toggle('hidden',x.id!==b.dataset.page));}); $('spoolForm').addEventListener('submit',e=>{e.preventDefault();const id=$('spoolId').value,initial=Number($('spoolInitial').value),remaining=Number($('spoolRemaining').value),price=Number($('spoolPrice').value);if(remaining>initial){alert('Il peso residuo non può superare il peso netto iniziale.');return;}const selectedSupplier=$('spoolSupplier').value;const obj={id:id||uid(),code:$('spoolCode').value.trim(),productId:$('spoolProduct').value||'',material:$('spoolMaterial').value,brand:$('spoolBrand').value.trim(),color:$('spoolColor').value.trim(),diameter:$('spoolDiameter').value,initial,remaining,price,minimum:Number($('spoolMinimum').value)||0,supplier:selectedSupplier,location:$('spoolLocation').value||'deposito',date:$('spoolDate').value,notes:$('spoolNotes').value.trim()};if(!obj.code)obj.code=autoSpoolCode(obj);if(db.spools.some(s=>s.code.trim().toLowerCase()===obj.code.trim().toLowerCase()&&s.id!==id)){alert('Codice bobina già presente. Lascia vuoto il campo per generarne uno automatico.');return;}if(id)db.spools[db.spools.findIndex(s=>s.id===id)]=obj;else db.spools.push(obj);save();resetSpool();}); $('spoolCancel').onclick=resetSpool; $('spoolSearch').addEventListener('input',renderSpools); $('warehouseTable').addEventListener('click',e=>{const b=e.target.closest('[data-pick-spool]');if(!b)return;const sp=getSpool(b.dataset.pickSpool);if(!sp)return;sp.location='inuso';save();alert('Bobina '+sp.code+' prelevata: ora è disponibile per registrare i consumi.');}); $('spoolTable').addEventListener('click',e=>{const edit=e.target.closest('[data-edit-spool]'),del=e.target.closest('[data-del-spool]'),toggle=e.target.closest('[data-toggle-location]');if(toggle){const sp=getSpool(toggle.dataset.toggleLocation);if(sp){sp.location=sp.location==='deposito'?'inuso':'deposito';save();}return;}if(edit){const s=getSpool(edit.dataset.editSpool);if(!s)return;for(const [k,id] of Object.entries({id:'spoolId',code:'spoolCode',productId:'spoolProduct',material:'spoolMaterial',brand:'spoolBrand',color:'spoolColor',diameter:'spoolDiameter',initial:'spoolInitial',remaining:'spoolRemaining',price:'spoolPrice',minimum:'spoolMinimum',supplier:'spoolSupplier',location:'spoolLocation',date:'spoolDate',notes:'spoolNotes'}))$(id).value=s[k]??'';$('spoolFormTitle').textContent='Modifica bobina';window.scrollTo({top:0,behavior:'smooth'});}if(del){const id=del.dataset.delSpool;if(db.jobs.some(j=>j.spoolId===id)){alert('Questa bobina ha consumi registrati. Elimina prima i relativi consumi, oppure conserva la bobina nell’archivio.');return;}if(confirm('Eliminare questa bobina?')){db.spools=db.spools.filter(s=>s.id!==id);save();}}}); $('printerForm').addEventListener('submit',e=>{e.preventDefault();const id=$('printerId').value,obj={id:id||uid(),name:$('printerName').value.trim(),brand:$('printerBrand').value.trim(),model:$('printerModel').value.trim(),location:$('printerLocation').value.trim(),notes:$('printerNotes').value.trim()};if(id)db.printers[db.printers.findIndex(p=>p.id===id)]=obj;else db.printers.push(obj);save();resetPrinter();}); $('printerCancel').onclick=resetPrinter; $('printerTable').addEventListener('click',e=>{const edit=e.target.closest('[data-edit-printer]'),del=e.target.closest('[data-del-printer]');if(edit){const p=getPrinter(edit.dataset.editPrinter);if(!p)return;for(const [k,id] of Object.entries({id:'printerId',name:'printerName',brand:'printerBrand',model:'printerModel',location:'printerLocation',notes:'printerNotes'}))$(id).value=p[k]??'';$('printerFormTitle').textContent='Modifica stampante';window.scrollTo({top:0,behavior:'smooth'});}if(del){const id=del.dataset.delPrinter;if(db.jobs.some(j=>j.printerId===id)){alert('Questa stampante è associata a lavori registrati e non può essere eliminata.');return;}if(confirm('Eliminare questa stampante?')){db.printers=db.printers.filter(p=>p.id!==id);save();}}}); $('jobForm').addEventListener('submit',e=>{e.preventDefault();const s=getSpool($('jobSpool').value),grams=Number($('jobGrams').value);if(!s){alert('Seleziona una bobina valida.');return;}if(grams<=0){alert('Inserisci un consumo maggiore di zero.');return;} // Scarico automatico: prima la bobina scelta, poi le altre bobine in uso dello stesso materiale e colore. const same=(a,b)=>spoolGroupKey(a)===spoolGroupKey(b); const candidates=[s,...db.spools.filter(x=>x.id!==s.id&&x.location==='inuso'&&Number(x.remaining)>0&&same(x,s)).sort((a,b)=>a.code.localeCompare(b.code))]; const available=candidates.reduce((sum,x)=>sum+Math.max(0,Number(x.remaining)||0),0);if(grams>available){alert(`Materiale insufficiente: per ${s.material} ${s.color||''} servono ${num(grams)} g, disponibili complessivamente ${num(available)} g sulle bobine compatibili.`);return;} let left=grams;const allocations=[];for(const sp of candidates){if(left<=0)break;const take=Math.min(left,Math.max(0,Number(sp.remaining)||0));if(take>0){allocations.push({spoolId:sp.id,spoolCode:sp.code,grams:take,unitCost:costPerGram(sp)});sp.remaining=Number((Number(sp.remaining)-take).toFixed(2));left=Number((left-take).toFixed(2));}} const cost=allocations.reduce((sum,a)=>sum+a.grams*a.unitCost,0);const j={id:uid(),date:$('jobDate').value,name:$('jobName').value.trim(),printerId:$('jobPrinter').value,spoolId:s.id,spoolCode:allocations.map(a=>a.spoolCode).join(' + '),allocations,grams,quantity:Number($('jobQuantity').value)||1,hours:Number($('jobHours').value)||0,notes:$('jobNotes').value.trim(),cost,unitCost:grams?cost/grams:0};db.jobs.push(j); const finished=allocations.filter(a=>{const sp=getSpool(a.spoolId);return sp&&Number(sp.remaining)<=0;});if(finished.length){const ids=new Set(finished.map(a=>a.spoolId));db.spools.forEach(x=>{if(ids.has(x.id)){x.remaining=0;x.location='esaurita';}});}save();resetJob();if(finished.length)alert('Bobine terminate rimosse dal magazzino: '+finished.map(a=>a.spoolCode).join(', ')+'. Lo storico stampa resta conservato.');}); $('jobReset').onclick=resetJob; $('jobSpool').addEventListener('change',updateJobPreview);$('jobGrams').addEventListener('input',updateJobPreview); $('jobSearch').addEventListener('input',renderJobs); $('jobTable').addEventListener('click',e=>{const b=e.target.closest('[data-del-job]');if(!b)return;const j=db.jobs.find(x=>x.id===b.dataset.delJob);if(!j)return;if(confirm(`Eliminare il consumo "${j.name}" e ripristinare ${num(j.grams)} g alle bobine utilizzate?`)){if(Array.isArray(j.allocations)&&j.allocations.length){j.allocations.forEach(a=>{const sp=getSpool(a.spoolId);if(sp){sp.remaining=Number((Number(sp.remaining)+Number(a.grams)).toFixed(2));if(sp.remaining>0&&sp.location==='esaurita')sp.location='inuso';}});}else{const sp=getSpool(j.spoolId);if(sp){sp.remaining=Number((Number(sp.remaining)+Number(j.grams)).toFixed(2));if(sp.remaining>0&&sp.location==='esaurita')sp.location='inuso';}}db.jobs=db.jobs.filter(x=>x.id!==j.id);save();}}); $('exitBtn').onclick=()=>{if(confirm('Uscire dalla schermata corrente? I dati salvati resteranno nel browser.')){document.querySelectorAll('nav button').forEach(x=>x.classList.toggle('active',x.dataset.page==='dashboard'));document.querySelectorAll('.page').forEach(x=>x.classList.toggle('hidden',x.id!=='dashboard'));window.scrollTo({top:0,behavior:'smooth'});}}; $('printJobs').onclick=()=>printReport('Storico stampe e consumi',['Data','Lavoro','Stampante','Bobina','Consumo (g)','Pezzi','Costo'],[...db.jobs].sort((a,b)=>(b.date||'').localeCompare(a.date||'')).map(j=>[j.date||'',j.name||'',getPrinter(j.printerId)?.name||'—',j.spoolCode||getSpool(j.spoolId)?.code||'—',num(j.grams),Number(j.quantity)||1,euro(j.cost)])); const printWarehouseReport=()=>printReport('Magazzino filamenti',['Marca','Materiale','Colore','Diametro','Numero bobine','Disponibilità totale (g)','Codici interni'],spoolGroups(db.spools.filter(s=>s.location==='deposito')).map(g=>[g.brand,g.material,g.color,g.diameter+' mm',g.spools.length,num(g.remaining),g.spools.map(sp=>sp.code+' ('+num(sp.remaining)+' g)').join('; ')])); $('printWarehouse').onclick=printWarehouseReport; $('printWarehouseFromTab').onclick=printWarehouseReport; $('printInUse').onclick=()=>printReport('Bobine in uso',['Codice','Materiale','Marca','Colore','Residuo (g)','Scorta minima (g)'],db.spools.filter(s=>s.location==='inuso').sort((a,b)=>a.code.localeCompare(b.code)).map(s=>[s.code,s.material,s.brand||'',s.color||'',num(s.remaining),num(s.minimum)])); $('exportBtn').onclick=()=>{const blob=new Blob([JSON.stringify({...db,backupVersion:2,exportedAt:new Date().toISOString()},null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='backup_filamento_fdm_'+today()+'.json';a.click();URL.revokeObjectURL(a.href);}; $('importBtn').onclick=()=>$('importFile').click(); $('importFile').addEventListener('change',async e=>{const f=e.target.files[0];if(!f)return;try{const parsed=JSON.parse(await f.text());if(!validData(parsed))throw Error('Formato non valido');if(!confirm('Importare '+parsed.spools.length+' bobine, '+parsed.printers.length+' stampanti e '+parsed.jobs.length+' lavori dal backup?'))return;const data={spools:parsed.spools.map(sp=>({...sp,location:sp.location||'inuso'})),printers:parsed.printers,jobs:parsed.jobs,suppliers:parsed.suppliers,products:parsed.products};await window.fdmCloud.importBackup(data);alert('Operazione completata. Controlla lo stato di sincronizzazione.');}catch(err){alert('Importazione non completata: '+err.message);}finally{e.target.value='';}}); $('gcodeAnalyze').addEventListener('click',async()=>{ const file=$('gcodeFile').files[0];if(!file){alert('Seleziona prima un file G-code.');return;} try{ const content=await file.text(); const readList=label=>{const re=new RegExp('^;\\s*'+label+'\\s*=\\s*(.*)$','im');const m=content.match(re);return m?m[1].split(',').map(x=>Number(x.trim())).filter(x=>Number.isFinite(x)&&x>0):[];}; const grams=readList('filament used \\[g\\]'); const totalMatch=content.match(/^;\\s*total filament used \\[g\\]\\s*=\\s*([0-9.]+)/im); const timeMatch=content.match(/^;\\s*estimated printing time \\(normal mode\\)\\s*=\\s*(.+)$/im); const materialMatch=content.match(/^;\\s*filament_type\\s*=\\s*(.+)$/im); const paintMatch=content.match(/^;\\s*paint_info\\s*=\\s*(.+)$/im); let paints=[];if(paintMatch){try{paints=JSON.parse(paintMatch[1]).map(x=>({color:'#'+x.paint_color.map(v=>Number(v).toString(16).padStart(2,'0')).join(''),material:x.material_type||'Filamento'}));}catch(e){}} if(!grams.length){$('gcodePreview').innerHTML='
Non trovo la riga dei grammi per filamento nel file. Prova a esportare di nuovo il G-code dallo slicer.
';return;} const materials=materialMatch?materialMatch[1].split(';').map(x=>x.trim()):[]; const options=db.spools.filter(sp=>sp.location==='inuso'&&Number(sp.remaining)>0).map(sp=>``).join(''); const defaultName=file.name.replace(/\\.(gcode|gco|g)$/i,''); window.pendingGcode={fileName:file.name,grams,materials,paints,total:totalMatch?Number(totalMatch[1]):grams.reduce((a,b)=>a+b,0),time:timeMatch?timeMatch[1].trim():'',content}; $('gcodePreview').innerHTML=`
File analizzato: ${esc(file.name)}
Consumo dichiarato totale: ${num(window.pendingGcode.total)} g${window.pendingGcode.time?' · Tempo stimato: '+esc(window.pendingGcode.time):''}
Associa ogni riga alla bobina realmente usata. Il colore nel G-code è solo un indizio: verifica la bobina prima di confermare.

Associa i filamenti alle bobine

${grams.map((g,i)=>{const paint=paints[i];const mat=materials[i]||paint?.material||'Non specificato';const color=paint?paint.color:'';return `
Filamento ${i+1}${color?``:''}${num(g)} g${esc(mat)}
Costo stimato dalla bobina selezionata: — · Residuo dopo importazione: —
`;}).join('')}
`; const update=()=>document.querySelectorAll('.gcode-spool-select').forEach(sel=>{const i=Number(sel.dataset.index),sp=getSpool(sel.value),g=grams[i];$('gcodeCost'+i).textContent=sp?euro(g*costPerGram(sp)):'—';$('gcodeRemain'+i).textContent=sp?num(Number(sp.remaining)-g)+' g':'—';$('gcodeRemain'+i).style.color=sp&&g>sp.remaining?'var(--red)':'inherit';}); $('gcodePreview').querySelectorAll('.gcode-spool-select').forEach(sel=>sel.addEventListener('change',update)); $('gcodeCancel').onclick=()=>{$('gcodePreview').innerHTML='';window.pendingGcode=null;}; $('gcodeConfirm').onclick=()=>{const selects=[...document.querySelectorAll('.gcode-spool-select')];const chosen=selects.map(x=>getSpool(x.value));if(chosen.some((x,i)=>!x)){alert('Seleziona una bobina per ogni filamento.');return;}const planned=new Map(db.spools.map(sp=>[sp.id,Number(sp.remaining)||0]));const rows=[];for(let i=0;ispoolGroupKey(a)===spoolGroupKey(b);const candidates=[first,...db.spools.filter(x=>x.id!==first.id&&x.location==='inuso'&&same(x,first)).sort((a,b)=>a.code.localeCompare(b.code))];let left=g;const allocations=[];for(const sp of candidates){const available=Math.max(0,planned.get(sp.id)||0);const take=Math.min(left,available);if(take>0){allocations.push({spoolId:sp.id,spoolCode:sp.code,grams:take,unitCost:costPerGram(sp)});planned.set(sp.id,Number((available-take).toFixed(2)));left=Number((left-take).toFixed(2));}if(left<=0)break;}if(left>0){alert(`Materiale insufficiente per ${first.material} ${first.color||''}: mancano ${num(left)} g anche considerando le altre bobine compatibili.`);return;}rows.push({first,g,allocations});}const name=$('gImportName').value.trim()||defaultName,date=$('gImportDate').value||today(),printerId=$('gImportPrinter').value;rows.forEach((r,i)=>{const cost=r.allocations.reduce((sum,a)=>sum+a.grams*a.unitCost,0);db.jobs.push({id:uid(),date,name,printerId,spoolId:r.first.id,spoolCode:r.allocations.map(a=>a.spoolCode).join(' + '),allocations:r.allocations,grams:r.g,quantity:1,hours:0,notes:`Importato da G-code: ${file.name}; filamento ${i+1}/${grams.length}`,cost,unitCost:r.g?cost/r.g:0,source:'gcode'});r.allocations.forEach(a=>{const sp=getSpool(a.spoolId);if(sp)sp.remaining=Number((Number(sp.remaining)-a.grams).toFixed(2));});});const finished=db.spools.filter(sp=>Number(sp.remaining)<=0&&sp.location==='inuso');finished.forEach(sp=>sp.location='esaurita');save();if(finished.length)alert('Bobine terminate rimosse dal magazzino: '+finished.map(sp=>sp.code).join(', ')+'. Lo storico stampa resta conservato.');$('gcodePreview').innerHTML='
Importazione completata. I consumi sono stati registrati e le giacenze aggiornate.
'; $('gcodeFile').value='';window.pendingGcode=null;}; }catch(err){$('gcodePreview').innerHTML='
Errore durante la lettura del file: '+esc(err.message)+'
';} }); $('spoolProduct').addEventListener('change',()=>{const p=getProduct($('spoolProduct').value);if(!p)return;$('spoolMaterial').value=p.material;$('spoolBrand').value=p.brand||'';$('spoolColor').value=p.color||'';$('spoolDiameter').value=p.diameter||'1.75';if(p.supplierId)$('spoolSupplier').value=p.supplierId;}); $('supplierForm').addEventListener('submit',e=>{e.preventDefault();const name=$('supplierName').value.trim();if(db.suppliers.some(s=>s.name.toLowerCase()===name.toLowerCase())){alert('Questo fornitore è già presente.');return;}db.suppliers.push({id:uid(),name,website:$('supplierWebsite').value.trim(),notes:$('supplierNotes').value.trim()});$('supplierForm').reset();save();}); $('supplierTable').addEventListener('click',e=>{const b=e.target.closest('[data-del-supplier]');if(!b)return;const id=b.dataset.delSupplier;if(db.products.some(p=>p.supplierId===id)){alert('Questo fornitore è associato a prodotti. Modifica prima i prodotti collegati.');return;}if(confirm('Eliminare questo fornitore?')){db.suppliers=db.suppliers.filter(s=>s.id!==id);save();}}); $('productCancel').onclick=()=>{$('productForm').reset();$('productId').value='';}; $('productForm').addEventListener('submit',e=>{e.preventDefault();const id=$('productId').value,obj={id:id||uid(),name:$('productName').value.trim(),material:$('productMaterial').value,brand:$('productBrand').value.trim(),color:$('productColor').value.trim(),diameter:$('productDiameter').value,supplierId:$('productSupplier').value};if(id)db.products[db.products.findIndex(p=>p.id===id)]=obj;else db.products.push(obj);$('productForm').reset();$('productId').value='';save();}); $('productTable').addEventListener('click',e=>{const edit=e.target.closest('[data-edit-product]'),del=e.target.closest('[data-del-product]');if(edit){const p=getProduct(edit.dataset.editProduct);if(!p)return;for(const [k,id] of Object.entries({id:'productId',name:'productName',material:'productMaterial',brand:'productBrand',color:'productColor',diameter:'productDiameter',supplierId:'productSupplier'}))$(id).value=p[k]??'';window.scrollTo({top:document.getElementById('catalogs').offsetTop,behavior:'smooth'});}if(del){const id=del.dataset.delProduct;if(db.spools.some(s=>s.productId===id)){alert('Questo prodotto è collegato a una o più bobine e non può essere eliminato.');return;}if(confirm('Eliminare questo prodotto?')){db.products=db.products.filter(p=>p.id!==id);save();}}}); $('jobDate').value=today();renderAll(); })();