Skip to content

controle mercado #1412

@mydreampersonalizados25-del

Description

`

<title>Controle de Gastos - Mensal e Semanal</title> <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script> <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> <script src="https://cdn.tailwindcss.com"></script>
<!-- Firebase Libraries -->
<script type="module">
    import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
    import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
    import { getFirestore, doc, setDoc, onSnapshot, collection, query, deleteDoc } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";

    const firebaseConfig = JSON.parse(__firebase_config);
    const app = initializeApp(firebaseConfig);
    const auth = getAuth(app);
    const db = getFirestore(app);
    const appId = typeof __app_id !== 'undefined' ? __app_id : 'controle-gastos-v4';

    window.FB = { auth, db, appId, doc, setDoc, onSnapshot, collection, query, deleteDoc, signInAnonymously, signInWithCustomToken };
</script>

<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap" rel="stylesheet">
<style>
    body { font-family: 'Inter', sans-serif; -webkit-tap-highlight-color: transparent; }
    .animate-pop { animation: pop 0.25s ease-out; }
    @keyframes pop { 0% { transform: scale(0.98); opacity: 0; } 100% { transform: scale(1); opacity: 1; } }
    .no-scrollbar::-webkit-scrollbar { display: none; }
    .glass { background: rgba(255, 255, 255, 0.8); backdrop-filter: blur(10px); }
</style>
<script type="text/babel">
    const { useState, useEffect, useMemo } = React;

    const CATEGORIES = [
        { id: 'mercado', nome: 'Mercado', icon: '🛒', color: 'bg-blue-500' },
        { id: 'feira', nome: 'Feira', icon: '🍎', color: 'bg-green-500' },
        { id: 'farmacia', nome: 'Farmácia', icon: '💊', color: 'bg-red-500' },
        { id: 'restaurante', nome: 'Restaurante', icon: '☕', color: 'bg-orange-500' },
        { id: 'outros', nome: 'Outros', icon: '✨', color: 'bg-slate-500' }
    ];

    const PAY_METHODS = { 'Crédito': '💳', 'Débito': '🏧', 'PIX': '📱', 'Dinheiro': '💵' };
    const MONTHS = ['Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'];
    
    const fmtCur = (v) => new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(v || 0);

    function App() {
        const [user, setUser] = useState(null);
        const [activeTab, setActiveTab] = useState('dashboard');
        const [transactions, setTransactions] = useState([]);
        const [config, setConfig] = useState({ tetoGlobal: 1500, subtetos: {} });
        const [loading, setLoading] = useState(true);
        const [isModalOpen, setIsModalOpen] = useState(false);
        const [editingItem, setEditingItem] = useState(null);
        
        // Estado do Filtro Temporal
        const today = new Date();
        const [viewDate, setViewDate] = useState({ month: today.getMonth(), year: today.getFullYear() });

        // 1. Firebase Auth
        useEffect(() => {
            const initAuth = async () => {
                const { auth, signInAnonymously, signInWithCustomToken } = window.FB;
                if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) {
                    await signInWithCustomToken(auth, __initial_auth_token);
                } else {
                    await signInAnonymously(auth);
                }
            };
            initAuth();
            return window.FB.auth.onAuthStateChanged(u => setUser(u));
        }, []);

        // 2. Firestore Listeners
        useEffect(() => {
            if (!user) return;
            const { db, appId, onSnapshot, doc, collection } = window.FB;

            const transCol = collection(db, 'artifacts', appId, 'users', user.uid, 'transactions');
            const unsubTrans = onSnapshot(transCol, (snapshot) => {
                const items = snapshot.docs.map(d => ({ ...d.data(), id: d.id }));
                setTransactions(items);
                setLoading(false);
            });

            const configDoc = doc(db, 'artifacts', appId, 'users', user.uid, 'settings', 'main');
            const unsubConfig = onSnapshot(configDoc, (docSnap) => {
                if (docSnap.exists()) setConfig(docSnap.data());
            });

            return () => { unsubTrans(); unsubConfig(); };
        }, [user]);

        // 3. Cálculos Derivados (Filtros de Período)
        const filteredData = useMemo(() => {
            return transactions.filter(t => {
                const d = new Date(t.data + 'T00:00:00');
                return d.getMonth() === viewDate.month && d.getFullYear() === viewDate.year;
            }).sort((a, b) => new Date(b.data) - new Date(a.data));
        }, [transactions, viewDate]);

        const weeklyTotal = useMemo(() => {
            const now = new Date();
            const startOfWeek = new Date(now.setDate(now.getDate() - now.getDay()));
            startOfWeek.setHours(0,0,0,0);
            
            return filteredData.filter(t => {
                const d = new Date(t.data + 'T00:00:00');
                return d >= startOfWeek;
            }).reduce((acc, t) => acc + t.valor, 0);
        }, [filteredData]);

        const monthlyTotal = filteredData.reduce((acc, t) => acc + t.valor, 0);
        const percentual = (monthlyTotal / config.tetoGlobal) * 100;

        // 4. Ações
        const handleSave = async (item) => {
            const { db, appId, doc, setDoc } = window.FB;
            const id = editingItem ? editingItem.id : Date.now().toString();
            const docRef = doc(db, 'artifacts', appId, 'users', user.uid, 'transactions', id);
            await setDoc(docRef, item);
            setIsModalOpen(false);
            setEditingItem(null);
        };

        const handleDelete = async (id) => {
            if (!confirm("Excluir este gasto?")) return;
            const { db, appId, doc, deleteDoc } = window.FB;
            await deleteDoc(doc(db, 'artifacts', appId, 'users', user.uid, 'transactions', id));
            setIsModalOpen(false);
            setEditingItem(null);
        };

        const changeMonth = (dir) => {
            setViewDate(prev => {
                let newMonth = prev.month + dir;
                let newYear = prev.year;
                if (newMonth > 11) { newMonth = 0; newYear++; }
                if (newMonth < 0) { newMonth = 11; newYear--; }
                return { month: newMonth, year: newYear };
            });
        };

        if (loading) return (
            <div className="flex flex-col items-center justify-center min-h-screen bg-white">
                <div className="w-10 h-10 border-4 border-indigo-600 border-t-transparent rounded-full animate-spin"></div>
            </div>
        );

        return (
            <div className="max-w-md mx-auto min-h-screen flex flex-col bg-slate-50 relative overflow-x-hidden">
                {/* Top Bar - Seletor de Mês */}
                <div className="bg-white px-5 py-4 border-b flex justify-between items-center sticky top-0 z-30 shadow-sm">
                    <button onClick={() => changeMonth(-1)} className="p-2 hover:bg-slate-100 rounded-full">◀</button>
                    <div className="text-center">
                        <h2 className="font-black text-slate-800 uppercase text-sm tracking-widest">{MONTHS[viewDate.month]}</h2>
                        <p className="text-[10px] font-bold text-slate-400">{viewDate.year}</p>
                    </div>
                    <button onClick={() => changeMonth(1)} className="p-2 hover:bg-slate-100 rounded-full">▶</button>
                </div>

                <main className="p-4 flex-1 mb-24 overflow-y-auto no-scrollbar">
                    {activeTab === 'dashboard' && (
                        <div className="animate-pop space-y-6">
                            {/* Resumo Semanal Mini */}
                            <div className="flex gap-3">
                                <div className="flex-1 bg-white p-4 rounded-3xl border border-slate-100 shadow-sm">
                                    <p className="text-[9px] font-black text-slate-400 uppercase tracking-tighter mb-1">Gasto na Semana</p>
                                    <p className="text-lg font-black text-slate-800">{fmtCur(weeklyTotal)}</p>
                                </div>
                                <div className="flex-1 bg-white p-4 rounded-3xl border border-slate-100 shadow-sm">
                                    <p className="text-[9px] font-black text-slate-400 uppercase tracking-tighter mb-1">Média Diária</p>
                                    <p className="text-lg font-black text-indigo-600">{fmtCur(monthlyTotal / (new Date(viewDate.year, viewDate.month + 1, 0).getDate()))}</p>
                                </div>
                            </div>

                            {/* Card Mensal Principal */}
                            <div className="bg-gradient-to-br from-indigo-600 to-indigo-900 rounded-[2.5rem] p-7 text-white shadow-xl shadow-indigo-100 relative overflow-hidden">
                                <div className="absolute top-[-10%] right-[-10%] w-40 h-40 bg-white/10 rounded-full blur-3xl"></div>
                                <p className="text-indigo-100 text-[10px] font-black uppercase tracking-widest mb-1">Total {MONTHS[viewDate.month]}</p>
                                <h2 className="text-4xl font-black mb-6 tracking-tighter">{fmtCur(monthlyTotal)}</h2>
                                
                                <div className="w-full bg-black/20 h-4 rounded-full overflow-hidden mb-3 p-1">
                                    <div className={`h-full rounded-full transition-all duration-1000 ${percentual > 100 ? 'bg-red-400' : 'bg-emerald-400'}`} style={{ width: `${Math.min(percentual, 100)}%` }}></div>
                                </div>
                                <div className="flex justify-between text-[11px] font-black opacity-80 uppercase">
                                    <span>Meta: {fmtCur(config.tetoGlobal)}</span>
                                    <span>{percentual.toFixed(1)}%</span>
                                </div>
                            </div>

                            {/* Categorias no Mês */}
                            <div className="grid grid-cols-2 gap-4">
                                {CATEGORIES.map(cat => {
                                    const gastoCat = filteredData.filter(t => t.categoria === cat.id).reduce((a, b) => a + b.valor, 0);
                                    const subteto = config.subtetos?.[cat.id] || 0;
                                    const catPct = subteto > 0 ? (gastoCat / subteto) * 100 : 0;
                                    return (
                                        <div key={cat.id} className="bg-white p-4 rounded-3xl border border-slate-100 shadow-sm flex flex-col justify-between h-32 relative overflow-hidden">
                                            <div className="flex justify-between items-start z-10">
                                                <span className="text-2xl">{cat.icon}</span>
                                                {subteto > 0 && <span className={`text-[9px] font-black px-2 py-0.5 rounded-lg ${catPct >= 100 ? 'bg-red-100 text-red-600' : 'bg-slate-100 text-slate-500'}`}>{catPct.toFixed(0)}%</span>}
                                            </div>
                                            <div className="z-10">
                                                <p className="text-[10px] font-bold text-slate-400 uppercase leading-none mb-1">{cat.nome}</p>
                                                <p className="text-sm font-black text-slate-800">{fmtCur(gastoCat)}</p>
                                            </div>
                                            {subteto > 0 && <div className="absolute bottom-0 left-0 h-1.5 bg-slate-50 w-full"><div className={`h-full ${catPct >= 100 ? 'bg-red-500' : cat.color} transition-all duration-700`} style={{ width: `${Math.min(catPct, 100)}%` }}></div></div>}
                                        </div>
                                    );
                                })}
                            </div>

                            {/* Últimos Lançamentos do Mês */}
                            <div>
                                <h3 className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-4 px-1">Atividade em {MONTHS[viewDate.month]}</h3>
                                <div className="space-y-3">
                                    {filteredData.slice(0, 8).map(t => (
                                        <div key={t.id} onClick={() => { setEditingItem(t); setIsModalOpen(true); }} className="bg-white p-4 rounded-3xl flex justify-between items-center border border-slate-100 shadow-sm active:scale-95 transition-all">
                                            <div className="flex items-center gap-4">
                                                <div className="text-xl bg-slate-50 w-12 h-12 rounded-2xl flex items-center justify-center">{CATEGORIES.find(c => c.id === t.categoria)?.icon}</div>
                                                <div>
                                                    <p className="text-sm font-bold text-slate-800 line-clamp-1">{t.descricao}</p>
                                                    <p className="text-[9px] text-slate-300 font-black uppercase">{t.data.split('-').reverse().slice(0,2).join('/')} • {t.formaPagamento}</p>
                                                </div>
                                            </div>
                                            <p className="font-black text-slate-800">{fmtCur(t.valor)}</p>
                                        </div>
                                    ))}
                                </div>
                            </div>
                        </div>
                    )}

                    {activeTab === 'history' && (
                        <div className="animate-pop space-y-4">
                            <h2 className="font-black text-xl text-slate-800 mb-4 px-1">Todos os Gastos de {MONTHS[viewDate.month]}</h2>
                            {filteredData.length === 0 ? (
                                <div className="text-center py-20 opacity-30">
                                    <span className="text-5xl block mb-2">📄</span>
                                    <p className="font-bold uppercase text-xs">Sem registros este mês</p>
                                </div>
                            ) : filteredData.map(t => (
                                <div key={t.id} onClick={() => { setEditingItem(t); setIsModalOpen(true); }} className="bg-white p-5 rounded-[2rem] flex justify-between items-center border border-slate-100 shadow-sm">
                                    <div>
                                        <p className="text-[9px] font-black text-indigo-500 mb-1 uppercase tracking-tighter">{t.data.split('-').reverse().join('/')}</p>
                                        <p className="font-bold text-slate-700 leading-tight">{t.descricao}</p>
                                        <p className="text-[9px] font-black uppercase text-slate-300 mt-1">{CATEGORIES.find(c => c.id === t.categoria)?.nome}</p>
                                    </div>
                                    <div className="text-right">
                                        <p className="font-black text-slate-800 text-lg">{fmtCur(t.valor)}</p>
                                        <p className="text-[9px] text-slate-400 font-bold uppercase tracking-widest">{t.formaPagamento}</p>
                                    </div>
                                </div>
                            ))}
                        </div>
                    )}

                    {activeTab === 'config' && (
                        <div className="animate-pop space-y-6">
                            <h2 className="font-black text-xl text-slate-800 px-1">Definições de Orçamento</h2>
                            <div className="bg-white p-7 rounded-[2.5rem] border border-slate-100 shadow-sm space-y-8">
                                <div>
                                    <label className="text-[10px] font-black text-slate-400 uppercase block mb-3 tracking-widest">Teto Global Mensal</label>
                                    <input type="number" value={config.tetoGlobal} onChange={e => {
                                        const val = Number(e.target.value);
                                        setConfig({...config, tetoGlobal: val});
                                        window.FB.setDoc(window.FB.doc(window.FB.db, 'artifacts', window.FB.appId, 'users', user.uid, 'settings', 'main'), {...config, tetoGlobal: val});
                                    }} className="w-full bg-slate-50 p-5 rounded-3xl font-black text-4xl text-indigo-600 outline-none border-2 border-transparent focus:border-indigo-100 transition-all" />
                                </div>
                                <div className="space-y-4">
                                    <label className="text-[10px] font-black text-slate-400 uppercase block mb-2 tracking-widest">Metas por Categoria</label>
                                    {CATEGORIES.map(cat => (
                                        <div key={cat.id} className="flex items-center gap-4 bg-slate-50 p-4 rounded-3xl border border-slate-100">
                                            <span className="text-2xl">{cat.icon}</span>
                                            <span className="text-xs font-black text-slate-600 flex-1">{cat.nome}</span>
                                            <input type="number" value={config.subtetos?.[cat.id] || ''} placeholder="0" onChange={e => {
                                                const newSub = {...(config.subtetos||{}), [cat.id]: Number(e.target.value)};
                                                const newConf = {...config, subtetos: newSub};
                                                setConfig(newConf);
                                                window.FB.setDoc(window.FB.doc(window.FB.db, 'artifacts', window.FB.appId, 'users', user.uid, 'settings', 'main'), newConf);
                                            }} className="w-24 bg-white p-2 rounded-xl font-black text-right outline-none border border-slate-100 text-slate-800" />
                                        </div>
                                    ))}
                                </div>
                            </div>
                            <div className="p-4 bg-indigo-50 rounded-3xl border border-indigo-100">
                                <p className="text-[10px] font-bold text-indigo-400 uppercase mb-1">ID da Nuvem</p>
                                <p className="text-[9px] font-mono text-indigo-300 break-all">{user.uid}</p>
                            </div>
                        </div>
                    )}
                </main>

                {/* Botão Flutuante */}
                <button onClick={() => { setEditingItem(null); setIsModalOpen(true); }} className="fixed bottom-28 right-6 w-16 h-16 bg-indigo-600 text-white rounded-3xl shadow-2xl shadow-indigo-200 flex items-center justify-center active:scale-90 transition-all z-40">
                    <span className="text-4xl font-light">+</span>
                </button>

                {/* Bottom Nav */}
                <nav className="fixed bottom-0 left-0 right-0 glass border-t p-6 flex justify-around items-center z-50 pb-8">
                    <NavButton icon="🏠" label="Resumo" active={activeTab === 'dashboard'} onClick={() => setActiveTab('dashboard')} />
                    <NavButton icon="📋" label="Histórico" active={activeTab === 'history'} onClick={() => setActiveTab('history')} />
                    <div className="w-12"></div>
                    <NavButton icon="🎯" label="Metas" active={activeTab === 'config'} onClick={() => setActiveTab('config')} />
                    <NavButton icon="👤" label="Perfil" onClick={() => alert("Sincronizado via: " + user.uid.substring(0,8))} />
                </nav>

                {/* Modal Form */}
                {isModalOpen && (
                    <div className="fixed inset-0 bg-slate-900/60 backdrop-blur-sm flex items-end sm:items-center justify-center p-0 sm:p-4 z-[100]">
                        <div className="bg-white w-full max-w-md p-7 rounded-t-[3rem] sm:rounded-[3rem] animate-pop shadow-2xl">
                            <div className="flex justify-between items-center mb-8">
                                <h2 className="font-black text-2xl text-slate-800">{editingItem ? '✏️ Editar' : '✨ Novo Gasto'}</h2>
                                <button onClick={() => setIsModalOpen(false)} className="text-slate-300 text-4xl">&times;</button>
                            </div>
                            <form onSubmit={e => {
                                e.preventDefault();
                                const fd = new FormData(e.target);
                                handleSave({
                                    data: fd.get('data'),
                                    valor: parseFloat(fd.get('valor')),
                                    descricao: fd.get('descricao'),
                                    categoria: fd.get('categoria'),
                                    formaPagamento: fd.get('forma')
                                });
                            }} className="space-y-6">
                                <div className="bg-slate-50 p-7 rounded-[2.5rem] border border-slate-100 text-center">
                                    <label className="text-[10px] font-black text-slate-400 block mb-2 uppercase tracking-widest">Valor do Lançamento</label>
                                    <div className="flex justify-center items-center">
                                        <span className="text-2xl font-black text-slate-200 mr-2">R$</span>
                                        <input name="valor" type="number" step="0.01" required autoFocus defaultValue={editingItem?.valor || ''} className="w-40 bg-transparent text-5xl font-black outline-none text-indigo-600 text-center" placeholder="0,00" />
                                    </div>
                                </div>
                                <div className="space-y-4">
                                    <input name="descricao" required defaultValue={editingItem?.descricao || ''} placeholder="Onde você gastou?" className="w-full bg-slate-50 p-5 rounded-2xl font-bold outline-none border border-slate-100 placeholder:text-slate-300" />
                                    <div className="grid grid-cols-2 gap-4">
                                        <select name="categoria" defaultValue={editingItem?.categoria || 'mercado'} className="bg-slate-50 p-4 rounded-2xl font-bold outline-none border border-slate-100 text-slate-600">
                                            {CATEGORIES.map(c => <option key={c.id} value={c.id}>{c.icon} {c.nome}</option>)}
                                        </select>
                                        <select name="forma" defaultValue={editingItem?.formaPagamento || 'Débito'} className="bg-slate-50 p-4 rounded-2xl font-bold outline-none border border-slate-100 text-slate-600">
                                            {Object.keys(PAY_METHODS).map(f => <option key={f} value={f}>{PAY_METHODS[f]} {f}</option>)}
                                        </select>
                                    </div>
                                    <input name="data" type="date" defaultValue={editingItem?.data || new Date().toISOString().split('T')[0]} className="w-full bg-slate-50 p-5 rounded-2xl font-bold outline-none border border-slate-100 text-slate-500" />
                                </div>
                                
                                <div className="flex gap-3">
                                    {editingItem && (
                                        <button type="button" onClick={() => handleDelete(editingItem.id)} className="w-16 h-16 bg-red-50 text-red-500 rounded-3xl flex items-center justify-center text-xl">🗑️</button>
                                    )}
                                    <button type="submit" className="flex-1 bg-indigo-600 text-white h-16 rounded-3xl font-black uppercase tracking-widest text-xs shadow-xl active:scale-95 transition-all">Sincronizar Agora</button>
                                </div>
                            </form>
                        </div>
                    </div>
                )}
            </div>
        );
    }

    function NavButton({ icon, label, active, onClick }) {
        return (
            <button onClick={onClick} className={`flex flex-col items-center gap-1 transition-all ${active ? 'text-indigo-600 scale-110' : 'text-slate-300'}`}>
                <span className="text-xl">{icon}</span>
                <span className="text-[9px] font-black uppercase tracking-tighter">{label}</span>
            </button>
        );
    }

    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(<App />);
</script>
`

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions