/* global React, ReactDOM, MATERIALS, PRINTERS, FINISHING_PRESETS, PLATFORM_PRESETS, CD3D, fmtBRL, fmtNum, fmtPct, cdStorage, CD3D_Steps, CD3D_UI */ const useS = React.useState, useE = React.useEffect, useM = React.useMemo, useC = React.useCallback, useR = React.useRef; const APP_STEPS = window.CD3D_Steps.STEPS; const APP_StepMaterial = window.CD3D_Steps.StepMaterial; const APP_StepPrint = window.CD3D_Steps.StepPrint; const APP_StepLabor = window.CD3D_Steps.StepLabor; const APP_StepPricing = window.CD3D_Steps.StepPricing; const APP_calculate = window.CD3D.calculate; const APP_DEFAULT_STATE = window.CD3D.DEFAULT_STATE; const APP_STORAGE_KEYS = window.CD3D.STORAGE_KEYS; // ====================== Receipt (right column) ====================== function Receipt({ s, calc }) { const totalCosts = calc.filamentCost + calc.energyCost + calc.wearCost + calc.laborCost + calc.packagingCost; const pct = (v) => totalCosts > 0 ? (v / totalCosts) * 100 : 0; const segs = [ { label: "Filamento", v: calc.filamentCost, cls: "seg-fil", color: "var(--accent)" }, { label: "Energia", v: calc.energyCost, cls: "seg-eng", color: "var(--cd-blue-500)" }, { label: "Desgaste", v: calc.wearCost, cls: "seg-wer", color: "var(--cd-yellow-warm)" }, { label: "Mão de obra", v: calc.laborCost, cls: "seg-lab", color: "var(--success)" }, { label: "Embalagem", v: calc.packagingCost, cls: "seg-pak", color: "var(--text-muted)" } ]; return (

Orçamento {s.quoteNumber && `Nº ${s.quoteNumber}`}

{new Date().toLocaleDateString("pt-BR")}
{segs.map(seg => )}
{segs.map(seg => ( {seg.label} ))}
{calc.platformFeeValue > 0 && ( )} {calc.taxValue > 0 && ( )} {calc.shippingIncluded && calc.shipping > 0 && ( )}
Item Valor (un.)
Filamento
{fmtNum(calc.effectiveWeight, 1)}g (c/ refugo)
{fmtBRL.format(calc.filamentCost)}
Energia
{fmtNum(calc.machineHoursPerPiece, 2)}h × {s.printerWatts}W
{fmtBRL.format(calc.energyCost)}
Depreciação
desgaste da máquina
{fmtBRL.format(calc.wearCost)}
Mão de obra
acabamento + design
{fmtBRL.format(calc.laborCost)}
Embalagem {fmtBRL.format(calc.packagingCost)}
Custo de produção {fmtBRL.format(calc.productionCost)}
+ Lucro ({fmtPct(s.profitMargin)}) {fmtBRL.format(calc.profit)}
Taxa plataforma
{s.platformFee}% (gross-up)
{fmtBRL.format(calc.platformFeeValue)}
Impostos
{s.taxRate}%
{fmtBRL.format(calc.taxValue)}
Frete
embutido
{fmtBRL.format(calc.shipping)}
Preço final
por peça
{fmtBRL.format(calc.finalUnitPrice)}
{calc.qty > 1 && (
Total ({calc.qty} pç) {fmtBRL.format(calc.totalRevenue)}
Lucro total {fmtBRL.format(calc.totalProfit)}
)}
Hora-máquina {fmtBRL.format(calc.hourlyMachineRate)}/h
Custo total ({calc.qty}) {fmtBRL.format(calc.totalCost)}
); } // ====================== History modal ====================== function HistoryModal({ open, onClose, items, onLoad, onDelete, onClear }) { if (!open) return null; return (
e.stopPropagation()}>

Histórico de Orçamentos

{items.length > 0 && }
{items.length === 0 &&
Nenhum orçamento salvo. Calcule e clique em "Salvar".
} {items.map((it, i) => (
onLoad(it)}>
{it.projectName || "Sem nome"} {it.clientName && `· ${it.clientName}`} Nº {it.quoteNumber} · {it.date} · {it.qty}pç
{fmtBRL.format(it.finalUnitPrice * it.qty)}
))}
); } // ====================== Tweaks Panel ====================== function CDTweaks() { const [t, setTweak] = window.useTweaks(/*EDITMODE-BEGIN*/{ "theme": "dark", "density": "comfortable", "lang": "pt", "showAdvanced": false, "accent": "#f5b71d" }/*EDITMODE-END*/); useE(() => { document.documentElement.dataset.theme = t.theme; document.documentElement.dataset.density = t.density; if (t.theme === "dark" || t.theme === "contrast") { document.documentElement.style.setProperty("--accent", t.accent); } else { document.documentElement.style.removeProperty("--accent"); } window.__cdLang = t.lang; window.dispatchEvent(new CustomEvent("cd-tweaks-change", { detail: t })); }, [t]); const { TweaksPanel, TweakSection, TweakRadio, TweakColor, TweakToggle } = window; return ( setTweak("theme", v)} options={["dark", "light", "contrast"]} /> setTweak("density", v)} options={["comfortable", "compact"]} /> setTweak("showAdvanced", v)} /> setTweak("accent", v)} options={["#f5b71d", "#3aa7ff", "#2dd4a0", "#ef4d4d"]} /> setTweak("lang", v)} options={["pt", "en"]} /> ); } // ====================== Main App ====================== function genQuoteNum() { const last = parseInt(localStorage.getItem("cd3d_last_quote") || "0", 10); const n = last + 1; localStorage.setItem("cd3d_last_quote", String(n)); return String(n).padStart(4, "0"); } function App() { const [state, setState] = useS(() => { const saved = cdStorage.load(APP_STORAGE_KEYS.STATE, null); return saved ? { ...APP_DEFAULT_STATE, ...saved, quoteNumber: saved.quoteNumber || genQuoteNum() } : { ...APP_DEFAULT_STATE, quoteNumber: genQuoteNum() }; }); const [step, setStep] = useS(0); const [advanced, setAdvanced] = useS(() => !!localStorage.getItem("cd3d_advanced")); const [history, setHistory] = useS(() => cdStorage.load(APP_STORAGE_KEYS.HISTORY, [])); const [historyOpen, setHistoryOpen] = useS(false); const [toast, setToast] = useS(null); // sync advanced from tweaks useE(() => { const handler = (e) => setAdvanced(e.detail.showAdvanced); window.addEventListener("cd-tweaks-change", handler); return () => window.removeEventListener("cd-tweaks-change", handler); }, []); useE(() => { localStorage.setItem("cd3d_advanced", advanced ? "1" : ""); }, [advanced]); const set = useC((patch) => setState(s => ({ ...s, ...patch })), []); // Persist on every change (debounced) const saveTimer = useR(); useE(() => { clearTimeout(saveTimer.current); saveTimer.current = setTimeout(() => cdStorage.save(APP_STORAGE_KEYS.STATE, state), 300); }, [state]); const calc = useM(() => APP_calculate(state), [state]); // Hash share (encode state to URL) useE(() => { if (location.hash.startsWith("#q=")) { try { const decoded = JSON.parse(decodeURIComponent(atob(location.hash.slice(3)))); setState(s => ({ ...s, ...decoded })); setToast("Orçamento carregado do link compartilhado"); } catch {} } }, []); const showToast = (msg) => { setToast(msg); setTimeout(() => setToast(null), 2400); }; const newQuote = () => { if (!confirm("Limpar todos os campos e começar um novo orçamento?")) return; setState({ ...APP_DEFAULT_STATE, quoteNumber: genQuoteNum() }); setStep(0); }; const saveToHistory = () => { const entry = { id: Date.now(), date: new Date().toLocaleDateString("pt-BR"), ...state, qty: calc.qty, finalUnitPrice: calc.finalUnitPrice, totalRevenue: calc.totalRevenue, productionCost: calc.productionCost }; const next = [entry, ...history].slice(0, 50); setHistory(next); cdStorage.save(APP_STORAGE_KEYS.HISTORY, next); showToast("✓ Salvo no histórico"); }; const loadHistory = (it) => { const { id, date, qty, finalUnitPrice, totalRevenue, productionCost, ...rest } = it; setState(rest); setHistoryOpen(false); showToast("Orçamento restaurado"); }; const deleteHistory = (id) => { const next = history.filter(h => h.id !== id); setHistory(next); cdStorage.save(APP_STORAGE_KEYS.HISTORY, next); }; const clearHistory = () => { if (!confirm("Apagar todo o histórico?")) return; setHistory([]); cdStorage.save(APP_STORAGE_KEYS.HISTORY, []); }; const shareLink = () => { const minimal = { ...state }; const hash = btoa(encodeURIComponent(JSON.stringify(minimal))); const url = `${location.origin}${location.pathname}#q=${hash}`; navigator.clipboard.writeText(url).then(() => showToast("✓ Link copiado para a área de transferência")); }; const shareWhatsApp = () => { const lines = [ `*Orçamento Nº ${state.quoteNumber}* — CIDesign Studio`, state.clientName && `Cliente: ${state.clientName}`, state.projectName && `Projeto: ${state.projectName}`, ``, `📦 Peça: ${state.pieceWeight}g · ${state.printHours}h${state.printMinutes}m de impressão`, `🎨 Material: ${MATERIALS.find(m => m.id === state.materialId)?.name || "—"}`, `🔢 Quantidade: ${calc.qty} peça(s)`, ``, `*Preço unitário: ${fmtBRL.format(calc.finalUnitPrice)}*`, calc.qty > 1 ? `*Total: ${fmtBRL.format(calc.totalRevenue)}*` : null, ``, `Validade: ${state.validityDays} dias`, state.observations && `Obs.: ${state.observations}` ].filter(Boolean).join("\n"); const url = `https://wa.me/?text=${encodeURIComponent(lines)}`; window.open(url, "_blank"); }; const exportCSV = () => { const rows = [ ["CIDesign Studio - Orçamento"], [], ["Orçamento Nº", state.quoteNumber], ["Data", new Date().toLocaleDateString("pt-BR")], ["Cliente", state.clientName], ["Projeto", state.projectName], [], ["Item", "Valor"], ["Filamento", calc.filamentCost.toFixed(2)], ["Energia", calc.energyCost.toFixed(2)], ["Depreciação", calc.wearCost.toFixed(2)], ["Mão de Obra", calc.laborCost.toFixed(2)], ["Embalagem", calc.packagingCost.toFixed(2)], ["Custo Total (un)", calc.productionCost.toFixed(2)], ["Lucro (un)", calc.profit.toFixed(2)], ["Preço Final (un)", calc.finalUnitPrice.toFixed(2)], ["Quantidade", calc.qty], ["Total a receber", calc.totalRevenue.toFixed(2)] ]; const csv = "\ufeff" + rows.map(r => r.map(c => `"${(c ?? "").toString().replace(/"/g, '""')}"`).join(";")).join("\n"); const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `Orcamento_${state.quoteNumber}.csv`; a.click(); URL.revokeObjectURL(url); showToast("✓ CSV exportado"); }; const exportPDF = () => { if (!window.jspdf) { showToast("Carregando PDF..."); return; } const { jsPDF } = window.jspdf; const doc = new jsPDF(); // Header doc.setFillColor(26, 39, 66); doc.rect(0, 0, 210, 32, "F"); doc.setTextColor(245, 183, 29); doc.setFontSize(18); doc.setFont(undefined, "bold"); doc.text("CIDesign Studio", 20, 16); doc.setTextColor(232, 236, 245); doc.setFontSize(10); doc.setFont(undefined, "normal"); doc.text("Orçamento de Impressão 3D", 20, 24); doc.setFontSize(9); doc.text(`Nº ${state.quoteNumber}`, 190, 16, { align: "right" }); doc.text(new Date().toLocaleDateString("pt-BR"), 190, 22, { align: "right" }); let y = 44; doc.setTextColor(0, 0, 0); doc.setFontSize(11); doc.setFont(undefined, "bold"); doc.text("DADOS DO ORÇAMENTO", 20, y); y += 7; doc.setFontSize(10); doc.setFont(undefined, "normal"); if (state.clientName) { doc.text(`Cliente: ${state.clientName}`, 20, y); y += 5; } if (state.projectName) { doc.text(`Projeto: ${state.projectName}`, 20, y); y += 5; } const validUntil = new Date(); validUntil.setDate(validUntil.getDate() + parseInt(state.validityDays || 0)); doc.text(`Válido até: ${validUntil.toLocaleDateString("pt-BR")}`, 20, y); y += 8; doc.setFont(undefined, "bold"); doc.text("ESPECIFICAÇÕES", 20, y); y += 6; doc.setFont(undefined, "normal"); const mat = MATERIALS.find(m => m.id === state.materialId); doc.text(`Material: ${mat?.name || "—"} · Peso: ${state.pieceWeight}g · Suportes: ${state.supportWeight}g`, 20, y); y += 5; doc.text(`Impressora: ${state.printerName} · Tempo: ${state.printHours}h ${state.printMinutes}min`, 20, y); y += 5; doc.text(`Quantidade: ${calc.qty} peça(s)`, 20, y); y += 10; doc.setFont(undefined, "bold"); doc.text("BREAKDOWN DE CUSTOS (UNITÁRIO)", 20, y); y += 7; doc.setFont(undefined, "normal"); const rows = [ ["Filamento", calc.filamentCost], ["Energia", calc.energyCost], ["Depreciação", calc.wearCost], ["Mão de obra", calc.laborCost], ["Embalagem", calc.packagingCost] ]; rows.forEach(([l, v]) => { doc.text(l, 20, y); doc.text(fmtBRL.format(v), 190, y, { align: "right" }); y += 5; }); y += 2; doc.setDrawColor(180,180,180); doc.line(20, y, 190, y); y += 6; doc.setFont(undefined, "bold"); doc.text("Custo de produção:", 20, y); doc.text(fmtBRL.format(calc.productionCost), 190, y, { align: "right" }); y += 8; doc.setFontSize(13); doc.setTextColor(245, 183, 29); doc.text("PREÇO FINAL (un):", 20, y); doc.text(fmtBRL.format(calc.finalUnitPrice), 190, y, { align: "right" }); if (calc.qty > 1) { y += 8; doc.setTextColor(0, 0, 0); doc.setFontSize(11); doc.text(`TOTAL (${calc.qty} pç):`, 20, y); doc.text(fmtBRL.format(calc.totalRevenue), 190, y, { align: "right" }); } if (state.observations) { y += 12; doc.setTextColor(0,0,0); doc.setFontSize(10); doc.setFont(undefined, "bold"); doc.text("OBSERVAÇÕES", 20, y); y += 5; doc.setFont(undefined, "normal"); const lines = doc.splitTextToSize(state.observations, 170); doc.text(lines, 20, y); } doc.setFontSize(8); doc.setTextColor(128, 128, 128); doc.text("CIDesign Studio · cidesign.net.br/calculadora3d", 105, 285, { align: "center" }); doc.save(`Orcamento_${state.quoteNumber}.pdf`); showToast("✓ PDF gerado"); }; const StepComp = [APP_StepMaterial, APP_StepPrint, APP_StepLabor, APP_StepPricing][step]; return ( <>
CIDesign
CIDesign Studio Calculadora 3D
{APP_STEPS.map(st => ( ))}
{step < APP_STEPS.length - 1 ? ( ) : ( )}
setHistoryOpen(false)} items={history} onLoad={loadHistory} onDelete={deleteHistory} onClear={clearHistory} /> {toast &&
{toast}
} ); } ReactDOM.createRoot(document.getElementById("root")).render();