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,353 @@
import React, { useState } from 'react';
import { Search, Plus, User, Briefcase, Pencil, X, Sparkles, Trash2 } from 'lucide-react';
import { Client, Service } from '../types';
import { useToast } from '../contexts/ToastContext';
import { CustomSelect } from './CustomSelect';
interface ManagementViewProps {
type: 'clients' | 'services';
clientsData: Client[];
setClientsData: (data: Client[]) => void;
servicesData: Service[];
setServicesData: (data: Service[]) => void;
}
// --- MODAIS ---
const ClientModal: React.FC<{ isOpen: boolean; onClose: () => void; client: Client | null; onSave: (client: Client) => void }> = ({ isOpen, onClose, client, onSave }) => {
const [formData, setFormData] = useState<Partial<Client>>(
client || { name: '', company: '', email: '', phone: '', address: '', status: 'active', value: 0 }
);
React.useEffect(() => {
if (client) setFormData(client);
else setFormData({ name: '', company: '', email: '', phone: '', address: '', status: 'active', value: 0 });
}, [client]);
if (!isOpen) return null;
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-700 text-sm transition-all shadow-sm placeholder:text-slate-400";
const labelClass = "block text-xs font-bold text-slate-600 mb-1.5 uppercase tracking-wide";
return (
<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={onClose}></div>
<div className="relative bg-white rounded-2xl shadow-2xl w-full max-w-lg animate-scale-up flex flex-col max-h-[90vh]">
<div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-white flex-shrink-0">
<h3 className="text-lg font-bold text-slate-800">{client ? 'Editar Cliente' : 'Novo Cliente'}</h3>
<button onClick={onClose}><X size={20} className="text-slate-400 hover:text-slate-600" /></button>
</div>
<div className="p-6 space-y-5 overflow-y-auto">
<div>
<label className={labelClass}>Nome do Contato</label>
<input type="text" value={formData.name} onChange={e => setFormData({...formData, name: e.target.value})} className={inputClass} placeholder="Ex: João Silva"/>
</div>
<div>
<label className={labelClass}>Empresa</label>
<input type="text" value={formData.company} onChange={e => setFormData({...formData, company: e.target.value})} className={inputClass} placeholder="Ex: Empresa X"/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={labelClass}>Email</label>
<input type="email" value={formData.email} onChange={e => setFormData({...formData, email: e.target.value})} className={inputClass} />
</div>
<div>
<label className={labelClass}>Telefone</label>
<input type="text" value={formData.phone} onChange={e => setFormData({...formData, phone: e.target.value})} className={inputClass} />
</div>
</div>
</div>
<div className="px-6 py-4 bg-slate-50 border-t border-slate-100 flex justify-end gap-3 flex-shrink-0 rounded-b-2xl">
<button onClick={onClose} className="px-4 py-2 text-slate-600 font-medium hover:bg-slate-200 rounded-xl transition-colors">Cancelar</button>
<button onClick={() => onSave(formData as Client)} className="px-6 py-2 bg-primary-500 text-white font-bold rounded-xl hover:bg-primary-600 shadow-lg shadow-primary-200">Salvar</button>
</div>
</div>
</div>
);
};
const ServiceModal: React.FC<{ isOpen: boolean; onClose: () => void; service: Service | null; onSave: (service: Service) => void }> = ({ isOpen, onClose, service, onSave }) => {
const [formData, setFormData] = useState<Partial<Service>>(
service || { name: '', category: 'Consultoria', price: 0, active: true, description: '', billingType: 'one-time' }
);
React.useEffect(() => {
if (service) setFormData(service);
else setFormData({ name: '', category: 'Consultoria', price: 0, active: true, description: '', billingType: 'one-time' });
}, [service]);
if (!isOpen) return null;
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-700 text-sm transition-all shadow-sm placeholder:text-slate-400";
const labelClass = "block text-xs font-bold text-slate-600 mb-1.5 uppercase tracking-wide";
return (
<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={onClose}></div>
<div className="relative bg-white rounded-2xl shadow-2xl w-full max-w-lg animate-scale-up flex flex-col max-h-[90vh]">
<div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-white flex-shrink-0">
<h3 className="text-lg font-bold text-slate-800">{service ? 'Editar Serviço' : 'Novo Serviço'}</h3>
<button onClick={onClose}><X size={20} className="text-slate-400 hover:text-slate-600" /></button>
</div>
<div className="p-6 space-y-5 overflow-y-auto">
<div>
<label className={labelClass}>Nome do Serviço</label>
<input type="text" value={formData.name} onChange={e => setFormData({...formData, name: e.target.value})} className={inputClass} placeholder="Ex: Consultoria SEO"/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={labelClass}>Preço (R$)</label>
<input type="number" value={formData.price} onChange={e => setFormData({...formData, price: Number(e.target.value)})} className={inputClass} />
</div>
<div>
<label className={labelClass}>Cobrança</label>
<CustomSelect
value={formData.billingType || 'one-time'}
onChange={(val) => setFormData({...formData, billingType: val})}
options={[
{ value: 'one-time', label: 'Pontual (Única)' },
{ value: 'recurring', label: 'Assinatura (Mensal)' }
]}
/>
</div>
</div>
<div>
<label className={labelClass}>Categoria</label>
<CustomSelect
value={formData.category || 'Consultoria'}
onChange={(val) => setFormData({...formData, category: val})}
options={[
{ value: 'Consultoria', label: 'Consultoria' },
{ value: 'TI', label: 'TI / Desenvolvimento' },
{ value: 'Marketing', label: 'Marketing / Design' },
{ value: 'Outro', label: 'Outro' }
]}
/>
</div>
<div>
<label className={labelClass}>Descrição</label>
<textarea rows={3} value={formData.description} onChange={e => setFormData({...formData, description: e.target.value})} className={inputClass} placeholder="O que está incluso..."></textarea>
</div>
</div>
<div className="px-6 py-4 bg-slate-50 border-t border-slate-100 flex justify-end gap-3 flex-shrink-0 rounded-b-2xl">
<button onClick={onClose} className="px-4 py-2 text-slate-600 font-medium hover:bg-slate-200 rounded-xl transition-colors">Cancelar</button>
<button onClick={() => onSave(formData as Service)} className="px-6 py-2 bg-primary-500 text-white font-bold rounded-xl hover:bg-primary-600 shadow-lg shadow-primary-200">Salvar</button>
</div>
</div>
</div>
);
};
export const ManagementView: React.FC<ManagementViewProps> = ({ type, clientsData, setClientsData, servicesData, setServicesData }) => {
const { addToast } = useToast();
const [isClientModalOpen, setIsClientModalOpen] = useState(false);
const [editingClient, setEditingClient] = useState<Client | null>(null);
const [isServiceModalOpen, setIsServiceModalOpen] = useState(false);
const [editingService, setEditingService] = useState<Service | null>(null);
const [searchTerm, setSearchTerm] = useState('');
// --- SERVICE ACTIONS ---
const handleSaveService = (serviceData: Service) => {
if (!serviceData.name || serviceData.price < 0) {
addToast({ type: 'warning', title: 'Dados Inválidos', message: 'Verifique o nome e o preço.' });
return;
}
if (editingService) {
setServicesData(servicesData.map(s => s.id === serviceData.id ? serviceData : s));
addToast({ type: 'success', title: 'Serviço Atualizado' });
} else {
const newService = { ...serviceData, id: Math.random().toString(36).substr(2, 9) };
setServicesData([...servicesData, newService]);
addToast({ type: 'success', title: 'Serviço Criado' });
}
setIsServiceModalOpen(false);
setEditingService(null);
};
const handleDeleteService = (id: string) => {
if (window.confirm('Excluir este serviço?')) {
setServicesData(servicesData.filter(s => s.id !== id));
addToast({ type: 'info', title: 'Serviço Removido' });
}
};
// --- CLIENT ACTIONS ---
const handleSaveClient = (clientData: Client) => {
if (!clientData.name) {
addToast({ type: 'warning', title: 'Nome Obrigatório' });
return;
}
if(editingClient) {
setClientsData(clientsData.map(c => c.id === clientData.id ? clientData : c));
addToast({ type: 'success', title: 'Cliente Atualizado' });
} else {
setClientsData([...clientsData, {...clientData, id: Math.random().toString().substr(2, 9)}]);
addToast({ type: 'success', title: 'Cliente Cadastrado' });
}
setIsClientModalOpen(false);
setEditingClient(null);
};
const handleDeleteClient = (id: string) => {
if (window.confirm('Excluir este cliente?')) {
setClientsData(clientsData.filter(c => c.id !== id));
addToast({ type: 'info', title: 'Cliente Removido' });
}
};
// --- FILTERING ---
const filteredServices = servicesData.filter(s => s.name.toLowerCase().includes(searchTerm.toLowerCase()) || s.category.toLowerCase().includes(searchTerm.toLowerCase()));
const filteredClients = clientsData.filter(c => c.name.toLowerCase().includes(searchTerm.toLowerCase()) || c.company.toLowerCase().includes(searchTerm.toLowerCase()));
return (
<div className="space-y-6 animate-fade-in relative pb-10">
<ClientModal
isOpen={isClientModalOpen}
onClose={() => setIsClientModalOpen(false)}
client={editingClient}
onSave={handleSaveClient}
/>
<ServiceModal
isOpen={isServiceModalOpen}
onClose={() => setIsServiceModalOpen(false)}
service={editingService}
onSave={handleSaveService}
/>
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
<div>
<h1 className="text-2xl font-bold text-slate-800">
{type === 'clients' ? 'Base de Clientes (Legado)' : 'Catálogo de Serviços'}
</h1>
<p className="text-slate-500">
{type === 'clients' ? 'Gestão simples de contatos.' : 'Gerencie preços e portfólio.'}
</p>
</div>
<button
onClick={() => {
if (type === 'clients') { setEditingClient(null); setIsClientModalOpen(true); }
else { setEditingService(null); setIsServiceModalOpen(true); }
}}
className="flex items-center gap-2 px-5 py-2.5 bg-primary-500 text-white rounded-xl hover:bg-primary-600 shadow-lg shadow-primary-200/50 transition-all font-bold"
>
<Plus size={18} />
{type === 'clients' ? 'Novo Cliente' : 'Novo Serviço'}
</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">
<div className="relative max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={18} />
<input
type="text"
placeholder={type === 'clients' ? "Buscar cliente..." : "Buscar serviço..."}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 bg-slate-50 border-none rounded-xl focus:ring-2 focus:ring-primary-500 outline-none text-slate-600"
/>
</div>
</div>
{/* Table Content */}
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead className="bg-slate-50/50">
<tr>
<th className="p-4 text-xs font-bold uppercase text-slate-500 tracking-wide">{type === 'clients' ? 'Nome / Empresa' : 'Serviço'}</th>
<th className="p-4 text-xs font-bold uppercase text-slate-500 tracking-wide">{type === 'clients' ? 'Contato' : 'Preço'}</th>
<th className="p-4 text-xs font-bold uppercase text-slate-500 tracking-wide">{type === 'clients' ? 'Status' : 'Tipo / Categoria'}</th>
<th className="p-4 text-right"></th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{type === 'services' && filteredServices.map(service => (
<tr key={service.id} className="hover:bg-slate-50 transition-colors group">
<td className="p-4">
<div className="font-bold text-slate-800">{service.name}</div>
{service.description && <div className="text-xs text-slate-400 line-clamp-1">{service.description}</div>}
</td>
<td className="p-4 font-bold text-slate-700">R$ {service.price.toLocaleString('pt-BR')}</td>
<td className="p-4">
<div className="flex gap-2">
<span className={`px-2 py-1 rounded text-[10px] font-bold uppercase tracking-wide ${service.billingType === 'recurring' ? 'bg-indigo-50 text-indigo-600' : 'bg-slate-100 text-slate-600'}`}>
{service.billingType === 'recurring' ? 'Assinatura' : 'Pontual'}
</span>
<span className="px-2 py-1 rounded text-[10px] font-bold uppercase tracking-wide bg-slate-100 text-slate-500">
{service.category}
</span>
</div>
</td>
<td className="p-4 text-right">
<div className="flex justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button onClick={() => { setEditingService(service); setIsServiceModalOpen(true); }} className="p-2 text-slate-400 hover:text-primary-500 hover:bg-primary-50 rounded-lg transition-colors">
<Pencil size={18} />
</button>
<button onClick={() => handleDeleteService(service.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>
))}
{type === 'clients' && filteredClients.map(client => (
<tr key={client.id} className="hover:bg-slate-50 transition-colors group">
<td className="p-4">
<div className="font-bold text-slate-800">{client.name}</div>
<div className="text-xs text-slate-400">{client.company}</div>
</td>
<td className="p-4 text-sm text-slate-600">
<div>{client.email}</div>
<div className="text-xs text-slate-400">{client.phone}</div>
</td>
<td className="p-4">
<span className="px-2 py-1 rounded text-[10px] font-bold uppercase bg-green-50 text-green-600">
Ativo
</span>
</td>
<td className="p-4 text-right">
<div className="flex justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button onClick={() => { setEditingClient(client); setIsClientModalOpen(true); }} className="p-2 text-slate-400 hover:text-primary-500 hover:bg-primary-50 rounded-lg transition-colors">
<Pencil size={18} />
</button>
<button onClick={() => handleDeleteClient(client.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>
{/* Empty State */}
{((type === 'services' && filteredServices.length === 0) || (type === 'clients' && filteredClients.length === 0)) && (
<div className="p-10 text-center text-slate-400 flex flex-col items-center">
<Sparkles size={32} className="mb-2 opacity-20"/>
<p>Nenhum registro encontrado.</p>
<button
onClick={() => {
if (type === 'clients') { setEditingClient(null); setIsClientModalOpen(true); }
else { setEditingService(null); setIsServiceModalOpen(true); }
}}
className="mt-4 text-sm font-bold text-primary-500 hover:underline"
>
Criar primeiro registro
</button>
</div>
)}
</div>
</div>
</div>
);
};