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

View File

@@ -0,0 +1,318 @@
import React, { useState } from 'react';
import { Search, Plus, DollarSign, Calendar, CheckCircle2, AlertCircle, Trash2, X, Pencil, Sparkles, ChevronDown } from 'lucide-react';
import { Expense } from '../types';
import { useToast } from '../contexts/ToastContext';
import { CustomSelect } from './CustomSelect';
interface AccountsPayableViewProps {
expenses: Expense[];
setExpenses: (expenses: Expense[]) => void;
}
export const AccountsPayableView: React.FC<AccountsPayableViewProps> = ({ expenses, setExpenses }) => {
const { addToast } = useToast();
const [isModalOpen, setIsModalOpen] = useState(false);
const [filterType, setFilterType] = useState<'all' | 'fixed' | 'variable'>('all');
const [newExpense, setNewExpense] = useState<Partial<Expense>>({
type: 'fixed',
status: 'pending',
dueDate: new Date().toISOString().split('T')[0]
});
const [editingId, setEditingId] = useState<string | null>(null);
// Totals Calculation
const totalPayable = expenses.reduce((acc, curr) => acc + curr.amount, 0);
const totalPaid = expenses.filter(e => e.status === 'paid').reduce((acc, curr) => acc + curr.amount, 0);
const totalPending = expenses.filter(e => e.status === 'pending' || e.status === 'overdue').reduce((acc, curr) => acc + curr.amount, 0);
const filteredExpenses = expenses.filter(e => filterType === 'all' ? true : e.type === filterType);
const openEditModal = (expense: Expense) => {
setNewExpense(expense);
setEditingId(expense.id);
setIsModalOpen(true);
};
const openCreateModal = () => {
setNewExpense({
type: 'fixed',
status: 'pending',
dueDate: new Date().toISOString().split('T')[0]
});
setEditingId(null);
setIsModalOpen(true);
}
const handleSaveExpense = () => {
if (!newExpense.title || !newExpense.amount) {
addToast({ type: 'warning', title: 'Campos Obrigatórios', message: 'Preencha o título e valor da despesa.' });
return;
}
if (editingId) {
// Edit existing
setExpenses(expenses.map(e => e.id === editingId ? { ...newExpense, id: editingId, amount: Number(newExpense.amount) } as Expense : e));
addToast({ type: 'success', title: 'Atualizado', message: 'Despesa atualizada com sucesso.' });
} else {
// Create new
const expense: Expense = {
...newExpense,
id: Math.random().toString(36).substr(2, 9),
amount: Number(newExpense.amount)
} as Expense;
setExpenses([...expenses, expense]);
addToast({ type: 'success', title: 'Registrado', message: 'Nova despesa adicionada.' });
}
setIsModalOpen(false);
setEditingId(null);
setNewExpense({ type: 'fixed', status: 'pending', dueDate: new Date().toISOString().split('T')[0] });
};
const handleDelete = (id: string) => {
if(window.confirm("Excluir conta?")) {
setExpenses(expenses.filter(e => e.id !== id));
addToast({ type: 'info', title: 'Excluído', message: 'Despesa removida.' });
}
}
const toggleStatus = (id: string) => {
setExpenses(expenses.map(e => {
if(e.id === id) {
const newStatus = e.status === 'paid' ? 'pending' : 'paid';
if (newStatus === 'paid') addToast({ type: 'success', title: 'Pago!', message: 'Despesa marcada como paga.' });
return { ...e, status: newStatus };
}
return e;
}));
}
const inputClass = "w-full p-3 bg-white border border-slate-200 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500 outline-none text-slate-800";
return (
<div className="space-y-6 animate-fade-in">
{/* KPI Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-white p-6 rounded-[2rem] border border-slate-50 shadow-sm flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-slate-100 flex items-center justify-center text-slate-500">
<DollarSign size={24} />
</div>
<div>
<p className="text-slate-400 text-xs font-bold uppercase">Total Previsto</p>
<h3 className="text-2xl font-bold text-slate-800">R$ {totalPayable.toLocaleString('pt-BR')}</h3>
</div>
</div>
<div className="bg-white p-6 rounded-[2rem] border border-slate-50 shadow-sm flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-green-50 flex items-center justify-center text-green-600">
<CheckCircle2 size={24} />
</div>
<div>
<p className="text-slate-400 text-xs font-bold uppercase">Total Pago</p>
<h3 className="text-2xl font-bold text-green-600">R$ {totalPaid.toLocaleString('pt-BR')}</h3>
</div>
</div>
<div className="bg-white p-6 rounded-[2rem] border border-slate-50 shadow-sm flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-red-50 flex items-center justify-center text-red-600">
<AlertCircle size={24} />
</div>
<div>
<p className="text-slate-400 text-xs font-bold uppercase">A Pagar / Pendente</p>
<h3 className="text-2xl font-bold text-red-600">R$ {totalPending.toLocaleString('pt-BR')}</h3>
</div>
</div>
</div>
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
<div>
<h1 className="text-2xl font-bold text-slate-800">Contas a Pagar</h1>
<p className="text-slate-500">Gerencie despesas fixas e variáveis.</p>
</div>
<button onClick={openCreateModal} className="flex items-center gap-2 px-5 py-3 bg-red-500 text-white rounded-xl shadow-lg shadow-red-200/50 hover:bg-red-600 font-bold transition-all">
<Plus size={20} /> Nova Despesa
</button>
</div>
<div className="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden">
{/* Toolbar */}
<div className="p-4 border-b border-slate-100 flex flex-wrap gap-4 items-center">
<div className="relative flex-1 min-w-[200px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={18} />
<input
type="text"
placeholder="Buscar despesa..."
className="w-full pl-10 pr-4 py-2 bg-slate-50 border-none rounded-xl focus:ring-2 focus:ring-primary-500 outline-none text-slate-600"
/>
</div>
<div className="flex gap-2 bg-slate-50 p-1 rounded-xl">
<button
onClick={() => setFilterType('all')}
className={`px-4 py-1.5 rounded-lg text-sm font-medium transition-colors ${filterType === 'all' ? 'bg-white shadow text-slate-800' : 'text-slate-500 hover:text-slate-700'}`}
>
Todas
</button>
<button
onClick={() => setFilterType('fixed')}
className={`px-4 py-1.5 rounded-lg text-sm font-medium transition-colors ${filterType === 'fixed' ? 'bg-white shadow text-slate-800' : 'text-slate-500 hover:text-slate-700'}`}
>
Fixas
</button>
<button
onClick={() => setFilterType('variable')}
className={`px-4 py-1.5 rounded-lg text-sm font-medium transition-colors ${filterType === 'variable' ? 'bg-white shadow text-slate-800' : 'text-slate-500 hover:text-slate-700'}`}
>
Variáveis
</button>
</div>
</div>
{/* Table */}
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead className="bg-slate-50/50">
<tr>
<th className="p-4 text-xs font-bold uppercase text-slate-500">Descrição</th>
<th className="p-4 text-xs font-bold uppercase text-slate-500">Categoria</th>
<th className="p-4 text-xs font-bold uppercase text-slate-500">Vencimento</th>
<th className="p-4 text-xs font-bold uppercase text-slate-500">Valor</th>
<th className="p-4 text-xs font-bold uppercase text-slate-500 text-center">Tipo</th>
<th className="p-4 text-xs font-bold uppercase text-slate-500 text-center">Status</th>
<th className="p-4"></th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{filteredExpenses.map(expense => (
<tr key={expense.id} className="hover:bg-slate-50 transition-colors group">
<td className="p-4 font-bold text-slate-800">{expense.title}</td>
<td className="p-4">
<span className="px-2 py-1 bg-slate-100 rounded text-xs text-slate-600">{expense.category}</span>
</td>
<td className="p-4 text-sm text-slate-600">
<div className="flex items-center gap-2">
<Calendar size={14} className="text-slate-400"/>
{new Date(expense.dueDate).toLocaleDateString('pt-BR')}
</div>
</td>
<td className="p-4 font-bold text-slate-800">R$ {expense.amount.toLocaleString('pt-BR')}</td>
<td className="p-4 text-center">
<span className={`text-[10px] font-bold uppercase px-2 py-0.5 rounded ${expense.type === 'fixed' ? 'bg-blue-50 text-blue-600' : 'bg-amber-50 text-amber-600'}`}>
{expense.type === 'fixed' ? 'Fixa' : 'Variável'}
</span>
</td>
<td className="p-4 text-center">
<button onClick={() => toggleStatus(expense.id)} className={`px-3 py-1 rounded-full text-xs font-bold border transition-all ${
expense.status === 'paid' ? 'bg-green-50 text-green-600 border-green-200 hover:bg-green-100' :
expense.status === 'overdue' ? 'bg-red-50 text-red-600 border-red-200 hover:bg-red-100' :
'bg-slate-50 text-slate-500 border-slate-200 hover:bg-slate-100'
}`}>
{expense.status === 'paid' ? 'PAGO' : expense.status === 'overdue' ? 'ATRASADO' : 'PENDENTE'}
</button>
</td>
<td className="p-4 text-right">
<div className="flex justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button onClick={() => openEditModal(expense)} className="p-2 text-slate-400 hover:text-primary-500 hover:bg-primary-50 rounded-lg transition-colors">
<Pencil size={18} />
</button>
<button onClick={() => handleDelete(expense.id)} className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors">
<Trash2 size={18} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
{filteredExpenses.length === 0 && (
<div className="p-10 text-center text-slate-400">
<Sparkles size={32} className="mx-auto mb-2 opacity-20"/>
<p>Nenhuma despesa encontrada.</p>
</div>
)}
</div>
</div>
{/* Modal */}
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm" onClick={() => setIsModalOpen(false)}></div>
<div className="relative bg-white rounded-2xl shadow-xl w-full max-w-md animate-scale-up flex flex-col max-h-[90vh]">
<div className="flex justify-between items-center px-6 py-4 border-b border-slate-100 flex-shrink-0">
<h3 className="font-bold text-slate-800 text-lg">{editingId ? 'Editar Despesa' : 'Nova Despesa'}</h3>
<button onClick={() => setIsModalOpen(false)}><X size={20} className="text-slate-400 hover:text-slate-600"/></button>
</div>
<div className="p-6 space-y-4 overflow-y-auto">
<div>
<label className="block text-sm font-bold text-slate-800 mb-1">Título</label>
<input
type="text"
className={inputClass}
placeholder="Ex: Aluguel Escritório"
value={newExpense.title || ''}
onChange={e => setNewExpense({...newExpense, title: e.target.value})}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-bold text-slate-800 mb-1">Valor (R$)</label>
<input
type="number"
className={inputClass}
placeholder="0,00"
value={newExpense.amount || ''}
onChange={e => setNewExpense({...newExpense, amount: Number(e.target.value)})}
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-800 mb-1">Vencimento</label>
<input
type="date"
className={inputClass}
value={newExpense.dueDate}
onChange={e => setNewExpense({...newExpense, dueDate: e.target.value})}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-bold text-slate-800 mb-1">Categoria</label>
<CustomSelect
value={newExpense.category || 'Operacional'}
onChange={(val) => setNewExpense({...newExpense, category: val})}
options={[
{ value: 'Operacional', label: 'Operacional' },
{ value: 'Administrativo', label: 'Administrativo' },
{ value: 'Impostos', label: 'Impostos' },
{ value: 'Marketing', label: 'Marketing' },
{ value: 'Pessoal', label: 'Pessoal / Folha' },
]}
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-800 mb-1">Tipo</label>
<CustomSelect
value={newExpense.type || 'fixed'}
onChange={(val) => setNewExpense({...newExpense, type: val})}
options={[
{ value: 'fixed', label: 'Fixa' },
{ value: 'variable', label: 'Variável' },
]}
/>
</div>
</div>
<button onClick={handleSaveExpense} className="w-full py-3 bg-red-500 text-white font-bold rounded-xl mt-4 hover:bg-red-600 shadow-lg shadow-red-200">
{editingId ? 'Salvar Alterações' : 'Registrar Despesa'}
</button>
</div>
</div>
</div>
)}
</div>
);
};