feat: Initialize ComFi project with Vite

Setup project structure, dependencies, and basic configuration for the ComFi application. Includes initial setup for Vite, React, TypeScript, Tailwind CSS, and essential development tools. Defines core types and provides a basic README for local development.
This commit is contained in:
MMrp89
2026-02-09 20:28:37 -03:00
parent 1e6a56d866
commit 1a57ac7754
28 changed files with 6070 additions and 8 deletions

195
contexts/ComFiContext.tsx Normal file
View File

@@ -0,0 +1,195 @@
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { Company, Expense, Receivable, Service, AppUser, Client, FinancialSummary, TenantProfile, Category, Proposal } from '../types';
import { useStickyState } from '../hooks/useStickyState';
// --- MOCK DATA GENERATORS (Moved from App.tsx) ---
const getDynamicDate = (daysOffset: number) => {
const date = new Date();
date.setDate(date.getDate() + daysOffset);
return date.toISOString().split('T')[0];
};
const initialServices: Service[] = [
{ id: '1', name: 'Consultoria Financeira', category: 'Consultoria', price: 2500, active: true, description: 'Análise completa.', billingType: 'one-time' },
{ id: '2', name: 'Gestão de Mídias', category: 'Marketing', price: 1500, active: true, description: 'Postagens mensais.', billingType: 'recurring' },
{ id: '3', name: 'Suporte TI', category: 'TI', price: 800, active: true, description: 'SLA 24h.', billingType: 'recurring' },
{ id: '4', name: 'Desenvolvimento Web', category: 'Tecnologia', price: 5000, active: true, description: 'Site institucional.', billingType: 'one-time' },
];
const initialCompanies: Company[] = [
{
id: '1', name: 'Uda Studios Tecnologia LTDA', fantasyName: 'Uda Studios', cnpj: '12.345.678/0001-90', ie: '123.456.789.000', city: 'São Paulo - SP', logo: 'https://i.pravatar.cc/150?u=uda', status: 'active', industry: 'Tecnologia', email: 'financeiro@uda.com', phone: '(11) 3344-5566', address: 'Av. Paulista, 1000, Bela Vista', since: '2022', description: 'Estúdio de software.', contacts: [{ id: 'c1', name: 'Roberto Silva', role: 'CEO', email: 'roberto@uda.com', phone: '11999998888', avatar: 'https://i.pravatar.cc/150?u=1' }], documents: [], activeServices: [initialServices[1], initialServices[3]]
},
{
id: '2', name: 'Angels Healthcare S.A.', fantasyName: 'Angels Health', cnpj: '98.765.432/0001-10', city: 'Rio de Janeiro - RJ', logo: 'https://i.pravatar.cc/150?u=angels', status: 'active', industry: 'Saúde', email: 'contato@angels.com', phone: '(21) 2233-4455', address: 'Rua da Saúde, 500', since: '2021', description: 'Rede de clínicas.', contacts: [], documents: [], activeServices: [initialServices[0], initialServices[2]]
},
{
id: '3', name: 'Padaria do João MEI', fantasyName: 'Padaria do João', cnpj: '11.111.111/0001-11', city: 'Belo Horizonte - MG', logo: 'https://i.pravatar.cc/150?u=joao', status: 'overdue', industry: 'Varejo', email: 'joao@padaria.com', phone: '(31) 3333-3333', address: 'Rua do Pão, 10', since: '2023', description: 'Padaria artesanal.', contacts: [], documents: [], activeServices: [initialServices[1]]
}
];
const initialExpenses: Expense[] = [
{ id: 'e1', title: 'Aluguel Escritório', category: 'Operacional', amount: 3500, dueDate: getDynamicDate(-30), status: 'paid', type: 'fixed' },
{ id: 'e2', title: 'Servidor AWS', category: 'TI', amount: 850, dueDate: getDynamicDate(-28), status: 'paid', type: 'variable' },
{ id: 'e3', title: 'Energia Elétrica', category: 'Operacional', amount: 450, dueDate: getDynamicDate(-2), status: 'paid', type: 'variable' },
{ id: 'e4', title: 'Licença Software CRM', category: 'TI', amount: 200, dueDate: getDynamicDate(5), status: 'pending', type: 'fixed' },
{ id: 'e5', title: 'Folha de Pagamento', category: 'Pessoal', amount: 15000, dueDate: getDynamicDate(10), status: 'pending', type: 'fixed' },
{ id: 'e6', title: 'DAS Simples Nacional', category: 'Impostos', amount: 1200, dueDate: getDynamicDate(15), status: 'pending', type: 'variable' },
{ id: 'e7', title: 'Aluguel Escritório', category: 'Operacional', amount: 3500, dueDate: getDynamicDate(30), status: 'pending', type: 'fixed' },
];
const initialProposals: Proposal[] = [
{
id: 'p1', number: 'PROP-001', clientId: '1', clientName: 'Uda Studios', clientEmail: 'roberto@uda.com',
issueDate: getDynamicDate(-2), validUntil: getDynamicDate(5), status: 'sent', totalValue: 7500,
items: [
{ id: 'i1', description: 'Consultoria Financeira', quantity: 1, unitPrice: 2500, total: 2500 },
{ id: 'i2', description: 'Desenvolvimento Web', quantity: 1, unitPrice: 5000, total: 5000 }
],
notes: 'Pagamento 50% na entrada e 50% na entrega.'
}
];
const initialSuperAdmin: AppUser = { id: 'u1', name: 'Nella Vita', email: 'admin@comfi.com', role: 'super_admin', active: true, avatar: 'https://i.pravatar.cc/150?u=admin', permissions: [] };
const initialUsers: AppUser[] = [
initialSuperAdmin,
{ id: 'u2', name: 'João Comercial', email: 'vendas@comfi.com', role: 'user', active: true, avatar: 'https://i.pravatar.cc/150?u=joao', permissions: ['crm', 'kanban', 'calendar', 'proposals'] },
{ id: 'u3', name: 'Maria Financeiro', email: 'fin@comfi.com', role: 'user', active: true, avatar: 'https://i.pravatar.cc/150?u=maria', permissions: ['dashboard', 'receivables', 'payables', 'invoicing'] }
];
const initialTenant: TenantProfile = {
name: 'Minha Empresa S.A.',
cnpj: '00.000.000/0001-00',
email: 'contato@minhaempresa.com',
phone: '(11) 99999-9999',
address: 'Av. Brigadeiro Faria Lima, 1234, SP',
logo: '',
primaryColor: '#f97316'
};
const initialCategories: Category[] = [
{ id: 'c1', name: 'Operacional', type: 'expense', color: 'bg-red-50 text-red-600' },
{ id: 'c2', name: 'Administrativo', type: 'expense', color: 'bg-blue-50 text-blue-600' },
{ id: 'c3', name: 'Marketing', type: 'expense', color: 'bg-purple-50 text-purple-600' },
{ id: 'c4', name: 'Pessoal', type: 'expense', color: 'bg-yellow-50 text-yellow-600' },
{ id: 'c5', name: 'Impostos', type: 'expense', color: 'bg-slate-50 text-slate-600' },
{ id: 'c6', name: 'Serviços', type: 'income', color: 'bg-green-50 text-green-600' },
];
const generateInitialReceivables = (companies: Company[]): Receivable[] => {
const receivables: Receivable[] = [];
companies.forEach((company, cIndex) => {
company.activeServices.forEach((service, sIndex) => {
receivables.push({ id: `r-${company.id}-${sIndex}-past`, description: service.name, companyName: company.fantasyName || company.name, category: service.category, value: service.price, dueDate: getDynamicDate(-30 - (cIndex * 2)), status: 'paid', type: service.billingType === 'recurring' ? 'recurring' : 'one-time' });
const isOverdue = company.status === 'overdue' && sIndex === 0;
receivables.push({ id: `r-${company.id}-${sIndex}-curr`, description: service.name, companyName: company.fantasyName || company.name, category: service.category, value: service.price, dueDate: getDynamicDate(isOverdue ? -5 : 10 + (cIndex * 3)), status: isOverdue ? 'overdue' : 'pending', type: service.billingType === 'recurring' ? 'recurring' : 'one-time' });
if (service.billingType === 'recurring') {
receivables.push({ id: `r-${company.id}-${sIndex}-next`, description: service.name, companyName: company.fantasyName || company.name, category: service.category, value: service.price, dueDate: getDynamicDate(40 + (cIndex * 2)), status: 'pending', type: 'recurring' });
}
});
});
return receivables;
};
// --- CONTEXT INTERFACE ---
interface ComFiContextData {
services: Service[];
setServices: (data: Service[]) => void;
companies: Company[];
setCompanies: (data: Company[]) => void;
expenses: Expense[];
setExpenses: (data: Expense[]) => void;
clients: Client[];
setClients: (data: Client[]) => void;
receivables: Receivable[];
setReceivables: React.Dispatch<React.SetStateAction<Receivable[]>>;
proposals: Proposal[];
setProposals: (data: Proposal[]) => void;
currentUser: AppUser;
setCurrentUser: (user: AppUser) => void;
users: AppUser[];
setUsers: (users: AppUser[]) => void;
financialSummary: FinancialSummary;
addReceivable: (receivable: Receivable) => void;
tenant: TenantProfile;
setTenant: (tenant: TenantProfile) => void;
categories: Category[];
setCategories: (categories: Category[]) => void;
}
const ComFiContext = createContext<ComFiContextData>({} as ComFiContextData);
export const ComFiProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
// State Initialization
const [services, setServices] = useStickyState<Service[]>(initialServices, 'comfi_services');
const [companies, setCompanies] = useStickyState<Company[]>(initialCompanies, 'comfi_companies');
const [expenses, setExpenses] = useStickyState<Expense[]>(initialExpenses, 'comfi_expenses');
const [clients, setClients] = useStickyState<Client[]>([], 'comfi_clients');
const [currentUser, setCurrentUser] = useStickyState<AppUser>(initialSuperAdmin, 'comfi_current_user');
const [users, setUsers] = useStickyState<AppUser[]>(initialUsers, 'comfi_users');
const [tenant, setTenant] = useStickyState<TenantProfile>(initialTenant, 'comfi_tenant');
const [categories, setCategories] = useStickyState<Category[]>(initialCategories, 'comfi_categories');
const [proposals, setProposals] = useStickyState<Proposal[]>(initialProposals, 'comfi_proposals');
const [receivables, setReceivables] = useState<Receivable[]>(() => {
try {
const saved = window.localStorage.getItem('comfi_receivables');
if (saved) return JSON.parse(saved);
return generateInitialReceivables(initialCompanies);
} catch (e) {
return generateInitialReceivables(initialCompanies);
}
});
useEffect(() => {
window.localStorage.setItem('comfi_receivables', JSON.stringify(receivables));
}, [receivables]);
// Derived State: Financial Summary
const financialSummary = useMemo(() => {
const currentMonth = new Date().getMonth();
const currentYear = new Date().getFullYear();
const isCurrentMonth = (dateStr: string) => dateStr.startsWith(`${currentYear}-${String(currentMonth + 1).padStart(2, '0')}`);
const currentReceivables = receivables.filter(r => isCurrentMonth(r.dueDate));
const currentExpenses = expenses.filter(e => isCurrentMonth(e.dueDate));
const totalRevenue = currentReceivables.reduce((acc, r) => acc + r.value, 0);
const totalExpenses = currentExpenses.reduce((acc, expense) => acc + expense.amount, 0);
const profit = totalRevenue - totalExpenses;
const payablePending = expenses.filter(e => e.status !== 'paid').reduce((acc, expense) => acc + expense.amount, 0);
const receivablePending = receivables.filter(r => r.status !== 'paid').reduce((acc, r) => acc + r.value, 0);
return { totalRevenue, totalExpenses, profit, payablePending, receivablePending };
}, [receivables, expenses]);
const addReceivable = (newRec: Receivable) => {
setReceivables(prev => [...prev, newRec]);
};
return (
<ComFiContext.Provider value={{
services, setServices,
companies, setCompanies,
expenses, setExpenses,
clients, setClients,
receivables, setReceivables,
currentUser, setCurrentUser,
users, setUsers,
financialSummary,
addReceivable,
tenant, setTenant,
categories, setCategories,
proposals, setProposals
}}>
{children}
</ComFiContext.Provider>
);
};
export const useComFi = () => {
const context = useContext(ComFiContext);
if (!context) throw new Error('useComFi must be used within a ComFiProvider');
return context;
};

82
contexts/ToastContext.tsx Normal file
View File

@@ -0,0 +1,82 @@
import React, { createContext, useContext, useState, useCallback } from 'react';
import { Toast } from '../types';
import { X, CheckCircle2, AlertCircle, Info, AlertTriangle } from 'lucide-react';
interface ToastContextData {
addToast: (toast: Omit<Toast, 'id'>) => void;
removeToast: (id: string) => void;
}
const ToastContext = createContext<ToastContextData>({} as ToastContextData);
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [toasts, setToasts] = useState<Toast[]>([]);
const addToast = useCallback(({ type, title, message, duration = 4000 }: Omit<Toast, 'id'>) => {
const id = Math.random().toString(36).substring(2, 9);
const newToast = { id, type, title, message, duration };
setToasts((state) => [...state, newToast]);
if (duration > 0) {
setTimeout(() => {
removeToast(id);
}, duration);
}
}, []);
const removeToast = useCallback((id: string) => {
setToasts((state) => state.filter((toast) => toast.id !== id));
}, []);
return (
<ToastContext.Provider value={{ addToast, removeToast }}>
{children}
{/* Toast Container Rendered Here Globally */}
<div className="fixed top-4 right-4 z-[9999] flex flex-col gap-2 pointer-events-none">
{toasts.map((toast) => (
<div
key={toast.id}
className={`pointer-events-auto min-w-[300px] max-w-sm rounded-xl p-4 shadow-xl border flex items-start gap-3 animate-slide-in-right transform transition-all duration-300 ${
toast.type === 'success' ? 'bg-white border-green-100' :
toast.type === 'error' ? 'bg-white border-red-100' :
toast.type === 'warning' ? 'bg-white border-amber-100' :
'bg-white border-blue-100'
}`}
>
<div className={`mt-0.5 ${
toast.type === 'success' ? 'text-green-500' :
toast.type === 'error' ? 'text-red-500' :
toast.type === 'warning' ? 'text-amber-500' :
'text-blue-500'
}`}>
{toast.type === 'success' && <CheckCircle2 size={20} />}
{toast.type === 'error' && <AlertCircle size={20} />}
{toast.type === 'warning' && <AlertTriangle size={20} />}
{toast.type === 'info' && <Info size={20} />}
</div>
<div className="flex-1">
<h4 className={`text-sm font-bold ${
toast.type === 'success' ? 'text-green-800' :
toast.type === 'error' ? 'text-red-800' :
toast.type === 'warning' ? 'text-amber-800' :
'text-blue-800'
}`}>{toast.title}</h4>
{toast.message && <p className="text-xs text-slate-500 mt-1 leading-relaxed">{toast.message}</p>}
</div>
<button onClick={() => removeToast(toast.id)} className="text-slate-400 hover:text-slate-600">
<X size={16} />
</button>
</div>
))}
</div>
</ToastContext.Provider>
);
};
export const useToast = () => {
const context = useContext(ToastContext);
if (!context) throw new Error('useToast must be used within a ToastProvider');
return context;
};