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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions