/* 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}
))}
| 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)} |
{calc.platformFeeValue > 0 && (
| Taxa plataforma {s.platformFee}% (gross-up) |
{fmtBRL.format(calc.platformFeeValue)} |
)}
{calc.taxValue > 0 && (
| Impostos {s.taxRate}% |
{fmtBRL.format(calc.taxValue)} |
)}
{calc.shippingIncluded && calc.shipping > 0 && (
| 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 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();