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.
196 lines
11 KiB
TypeScript
196 lines
11 KiB
TypeScript
|
|
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;
|
|
};
|