import { useState, useEffect, useRef, useCallback } from “react”; const OPERAI = [ { id: “luca”, nome: “Luca”, cognome: “Zani”, alias: “Luca” }, { id: “gianpaolo”, nome: “Gianpaolo”, cognome: “Zani”, alias: “James” }, { id: “ezio”, nome: “Ezio”, cognome: “Zani”, alias: “papà” }, { id: “demis”, nome: “Demis”, cognome: “”, alias: “Demis” }, { id: “christian”, nome: “Christian”, cognome: “”, alias: “Christian” }, ]; const PALETTE = [“#3498db”,”#2ecc71″,”#e74c3c”,”#f39c12″,”#9b59b6″,”#1abc9c”,”#e67e22″,”#e91e63″,”#00bcd4″,”#8bc34a”,”#ff5722″,”#607d8b”]; const PALETTE_NAMES = { “#3498db”:”blu”,”#2ecc71″:”verde”,”#e74c3c”:”rosso”,”#f39c12″:”arancione”,”#9b59b6″:”viola”,”#1abc9c”:”turchese”,”#e67e22″:”arancio scuro”,”#e91e63″:”rosa”,”#00bcd4″:”azzurro”,”#8bc34a”:”verde chiaro”,”#ff5722″:”arancio vivo”,”#607d8b”:”grigio” }; const COLOR_MAP = { blu:”#3498db”, verde:”#2ecc71″, rosso:”#e74c3c”, arancione:”#f39c12″, viola:”#9b59b6″, turchese:”#1abc9c”, grigio:”#607d8b”, rosa:”#e91e63″, azzurro:”#00bcd4″ }; const DEFAULT_CANTIERI = [ { id:”c1″, nome:”Via Roma 14″, colore:”#3498db”, stato:”aperto”, note:”” }, { id:”c2″, nome:”Capannone Logistico”, colore:”#2ecc71″, stato:”aperto”, note:”” }, ]; const hoje = new Date(); const Y0 = hoje.getFullYear(), M0 = hoje.getMonth(); const MESI = [“Gennaio”,”Febbraio”,”Marzo”,”Aprile”,”Maggio”,”Giugno”,”Luglio”,”Agosto”,”Settembre”,”Ottobre”,”Novembre”,”Dicembre”]; const GIORNI_SHORT = [“Dom”,”Lun”,”Mar”,”Mer”,”Gio”,”Ven”,”Sab”]; function uid() { return “c” + Date.now() + Math.random().toString(36).slice(2,6); } function daysInMonth(y,m){ return new Date(y,m+1,0).getDate(); } function isoKey(y,m,d){ return `${y}-${String(m+1).padStart(2,”0″)}-${String(d).padStart(2,”0″)}`; } function weekOfMonth(y,m,d){ const first = new Date(y,m,1).getDay(); return Math.floor((d – 1 + first) / 7); } function isWeekend(y,m,d){ const dow = new Date(y,m,d).getDay(); return dow===0||dow===6; } async function storageGet(key){ try{ const r=await window.storage.get(key); return r?JSON.parse(r.value):null; }catch{ return null; } } async function storageSet(key,val){ try{ await window.storage.set(key,JSON.stringify(val)); }catch{} } const css = ` @import url(‘https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Sans:wght@300;400;500;600&display=swap’); *{box-sizing:border-box;margin:0;padding:0} :root{–dark:#1a1815;–paper:#f4f1eb;–ink:#111010;–gold:#b8922a;–muted:#7a766e;–gold-dim:#7a5a1a} body{font-family:’DM Sans’,sans-serif;background:var(–dark);color:var(–paper);height:100vh;overflow:hidden} .app{display:flex;flex-direction:column;height:100vh} .hdr{background:var(–dark);border-bottom:1px solid var(–gold-dim);padding:0 20px;display:flex;align-items:center;gap:16px;height:52px;flex-shrink:0} .hdr-logo{font-family:’Bebas Neue’,sans-serif;font-size:22px;color:var(–gold);letter-spacing:2px} .hdr-sub{font-size:12px;color:var(–muted);letter-spacing:1px} .body{display:grid;grid-template-columns:1fr 340px;flex:1;min-height:0} .main{background:var(–paper);color:var(–ink);display:flex;flex-direction:column;overflow:hidden} .main-scroll{flex:1;overflow-y:auto;padding:16px 20px 20px} .main-scroll::-webkit-scrollbar{width:4px} .main-scroll::-webkit-scrollbar-thumb{background:var(–gold);border-radius:2px} .nav-bar{display:flex;align-items:center;gap:10px;margin-bottom:14px;flex-wrap:wrap} .btn-nav{background:none;border:1px solid #ccc;color:var(–ink);padding:4px 10px;border-radius:4px;cursor:pointer;font-family:’DM Sans’,sans-serif;font-size:13px} .btn-nav:hover{border-color:var(–gold);color:var(–gold)} .month-label{font-family:’Bebas Neue’,sans-serif;font-size:20px;color:var(–ink);letter-spacing:1px;min-width:160px;text-align:center} .tabs{display:flex;gap:0;border-bottom:1px solid #ddd;margin-bottom:0} .tab{padding:8px 14px;font-size:13px;font-family:’DM Sans’,sans-serif;cursor:pointer;border:none;background:none;color:var(–muted);border-bottom:2px solid transparent;transition:all .2s} .tab.active{color:var(–gold);border-bottom-color:var(–gold);font-weight:600} .tab:hover:not(.active){color:var(–ink)} .tab-views{display:flex;border-bottom:2px solid #ddd;margin-bottom:12px;gap:2px} .tab-view-btn{padding:6px 14px;font-size:12px;font-family:’DM Sans’,sans-serif;cursor:pointer;border:none;background:none;color:var(–muted);border-bottom:2px solid transparent;margin-bottom:-2px;transition:all .2s} .tab-view-btn.active{color:var(–gold);border-bottom-color:var(–gold);font-weight:600} .grid-header{display:grid;grid-template-columns:36px 80px 1fr 90px;gap:4px;font-size:11px;font-weight:600;color:var(–muted);padding:4px 0;border-bottom:1px solid #ddd;margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px} .day-row{display:grid;grid-template-columns:36px 80px 1fr 90px;gap:4px;align-items:center;padding:2px 0;border-radius:4px;transition:background .15s} .day-row:hover{background:rgba(184,146,42,.08)} .day-row.today{background:rgba(184,146,42,.15);border-left:3px solid var(–gold);padding-left:4px} .day-row.weekend{opacity:.55} .day-row.weekend .day-num{color:var(–muted)} .day-num{font-size:12px;font-weight:600;color:var(–ink);text-align:center} .day-name{font-size:11px;color:var(–muted)} .day-inp{height:26px;border:1px solid #d0ccbe;border-radius:3px;padding:0 6px;font-size:13px;font-family:’DM Sans’,sans-serif;background:#fff;color:var(–ink);width:52px} .day-inp:focus{outline:none;border-color:var(–gold)} .day-inp.filled{background:#fffdf4;border-color:var(–gold-dim)} .day-select{height:26px;border:1px solid #d0ccbe;border-radius:3px;padding:0 4px;font-size:11px;font-family:’DM Sans’,sans-serif;background:#fff;color:var(–ink);flex:1} .day-select:focus{outline:none;border-color:var(–gold)} .day-note{height:26px;border:1px solid #d0ccbe;border-radius:3px;padding:0 5px;font-size:11px;font-family:’DM Sans’,sans-serif;background:#fff;color:var(–ink);width:100%} .day-note:focus{outline:none;border-color:var(–gold)} .day-input-group{display:flex;gap:4px;align-items:center} .week-sep{background:#e8e4da;height:1px;margin:4px 0;grid-column:1/-1} .week-total{font-size:11px;font-weight:600;color:var(–gold);text-align:right;padding:2px 0 6px;border-bottom:1px solid #ddd;margin-bottom:4px} .summary{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-top:16px;padding-top:16px;border-top:2px solid var(–gold)} .sum-card{background:#fff;border:1px solid #ddd;border-radius:6px;padding:10px 14px;text-align:center} .sum-label{font-size:11px;color:var(–muted);letter-spacing:.5px;text-transform:uppercase;margin-bottom:4px} .sum-val{font-family:’Bebas Neue’,sans-serif;font-size:28px;color:var(–gold);letter-spacing:1px} .cantieri-section{padding:4px 0} .cant-list{display:flex;flex-direction:column;gap:8px;margin-top:12px} .cant-card{background:#fff;border:1px solid #ddd;border-radius:6px;padding:10px 14px;display:flex;align-items:center;gap:10px;position:relative} .cant-card.closed{opacity:.5} .cant-dot{width:14px;height:14px;border-radius:50%;flex-shrink:0} .cant-name{font-weight:600;font-size:14px;flex:1} .cant-info{font-size:11px;color:var(–muted);margin-top:2px} .cant-hrs{font-family:’Bebas Neue’,sans-serif;font-size:20px;color:var(–gold);margin-left:auto;margin-right:8px} .cant-actions{display:flex;gap:4px} .btn-icon{background:none;border:1px solid #ddd;border-radius:4px;padding:4px 7px;cursor:pointer;font-size:12px;color:var(–muted)} .btn-icon:hover{border-color:var(–gold);color:var(–gold)} .btn-add{background:var(–gold);color:#fff;border:none;border-radius:5px;padding:7px 16px;font-family:’DM Sans’,sans-serif;font-size:13px;font-weight:600;cursor:pointer;margin-top:12px} .btn-add:hover{background:#a07a20} .palette-row{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px} .pal-dot{width:20px;height:20px;border-radius:50%;cursor:pointer;border:2px solid transparent;transition:transform .15s} .pal-dot.selected{border-color:#333;transform:scale(1.2)} .modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:100} .modal{background:var(–paper);color:var(–ink);border-radius:8px;padding:20px 24px;min-width:300px;max-width:400px;width:90%} .modal h3{font-family:’Bebas Neue’,sans-serif;font-size:20px;margin-bottom:14px;color:var(–gold)} .modal label{font-size:12px;color:var(–muted);display:block;margin-bottom:3px;margin-top:10px} .modal input,.modal textarea{width:100%;border:1px solid #ccc;border-radius:4px;padding:6px 8px;font-family:’DM Sans’,sans-serif;font-size:13px;background:#fff;color:var(–ink)} .modal input:focus,.modal textarea:focus{outline:none;border-color:var(–gold)} .modal-btns{display:flex;justify-content:flex-end;gap:8px;margin-top:16px} .btn-cancel{background:none;border:1px solid #ccc;color:var(–muted);border-radius:4px;padding:6px 14px;cursor:pointer;font-family:’DM Sans’,sans-serif;font-size:13px} .btn-confirm{background:var(–gold);color:#fff;border:none;border-radius:4px;padding:6px 14px;cursor:pointer;font-family:’DM Sans’,sans-serif;font-size:13px;font-weight:600} .ai-panel{background:#0f0d0b;display:flex;flex-direction:column;border-left:1px solid var(–gold-dim)} .ai-hdr{padding:12px 16px;border-bottom:1px solid #2a2520;display:flex;align-items:center;gap:8px;flex-shrink:0} .ai-hdr-title{font-family:’Bebas Neue’,sans-serif;font-size:16px;color:var(–gold);letter-spacing:1px} .ai-msgs{flex:1;overflow-y:auto;padding:12px 14px;display:flex;flex-direction:column;gap:10px} .ai-msgs::-webkit-scrollbar{width:3px} .ai-msgs::-webkit-scrollbar-thumb{background:var(–gold-dim);border-radius:2px} .msg{padding:8px 11px;border-radius:6px;font-size:13px;line-height:1.5;max-width:92%} .msg.user{background:#2a2520;color:#e0d8c8;align-self:flex-end} .msg.ai{background:#1a1612;color:#c8c0b0;border:1px solid #2a2520;align-self:flex-start} .msg.system{background:transparent;color:var(–muted);font-size:12px;font-style:italic;align-self:center;text-align:center} .msg.confirm{background:#1e1a10;border:1px solid var(–gold-dim);color:#e0d8c8;align-self:flex-start;width:92%} .confirm-actions{display:flex;gap:6px;margin-top:8px} .btn-yes{background:var(–gold);color:#fff;border:none;border-radius:4px;padding:4px 12px;cursor:pointer;font-size:12px;font-family:’DM Sans’,sans-serif} .btn-no{background:none;border:1px solid #555;color:var(–muted);border-radius:4px;padding:4px 12px;cursor:pointer;font-size:12px;font-family:’DM Sans’,sans-serif} .ai-chips{display:flex;flex-wrap:wrap;gap:6px;padding:0 14px 10px} .chip{background:#1a1612;border:1px solid #3a3530;color:#a09080;border-radius:12px;padding:4px 10px;font-size:11px;cursor:pointer;font-family:’DM Sans’,sans-serif;transition:all .2s} .chip:hover{border-color:var(–gold);color:var(–gold)} .ai-input-row{display:flex;gap:6px;padding:10px 14px;border-top:1px solid #2a2520;flex-shrink:0;align-items:flex-end} .ai-inp{flex:1;background:#1a1612;border:1px solid #3a3530;border-radius:5px;color:#e0d8c8;padding:7px 10px;font-family:’DM Sans’,sans-serif;font-size:13px;resize:none;height:38px;max-height:100px;overflow-y:auto} .ai-inp:focus{outline:none;border-color:var(–gold)} .btn-mic{background:none;border:1px solid #3a3530;border-radius:5px;padding:0 10px;height:38px;color:var(–muted);cursor:pointer;font-size:16px;flex-shrink:0;transition:all .2s} .btn-mic:hover{border-color:var(–gold);color:var(–gold)} .btn-mic.listening{border-color:#e74c3c;color:#e74c3c;animation:pulse 1s infinite} .btn-send{background:var(–gold);border:none;border-radius:5px;padding:0 12px;height:38px;color:#fff;cursor:pointer;font-size:16px;flex-shrink:0} .btn-send:hover{background:#a07a20} .btn-send:disabled{background:#3a3530;cursor:default} .loading-dots{display:inline-flex;gap:3px;align-items:center} .loading-dots span{width:5px;height:5px;background:var(–muted);border-radius:50%;animation:dot .8s infinite} .loading-dots span:nth-child(2){animation-delay:.2s} .loading-dots span:nth-child(3){animation-delay:.4s} @keyframes dot{0%,80%,100%{transform:scale(.8);opacity:.4}40%{transform:scale(1.1);opacity:1}} @keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}} .status-dot{width:7px;height:7px;border-radius:50%;background:var(–gold);margin-left:auto} .cant-badge{display:inline-block;padding:1px 7px;border-radius:10px;font-size:11px;font-weight:600;color:#fff;margin-right:4px} .note-col{font-size:11px;color:var(–muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:80px} `; export default function App() { const [ore, setOre] = useState({}); const [cantieri, setCantieri] = useState(DEFAULT_CANTIERI); const [mese, setMese] = useState(M0); const [anno, setAnno] = useState(Y0); const [operaioIdx, setOperaioIdx] = useState(0); const [view, setView] = useState(“timesheet”); const [msgs, setMsgs] = useState([]); const [aiInput, setAiInput] = useState(“”); const [aiLoading, setAiLoading] = useState(false); const [listening, setListening] = useState(false); const [modal, setModal] = useState(null); const [pendingActions, setPendingActions] = useState(null); const [chipsShown, setChipsShown] = useState(true); const msgsEndRef = useRef(null); const recognitionRef = useRef(null); const oreRef = useRef(ore); const cantieriRef = useRef(cantieri); oreRef.current = ore; cantieriRef.current = cantieri; useEffect(() => { (async () => { const savedOre = await storageGet(“tc_ore”); const savedCant = await storageGet(“tc_cantieri”); if (savedOre) setOre(savedOre); if (savedCant) setCantieri(savedCant); setMsgs([{ role:”ai”, text:”Ciao! Sono il tuo assistente per TerraCielo. Puoi dirmi cosa inserire in linguaggio naturale.” }]); })(); }, []); useEffect(() => { storageSet(“tc_ore”, ore); }, [ore]); useEffect(() => { storageSet(“tc_cantieri”, cantieri); }, [cantieri]); useEffect(() => { msgsEndRef.current?.scrollIntoView({behavior:”smooth”}); }, [msgs]); const getOre = (opId, key) => ore[opId]?.[key] || {}; const setGiorno = (opId, key, field, val) => { setOre(prev => ({ …prev, [opId]: { …(prev[opId]||{}), [key]: { …(prev[opId]?.[key]||{}), [field]: val } } })); }; const days = daysInMonth(anno, mese); const opId = OPERAI[operaioIdx].id; const cantieriAperti = cantieri.filter(c => c.stato === “aperto”); const weeks = []; for(let d=1;d<=days;d++){ const w = weekOfMonth(anno,mese,d); if(!weeks[w]) weeks[w]=[]; weeks[w].push(d); } const totaleOreOperaio = () => { let tot=0; for(let d=1;d<=days;d++){ const k=isoKey(anno,mese,d); const h=parseFloat(getOre(opId,k).ore)||0; tot+=h; } return tot; }; const mediaGiornaliera = () => { let tot=0,cnt=0; for(let d=1;d<=days;d++){ const k=isoKey(anno,mese,d); const h=parseFloat(getOre(opId,k).ore)||0; if(h>0){tot+=h;cnt++;} } return cnt>0?(tot/cnt).toFixed(1):”—”; }; const orePerCantiere = (cId) => { let tot=0; OPERAI.forEach(op=>{ for(let d=1;d<=days;d++){ const k=isoKey(anno,mese,d); const row=getOre(op.id,k); if(row.cantiere===cId) tot+=parseFloat(row.ore)||0; } }); return tot; }; const weekTotal = (wDays) => { return wDays.reduce((s,d)=>{ const k=isoKey(anno,mese,d); return s+(parseFloat(getOre(opId,k).ore)||0); },0); }; const navMese = (dir) => { let nm=mese+dir, na=anno; if(nm<0){nm=11;na--;} if(nm>11){nm=0;na++;} setMese(nm); setAnno(na); }; const buildSystemPrompt = () => { const oreSerial = {}; OPERAI.forEach(op=>{ oreSerial[op.id]={}; for(let d=1;d<=days;d++){ const k=isoKey(anno,mese,d); const r=getOre(op.id,k); if(r.ore||r.cantiere||r.nota) oreSerial[op.id][k]=r; } }); return `Sei un assistente per la gestione delle ore degli operai di TerraCielo Costruzioni Srl (Brescia). Data odierna: ${hoje.toLocaleDateString('it-IT')}. Mese visualizzato: ${MESI[mese]} ${anno}. Operai: ${OPERAI.map(o=>`${o.id} (${o.nome} ${o.cognome}${o.alias&&o.alias!==o.nome?’, alias ‘+o.alias:”})`).join(‘, ‘)}. Cantieri: ${cantieriRef.current.map(c=>`id=${c.id} nome=”${c.nome}” colore=${c.colore} stato=${c.stato}`).join(‘ | ‘)}. Mappa colori IT→HEX: ${Object.entries(COLOR_MAP).map(([k,v])=>`${k}=${v}`).join(‘, ‘)}. Ore inserite: ${JSON.stringify(oreSerial)}. Rispondi SEMPRE e SOLO con un JSON valido, senza markdown, senza testo fuori dal JSON. Formato risposta: {“azioni”:[…],”messaggio”:”testo breve in italiano”} Azioni possibili: – {“tipo”:”insert”,”operaio”:”id”,”data”:”YYYY-MM-DD”,”ore”:8,”cantiere”:”id_cantiere”,”nota”:”testo”} – {“tipo”:”add_cantiere”,”nome”:”nome”,”colore”:”#hex”} – {“tipo”:”delete_cantiere”,”id”:”id”} – {“tipo”:”rename_cantiere”,”id”:”id”,”nuovo_nome”:”nome”} – {“tipo”:”close_cantiere”,”id”:”id”} – {“tipo”:”reopen_cantiere”,”id”:”id”} – {“tipo”:”info”} (solo messaggio, nessuna azione) – {“tipo”:”clarify”} (chiedi chiarimento) Se l’utente inserisce ore senza specificare il cantiere, usa il primo cantiere aperto disponibile. Se dice “domani”, “ieri”, calcola la data rispetto ad oggi. Se dice un colore in italiano, usa la mappa colori.`; }; const applyActions = (actions) => { const newOre = {…oreRef.current}; const newCant = […cantieriRef.current]; actions.forEach(a => { if(a.tipo===”insert”){ if(!newOre[a.operaio]) newOre[a.operaio]={}; newOre[a.operaio][a.data]={ ore:String(a.ore), cantiere:a.cantiere||””, nota:a.nota||”” }; } else if(a.tipo===”add_cantiere”){ newCant.push({id:uid(),nome:a.nome,colore:a.colore||PALETTE[0],stato:”aperto”,note:””}); } else if(a.tipo===”delete_cantiere”){ const idx=newCant.findIndex(c=>c.id===a.id); if(idx>=0) newCant.splice(idx,1); } else if(a.tipo===”rename_cantiere”){ const c=newCant.find(c=>c.id===a.id); if(c) c.nome=a.nuovo_nome; } else if(a.tipo===”close_cantiere”){ const c=newCant.find(c=>c.id===a.id); if(c) c.stato=”chiuso”; } else if(a.tipo===”reopen_cantiere”){ const c=newCant.find(c=>c.id===a.id); if(c) c.stato=”aperto”; } }); setOre(newOre); setCantieri(newCant); }; const sendToAI = async (text) => { if(!text.trim()||aiLoading) return; setChipsShown(false); const userMsg = {role:”user”,text}; setMsgs(prev=>[…prev,userMsg]); setAiInput(“”); setAiLoading(true); setMsgs(prev=>[…prev,{role:”loading”}]); try { const res = await fetch(“https://api.anthropic.com/v1/messages”,{ method:”POST”, headers:{“Content-Type”:”application/json”,”anthropic-dangerous-direct-browser-access”:”true”}, body:JSON.stringify({ model:”claude-haiku-4-5-20251001″, max_tokens:1000, system:buildSystemPrompt(), messages:[{role:”user”,content:text}] }) }); const data = await res.json(); const raw = data.content?.[0]?.text||”{}”; let parsed; try{ parsed=JSON.parse(raw.replace(/“`json|“`/g,””).trim()); } catch{ parsed={azioni:[],messaggio:”Risposta non valida dall’AI.”}; } const azioni = (parsed.azioni||[]).filter(a=>a.tipo!==”info”&&a.tipo!==”clarify”); setMsgs(prev=>prev.filter(m=>m.role!==”loading”)); if(azioni.length>0){ setMsgs(prev=>[…prev,{role:”confirm”,text:parsed.messaggio,azioni}]); setPendingActions(azioni); } else { setMsgs(prev=>[…prev,{role:”ai”,text:parsed.messaggio||”Fatto!”}]); } } catch(e){ setMsgs(prev=>prev.filter(m=>m.role!==”loading”).concat({role:”ai”,text:”Errore di connessione con l’AI.”})); } setAiLoading(false); }; const confirmActions = (yes) => { if(yes && pendingActions){ applyActions(pendingActions); setMsgs(prev=>prev.map(m=>m.role===”confirm”?{…m,confirmed:true}:m).concat({role:”system”,text:”✓ Modifiche applicate”})); } else { setMsgs(prev=>prev.map(m=>m.role===”confirm”?{…m,confirmed:true}:m).concat({role:”system”,text:”Annullato.”})); } setPendingActions(null); }; const toggleMic = () => { if(!(“webkitSpeechRecognition” in window || “SpeechRecognition” in window)){ alert(“Speech API non supportata in questo browser.”); return; } if(listening){ recognitionRef.current?.stop(); setListening(false); return; } const SR = window.SpeechRecognition||window.webkitSpeechRecognition; const r = new SR(); r.lang=”it-IT”; r.interimResults=false; r.maxAlternatives=1; r.onresult=(e)=>{ setAiInput(e.results[0][0].transcript); }; r.onend=()=>setListening(false); r.start(); recognitionRef.current=r; setListening(true); }; const openModal = (type, data={}) => setModal({type,…data}); const closeModal = () => setModal(null); const saveCantiere = () => { const {type,id,nome,colore,note} = modal; if(type===”new”){ setCantieri(prev=>[…prev,{id:uid(),nome,colore:colore||PALETTE[0],stato:”aperto”,note:note||””}]); } else if(type===”edit”){ setCantieri(prev=>prev.map(c=>c.id===id?{…c,nome,colore,note:note||””}:c)); } closeModal(); }; const CHIPS = [ “Inserisci 8h a Luca oggi in Via Roma 14”, “Gianpaolo ha lavorato ieri 7h al Capannone”, “Aggiungi cantiere ‘Residenza Parco’ in viola”, “Mostrami il totale ore di questo mese”, “Chiudi il cantiere Capannone Logistico”, ]; return ( <>
TerraCielo Costruzioni Srl · Brescia
Gestionale Ore
{view===”timesheet” && ( <>
{MESI[mese]} {anno}
{OPERAI.map((op,i)=>( ))}
#
Giorno
Ore · Cantiere · Nota
Tot.sett
{weeks.map((wDays,wi)=>(
{wDays.map(d=>{ const k=isoKey(anno,mese,d); const row=getOre(opId,k); const isToday=(d===hoje.getDate()&&mese===M0&&anno===Y0); const dow=new Date(anno,mese,d).getDay(); const dName=GIORNI_SHORT[dow]; return (
{d}
{dName} setGiorno(opId,k,”ore”,e.target.value)}/>
setGiorno(opId,k,”nota”,e.target.value)} style={{width:80}}/>
{wDays[wDays.length-1]===d?(
Sett {wi+1}: {weekTotal(wDays)}h
):
}
); })} {wi}
))}
Totale mese
{totaleOreOperaio()}h
Media/giorno
{mediaGiornaliera()}h
Giorni lavorati
{Array.from({length:days},(_,i)=>i+1).filter(d=>{const k=isoKey(anno,mese,d);return parseFloat(getOre(opId,k).ore)>0;}).length}
)} {view===”cantieri” && (
Cantieri — {MESI[mese]} {anno}
{cantieri.map(c=>(
{c.nome}
{c.stato===”chiuso”?”CHIUSO · “:””}{c.note||”Nessuna nota”}
{orePerCantiere(c.id)}h
))}
)}
🤖 Assistente AI {aiLoading&&
}
{msgs.map((m,i)=>(
{m.role===”loading”?
:m.text} {m.role===”confirm”&&!m.confirmed&&(
)}
))}
{chipsShown&&(
{CHIPS.map((c,i)=>( ))}
)}