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.
649 lines
37 KiB
TypeScript
649 lines
37 KiB
TypeScript
|
|
import React, { useState, useRef } from 'react';
|
|
import {
|
|
Building2, Phone, Mail, MapPin, Globe, Calendar,
|
|
FileText, Users, Plus, ArrowLeft, MoreHorizontal,
|
|
Download, Search, Filter, CheckCircle, Briefcase, ExternalLink, X, Save, UploadCloud, DollarSign, MessageCircle, AlertTriangle, Pencil, Camera, Trash2, ChevronDown
|
|
} from 'lucide-react';
|
|
import { Company, ContactPerson, CompanyDocument, Service } from '../types';
|
|
import { useToast } from '../contexts/ToastContext';
|
|
|
|
interface CRMViewProps {
|
|
companies: Company[];
|
|
setCompanies: (companies: Company[]) => void;
|
|
availableServices: Service[];
|
|
}
|
|
|
|
// Simple Modal (Reused)
|
|
const Modal: React.FC<{isOpen: boolean; onClose: () => void; title: string; children: React.ReactNode; maxWidth?: string}> = ({ isOpen, onClose, title, children, maxWidth = 'max-w-lg' }) => {
|
|
if (!isOpen) return null;
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
<div className="absolute inset-0 bg-slate-900/50 backdrop-blur-sm" onClick={onClose}></div>
|
|
<div className={`relative bg-white rounded-2xl shadow-xl w-full ${maxWidth} overflow-hidden animate-fade-in flex flex-col max-h-[90vh]`}>
|
|
<div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center flex-shrink-0">
|
|
<h3 className="font-bold text-slate-800 text-lg">{title}</h3>
|
|
<button onClick={onClose} className="text-slate-400 hover:text-slate-600"><X size={20}/></button>
|
|
</div>
|
|
<div className="p-6 overflow-y-auto">{children}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const CRMView: React.FC<CRMViewProps> = ({ companies, setCompanies, availableServices }) => {
|
|
const { addToast } = useToast();
|
|
const [viewMode, setViewMode] = useState<'list' | 'detail'>('list');
|
|
const [selectedCompanyId, setSelectedCompanyId] = useState<string | null>(null);
|
|
const [activeTab, setActiveTab] = useState<'overview' | 'contacts' | 'documents' | 'services'>('overview');
|
|
|
|
// Modal States
|
|
const [isCompanyModalOpen, setIsCompanyModalOpen] = useState(false);
|
|
const [isContactModalOpen, setIsContactModalOpen] = useState(false);
|
|
const [isServiceModalOpen, setIsServiceModalOpen] = useState(false);
|
|
|
|
// Editing States
|
|
const [isEditingOverview, setIsEditingOverview] = useState(false);
|
|
const [editingContactId, setEditingContactId] = useState<string | null>(null);
|
|
|
|
// Forms State
|
|
const [newCompany, setNewCompany] = useState<Partial<Company>>({ status: 'active', industry: 'Outro', logo: '' });
|
|
const [overviewForm, setOverviewForm] = useState<Partial<Company>>({});
|
|
const [contactForm, setContactForm] = useState<Partial<ContactPerson>>({});
|
|
const [selectedServiceId, setSelectedServiceId] = useState('');
|
|
|
|
// Refs for File Uploads
|
|
const logoInputRef = useRef<HTMLInputElement>(null);
|
|
const avatarInputRef = useRef<HTMLInputElement>(null);
|
|
const docInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const selectedCompany = companies.find(c => c.id === selectedCompanyId);
|
|
const clientRevenue = selectedCompany ? selectedCompany.activeServices.reduce((acc, s) => acc + s.price, 0) : 0;
|
|
|
|
// --- HELPER: FILE TO BASE64 ---
|
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>, callback: (base64: string) => void) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
callback(reader.result as string);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
};
|
|
|
|
// --- ACTIONS ---
|
|
|
|
const handleSaveCompany = () => {
|
|
if (!newCompany.name) {
|
|
addToast({ type: 'warning', title: 'Campos Obrigatórios', message: 'Por favor, informe ao menos a Razão Social.' });
|
|
return;
|
|
}
|
|
const company: Company = {
|
|
...newCompany,
|
|
id: Math.random().toString(36).substr(2, 9),
|
|
logo: newCompany.logo || `https://ui-avatars.com/api/?name=${newCompany.name}&background=random`,
|
|
contacts: [],
|
|
documents: [],
|
|
activeServices: []
|
|
} as Company;
|
|
setCompanies([...companies, company]);
|
|
setIsCompanyModalOpen(false);
|
|
setNewCompany({ status: 'active', industry: 'Outro', logo: '' });
|
|
addToast({ type: 'success', title: 'Empresa Cadastrada', message: `${company.name} foi adicionada com sucesso.` });
|
|
};
|
|
|
|
const handleUpdateOverview = () => {
|
|
if (!selectedCompany || !overviewForm.name) return;
|
|
|
|
const updatedCompanies = companies.map(c => {
|
|
if (c.id === selectedCompany.id) {
|
|
return { ...c, ...overviewForm };
|
|
}
|
|
return c;
|
|
});
|
|
setCompanies(updatedCompanies);
|
|
setIsEditingOverview(false);
|
|
addToast({ type: 'success', title: 'Dados Atualizados', message: 'As informações da empresa foram salvas.' });
|
|
};
|
|
|
|
const openContactModal = (contact?: ContactPerson) => {
|
|
if (contact) {
|
|
setEditingContactId(contact.id);
|
|
setContactForm(contact);
|
|
} else {
|
|
setEditingContactId(null);
|
|
setContactForm({ avatar: '' });
|
|
}
|
|
setIsContactModalOpen(true);
|
|
};
|
|
|
|
const handleSaveContact = () => {
|
|
if (!selectedCompany || !contactForm.name) {
|
|
addToast({ type: 'warning', title: 'Nome Obrigatório', message: 'Informe o nome do responsável.' });
|
|
return;
|
|
}
|
|
|
|
let updatedContacts = [...selectedCompany.contacts];
|
|
|
|
if (editingContactId) {
|
|
// Edit existing
|
|
updatedContacts = updatedContacts.map(c => c.id === editingContactId ? { ...c, ...contactForm } as ContactPerson : c);
|
|
} else {
|
|
// Create new
|
|
const newContact: ContactPerson = {
|
|
...contactForm,
|
|
id: Math.random().toString(36).substr(2, 9),
|
|
avatar: contactForm.avatar || `https://ui-avatars.com/api/?name=${contactForm.name}&background=random`
|
|
} as ContactPerson;
|
|
updatedContacts.push(newContact);
|
|
}
|
|
|
|
const updatedCompanies = companies.map(c => {
|
|
if (c.id === selectedCompany.id) {
|
|
return { ...c, contacts: updatedContacts };
|
|
}
|
|
return c;
|
|
});
|
|
setCompanies(updatedCompanies);
|
|
setIsContactModalOpen(false);
|
|
setContactForm({});
|
|
setEditingContactId(null);
|
|
addToast({ type: 'success', title: 'Responsável Salvo', message: 'Lista de contatos atualizada.' });
|
|
};
|
|
|
|
const handleDeleteContact = (contactId: string) => {
|
|
if (!selectedCompany || !window.confirm("Excluir responsável?")) return;
|
|
const updatedCompanies = companies.map(c => {
|
|
if (c.id === selectedCompany.id) {
|
|
return { ...c, contacts: c.contacts.filter(ct => ct.id !== contactId) };
|
|
}
|
|
return c;
|
|
});
|
|
setCompanies(updatedCompanies);
|
|
addToast({ type: 'info', title: 'Responsável Removido' });
|
|
};
|
|
|
|
const handleUploadDocument = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file || !selectedCompany) return;
|
|
|
|
const newDoc: CompanyDocument = {
|
|
id: Math.random().toString(36).substr(2, 9),
|
|
title: file.name,
|
|
type: file.name.endsWith('.pdf') ? 'briefing' : 'other',
|
|
date: new Date().toLocaleDateString('pt-BR'),
|
|
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`
|
|
};
|
|
|
|
const updatedCompanies = companies.map(c => {
|
|
if (c.id === selectedCompany.id) {
|
|
return { ...c, documents: [...c.documents, newDoc] };
|
|
}
|
|
return c;
|
|
});
|
|
setCompanies(updatedCompanies);
|
|
addToast({ type: 'success', title: 'Upload Concluído', message: 'Arquivo anexado com sucesso.' });
|
|
};
|
|
|
|
const handleAddService = () => {
|
|
if (!selectedCompany || !selectedServiceId) return;
|
|
const serviceToAdd = availableServices.find(s => s.id === selectedServiceId);
|
|
if (!serviceToAdd) return;
|
|
|
|
const updatedCompanies = companies.map(c => {
|
|
if (c.id === selectedCompany.id) {
|
|
return { ...c, activeServices: [...c.activeServices, serviceToAdd] };
|
|
}
|
|
return c;
|
|
});
|
|
setCompanies(updatedCompanies);
|
|
setIsServiceModalOpen(false);
|
|
setSelectedServiceId('');
|
|
addToast({ type: 'success', title: 'Serviço Adicionado', message: 'Contrato atualizado.' });
|
|
};
|
|
|
|
const handleRemoveService = (index: number) => {
|
|
if (!selectedCompany) return;
|
|
const updatedCompanies = companies.map(c => {
|
|
if (c.id === selectedCompany.id) {
|
|
const newServices = [...c.activeServices];
|
|
newServices.splice(index, 1);
|
|
return { ...c, activeServices: newServices };
|
|
}
|
|
return c;
|
|
});
|
|
setCompanies(updatedCompanies);
|
|
addToast({ type: 'info', title: 'Serviço Removido' });
|
|
};
|
|
|
|
const openWhatsApp = (phone: string) => {
|
|
const cleanPhone = phone.replace(/\D/g, '');
|
|
window.open(`https://wa.me/55${cleanPhone}`, '_blank');
|
|
};
|
|
|
|
// Styles
|
|
const inputClass = "w-full p-3 bg-white border border-slate-200 rounded-xl focus:ring-2 focus:ring-primary-200 outline-none text-slate-800 text-sm";
|
|
const selectClass = "w-full bg-white border border-slate-200 rounded-xl px-4 py-3 pr-10 text-sm font-medium text-slate-700 outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 appearance-none transition-all cursor-pointer shadow-sm";
|
|
const labelClass = "block text-xs font-bold text-slate-700 mb-1 uppercase tracking-wide";
|
|
|
|
// --- VIEWS ---
|
|
|
|
if (viewMode === 'list') {
|
|
return (
|
|
<div className="space-y-8 animate-fade-in">
|
|
<Modal isOpen={isCompanyModalOpen} onClose={() => setIsCompanyModalOpen(false)} title="Nova Empresa" maxWidth="max-w-2xl">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
{/* Logo Upload */}
|
|
<div className="col-span-2 flex flex-col items-center justify-center mb-4">
|
|
<div
|
|
className="w-24 h-24 rounded-2xl bg-slate-100 border-2 border-dashed border-slate-300 flex items-center justify-center cursor-pointer overflow-hidden hover:border-primary-400 transition-colors relative group"
|
|
onClick={() => logoInputRef.current?.click()}
|
|
>
|
|
{newCompany.logo ? (
|
|
<img src={newCompany.logo} alt="Logo" className="w-full h-full object-cover" />
|
|
) : (
|
|
<Camera className="text-slate-400 group-hover:text-primary-500" size={32} />
|
|
)}
|
|
<div className="absolute inset-0 bg-black/50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<span className="text-white text-xs font-bold">Alterar</span>
|
|
</div>
|
|
</div>
|
|
<span className="text-xs text-slate-400 mt-2">Clique para adicionar logo</span>
|
|
<input
|
|
type="file"
|
|
ref={logoInputRef}
|
|
className="hidden"
|
|
accept="image/*"
|
|
onChange={(e) => handleFileChange(e, (base64) => setNewCompany({...newCompany, logo: base64}))}
|
|
/>
|
|
</div>
|
|
|
|
<div className="col-span-2">
|
|
<label className={labelClass}>Razão Social</label>
|
|
<input type="text" className={inputClass} value={newCompany.name || ''} onChange={e => setNewCompany({...newCompany, name: e.target.value})} placeholder="Ex: Tech Solutions LTDA" />
|
|
</div>
|
|
<div className="col-span-1">
|
|
<label className={labelClass}>Nome Fantasia</label>
|
|
<input type="text" className={inputClass} value={newCompany.fantasyName || ''} onChange={e => setNewCompany({...newCompany, fantasyName: e.target.value})} placeholder="Ex: Tech Sol" />
|
|
</div>
|
|
<div className="col-span-1">
|
|
<label className={labelClass}>CNPJ</label>
|
|
<input type="text" className={inputClass} value={newCompany.cnpj || ''} onChange={e => setNewCompany({...newCompany, cnpj: e.target.value})} placeholder="00.000.000/0001-00" />
|
|
</div>
|
|
<div className="col-span-1">
|
|
<label className={labelClass}>Inscrição Estadual</label>
|
|
<input type="text" className={inputClass} value={newCompany.ie || ''} onChange={e => setNewCompany({...newCompany, ie: e.target.value})} />
|
|
</div>
|
|
<div className="col-span-1">
|
|
<label className={labelClass}>Cidade - UF</label>
|
|
<input type="text" className={inputClass} value={newCompany.city || ''} onChange={e => setNewCompany({...newCompany, city: e.target.value})} />
|
|
</div>
|
|
<div className="col-span-1">
|
|
<label className={labelClass}>Telefone Geral</label>
|
|
<input type="text" className={inputClass} value={newCompany.phone || ''} onChange={e => setNewCompany({...newCompany, phone: e.target.value})} />
|
|
</div>
|
|
<div className="col-span-1">
|
|
<label className={labelClass}>Email Geral</label>
|
|
<input type="email" className={inputClass} value={newCompany.email || ''} onChange={e => setNewCompany({...newCompany, email: e.target.value})} />
|
|
</div>
|
|
<div className="col-span-2">
|
|
<label className={labelClass}>Endereço Completo</label>
|
|
<input type="text" className={inputClass} value={newCompany.address || ''} onChange={e => setNewCompany({...newCompany, address: e.target.value})} />
|
|
</div>
|
|
<div className="col-span-1">
|
|
<label className={labelClass}>Status</label>
|
|
<div className="relative">
|
|
<select className={selectClass} value={newCompany.status} onChange={e => setNewCompany({...newCompany, status: e.target.value as any})}>
|
|
<option value="active">Ativo</option>
|
|
<option value="pending">Pendente</option>
|
|
<option value="inactive">Inativo</option>
|
|
<option value="overdue">Em Atraso</option>
|
|
</select>
|
|
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none" size={18} />
|
|
</div>
|
|
</div>
|
|
<div className="col-span-2 pt-4">
|
|
<button onClick={handleSaveCompany} className="w-full py-3 bg-primary-500 text-white font-bold rounded-xl shadow-lg hover:bg-primary-600 transition-colors">Cadastrar Empresa</button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
|
|
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-slate-800">Clientes & Empresas</h1>
|
|
<p className="text-slate-500">Gerencie contratos, responsáveis e documentos.</p>
|
|
</div>
|
|
<button onClick={() => setIsCompanyModalOpen(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} /> Nova Empresa
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
|
{companies.map((company) => {
|
|
const totalValue = company.activeServices.reduce((acc, s) => acc + s.price, 0);
|
|
return (
|
|
<div
|
|
key={company.id}
|
|
onClick={() => { setSelectedCompanyId(company.id); setViewMode('detail'); }}
|
|
className={`group bg-white p-6 rounded-[2rem] shadow-sm hover:shadow-lg hover:-translate-y-1 transition-all border cursor-pointer relative overflow-hidden ${company.status === 'overdue' ? 'border-red-200 ring-1 ring-red-100' : 'border-slate-50'}`}
|
|
>
|
|
{company.status === 'overdue' && (
|
|
<div className="absolute top-0 right-0 bg-red-500 text-white text-[10px] font-bold px-3 py-1 rounded-bl-xl">
|
|
EM ATRASO
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex gap-4 mb-4 mt-2">
|
|
<div className="w-16 h-16 rounded-2xl bg-slate-100 flex items-center justify-center text-slate-400 font-bold text-xl uppercase overflow-hidden border border-slate-100">
|
|
{company.logo ? <img src={company.logo} className="w-full h-full object-cover"/> : company.name.substring(0,2)}
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-slate-800 text-lg group-hover:text-primary-500 line-clamp-1">{company.fantasyName || company.name}</h3>
|
|
<p className="text-xs text-slate-400">{company.city || 'Local não informado'}</p>
|
|
<div className="mt-1 flex gap-2">
|
|
{company.status === 'active' && <span className="inline-block px-2 py-0.5 bg-green-50 text-green-600 rounded text-[10px] font-bold uppercase">Ativo</span>}
|
|
{company.status === 'pending' && <span className="inline-block px-2 py-0.5 bg-amber-50 text-amber-600 rounded text-[10px] font-bold uppercase">Pendente</span>}
|
|
{company.status === 'overdue' && <span className="inline-block px-2 py-0.5 bg-red-50 text-red-600 rounded text-[10px] font-bold uppercase">Financeiro Pendente</span>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="border-t border-slate-50 pt-4 flex justify-between items-center">
|
|
<span className="text-sm text-slate-500 flex items-center gap-1"><Users size={14}/> {company.contacts.length} Resp.</span>
|
|
<span className="font-bold text-slate-800 text-sm">R$ {totalValue.toLocaleString('pt-BR')} /mês</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// DETALHE DA EMPRESA
|
|
if (selectedCompany) {
|
|
return (
|
|
<div className="space-y-6 animate-fade-in pb-10">
|
|
|
|
{/* Modal Contato (Novo/Edição) */}
|
|
<Modal isOpen={isContactModalOpen} onClose={() => setIsContactModalOpen(false)} title={editingContactId ? "Editar Responsável" : "Novo Responsável"}>
|
|
<div className="space-y-4">
|
|
<div className="flex justify-center mb-4">
|
|
<div
|
|
className="w-20 h-20 rounded-full bg-slate-100 border-2 border-dashed border-slate-300 flex items-center justify-center cursor-pointer overflow-hidden hover:border-primary-400 relative group"
|
|
onClick={() => avatarInputRef.current?.click()}
|
|
>
|
|
{contactForm.avatar ? (
|
|
<img src={contactForm.avatar} alt="Avatar" className="w-full h-full object-cover" />
|
|
) : (
|
|
<Camera className="text-slate-400 group-hover:text-primary-500" size={24} />
|
|
)}
|
|
<div className="absolute inset-0 bg-black/50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity rounded-full">
|
|
<span className="text-white text-[10px] font-bold">Foto</span>
|
|
</div>
|
|
</div>
|
|
<input
|
|
type="file"
|
|
ref={avatarInputRef}
|
|
className="hidden"
|
|
accept="image/*"
|
|
onChange={(e) => handleFileChange(e, (base64) => setContactForm({...contactForm, avatar: base64}))}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className={labelClass}>Nome Completo</label>
|
|
<input type="text" className={inputClass} value={contactForm.name || ''} onChange={e => setContactForm({...contactForm, name: e.target.value})} />
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>Cargo / Função</label>
|
|
<input type="text" className={inputClass} value={contactForm.role || ''} onChange={e => setContactForm({...contactForm, role: e.target.value})} />
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>Email</label>
|
|
<input type="email" className={inputClass} value={contactForm.email || ''} onChange={e => setContactForm({...contactForm, email: e.target.value})} />
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>Telefone / WhatsApp</label>
|
|
<input type="text" className={inputClass} value={contactForm.phone || ''} onChange={e => setContactForm({...contactForm, phone: e.target.value})} placeholder="(00) 00000-0000" />
|
|
</div>
|
|
<button onClick={handleSaveContact} className="w-full py-3 bg-primary-500 text-white font-bold rounded-xl mt-4">Salvar Responsável</button>
|
|
</div>
|
|
</Modal>
|
|
|
|
{/* Modal Adicionar Serviço */}
|
|
<Modal isOpen={isServiceModalOpen} onClose={() => setIsServiceModalOpen(false)} title="Adicionar Serviço">
|
|
<div className="space-y-4">
|
|
<label className={labelClass}>Selecione o Serviço</label>
|
|
<div className="relative">
|
|
<select className={selectClass} value={selectedServiceId} onChange={e => setSelectedServiceId(e.target.value)}>
|
|
<option value="">Selecione...</option>
|
|
{availableServices.map(s => <option key={s.id} value={s.id}>{s.name} - R$ {s.price}</option>)}
|
|
</select>
|
|
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none" size={18} />
|
|
</div>
|
|
<button onClick={handleAddService} disabled={!selectedServiceId} className="w-full py-3 bg-primary-500 text-white font-bold rounded-xl mt-4 disabled:opacity-50">Adicionar</button>
|
|
</div>
|
|
</Modal>
|
|
|
|
{/* Header Detalhe */}
|
|
<div className="flex items-center gap-4">
|
|
<button onClick={() => setViewMode('list')} className="p-2 bg-white border border-slate-200 rounded-xl hover:bg-slate-50 transition-colors">
|
|
<ArrowLeft size={20} />
|
|
</button>
|
|
<div className="flex-1">
|
|
<h1 className="text-2xl font-bold text-slate-800">{selectedCompany.fantasyName || selectedCompany.name}</h1>
|
|
<p className="text-sm text-slate-500">{selectedCompany.name} - {selectedCompany.cnpj}</p>
|
|
</div>
|
|
{selectedCompany.status === 'overdue' && (
|
|
<div className="px-4 py-2 bg-red-100 text-red-700 rounded-xl font-bold flex items-center gap-2 animate-pulse">
|
|
<AlertTriangle size={18} /> CLIENTE EM ATRASO
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="flex border-b border-slate-200">
|
|
{['overview', 'contacts', 'documents', 'services'].map(tab => (
|
|
<button
|
|
key={tab}
|
|
onClick={() => setActiveTab(tab as any)}
|
|
className={`px-6 py-3 text-sm font-bold border-b-2 transition-colors ${
|
|
activeTab === tab
|
|
? 'border-primary-500 text-primary-600'
|
|
: 'border-transparent text-slate-500 hover:text-slate-700'
|
|
}`}
|
|
>
|
|
{tab === 'overview' && 'Visão Geral'}
|
|
{tab === 'contacts' && 'Responsáveis'}
|
|
{tab === 'documents' && 'Arquivos'}
|
|
{tab === 'services' && 'Serviços'}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Tab Content */}
|
|
<div className="animate-fade-in">
|
|
{activeTab === 'overview' && (
|
|
<div className="space-y-6">
|
|
<div className="flex justify-end">
|
|
{!isEditingOverview ? (
|
|
<button onClick={() => { setOverviewForm(selectedCompany); setIsEditingOverview(true); }} className="flex items-center gap-2 px-4 py-2 text-primary-600 bg-primary-50 hover:bg-primary-100 rounded-xl font-bold text-sm transition-colors">
|
|
<Pencil size={16} /> Editar Dados
|
|
</button>
|
|
) : (
|
|
<div className="flex gap-2">
|
|
<button onClick={() => setIsEditingOverview(false)} className="px-4 py-2 text-slate-500 hover:text-slate-700 font-medium">Cancelar</button>
|
|
<button onClick={handleUpdateOverview} className="flex items-center gap-2 px-4 py-2 bg-green-500 text-white rounded-xl font-bold hover:bg-green-600 shadow-lg shadow-green-200">
|
|
<Save size={16} /> Salvar Alterações
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="bg-white p-6 rounded-[2rem] border border-slate-50 shadow-sm space-y-4">
|
|
<h3 className="font-bold text-slate-800 text-lg border-b border-slate-50 pb-2">Dados Cadastrais</h3>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<span className="block text-xs text-slate-400">CNPJ</span>
|
|
{isEditingOverview ? <input className={inputClass} value={overviewForm.cnpj || ''} onChange={e => setOverviewForm({...overviewForm, cnpj: e.target.value})} /> : <span className="text-sm font-medium text-slate-700">{selectedCompany.cnpj || '-'}</span>}
|
|
</div>
|
|
<div>
|
|
<span className="block text-xs text-slate-400">Inscrição Estadual</span>
|
|
{isEditingOverview ? <input className={inputClass} value={overviewForm.ie || ''} onChange={e => setOverviewForm({...overviewForm, ie: e.target.value})} /> : <span className="text-sm font-medium text-slate-700">{selectedCompany.ie || 'Isento'}</span>}
|
|
</div>
|
|
<div className="col-span-2">
|
|
<span className="block text-xs text-slate-400">Razão Social</span>
|
|
{isEditingOverview ? <input className={inputClass} value={overviewForm.name || ''} onChange={e => setOverviewForm({...overviewForm, name: e.target.value})} /> : <span className="text-sm font-medium text-slate-700">{selectedCompany.name}</span>}
|
|
</div>
|
|
<div className="col-span-2">
|
|
<span className="block text-xs text-slate-400">Endereço</span>
|
|
{isEditingOverview ? (
|
|
<div className="grid grid-cols-3 gap-2">
|
|
<input className={`${inputClass} col-span-2`} value={overviewForm.address || ''} onChange={e => setOverviewForm({...overviewForm, address: e.target.value})} placeholder="Rua..." />
|
|
<input className={inputClass} value={overviewForm.city || ''} onChange={e => setOverviewForm({...overviewForm, city: e.target.value})} placeholder="Cidade..." />
|
|
</div>
|
|
) : (
|
|
<span className="text-sm font-medium text-slate-700">{selectedCompany.address}, {selectedCompany.city}</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="bg-white p-6 rounded-[2rem] border border-slate-50 shadow-sm space-y-4">
|
|
<h3 className="font-bold text-slate-800 text-lg border-b border-slate-50 pb-2">Contato Geral</h3>
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-3">
|
|
<Phone size={18} className="text-slate-400"/>
|
|
{isEditingOverview ? <input className={inputClass} value={overviewForm.phone || ''} onChange={e => setOverviewForm({...overviewForm, phone: e.target.value})} /> : <span className="text-sm text-slate-700">{selectedCompany.phone}</span>}
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Mail size={18} className="text-slate-400"/>
|
|
{isEditingOverview ? <input className={inputClass} value={overviewForm.email || ''} onChange={e => setOverviewForm({...overviewForm, email: e.target.value})} /> : <span className="text-sm text-slate-700">{selectedCompany.email}</span>}
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Globe size={18} className="text-slate-400"/>
|
|
{isEditingOverview ? <input className={inputClass} value={overviewForm.website || ''} onChange={e => setOverviewForm({...overviewForm, website: e.target.value})} placeholder="Website" /> : <span className="text-sm text-slate-700">{selectedCompany.website || 'Sem site'}</span>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'contacts' && (
|
|
<div className="space-y-4">
|
|
<div className="flex justify-end">
|
|
<button onClick={() => openContactModal()} className="flex items-center gap-2 px-4 py-2 bg-slate-800 text-white rounded-xl text-sm font-bold hover:bg-slate-900"><Plus size={16}/> Adicionar Responsável</button>
|
|
</div>
|
|
{selectedCompany.contacts.length === 0 ? (
|
|
<div className="text-center py-10 text-slate-400 bg-white rounded-[2rem] border border-dashed border-slate-200">
|
|
Nenhum responsável cadastrado.
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{selectedCompany.contacts.map(contact => (
|
|
<div key={contact.id} className="bg-white p-4 rounded-2xl border border-slate-100 flex items-center justify-between shadow-sm group">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center font-bold text-slate-500 overflow-hidden border border-slate-200">
|
|
{contact.avatar ? <img src={contact.avatar} className="w-full h-full object-cover"/> : contact.name.charAt(0)}
|
|
</div>
|
|
<div>
|
|
<h4 className="font-bold text-slate-800">{contact.name}</h4>
|
|
<p className="text-xs text-slate-500">{contact.role}</p>
|
|
<p className="text-xs text-slate-400 mt-1">{contact.email}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button onClick={() => openContactModal(contact)} className="w-8 h-8 flex items-center justify-center rounded-lg text-slate-400 hover:bg-slate-100 hover:text-slate-600 transition-colors">
|
|
<Pencil size={16} />
|
|
</button>
|
|
<button onClick={() => handleDeleteContact(contact.id)} className="w-8 h-8 flex items-center justify-center rounded-lg text-slate-400 hover:bg-red-50 hover:text-red-500 transition-colors">
|
|
<Trash2 size={16} />
|
|
</button>
|
|
<button
|
|
onClick={() => openWhatsApp(contact.phone)}
|
|
className="w-8 h-8 rounded-full bg-green-50 text-green-600 flex items-center justify-center hover:bg-green-500 hover:text-white transition-colors"
|
|
title={`WhatsApp: ${contact.phone}`}
|
|
>
|
|
<MessageCircle size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'documents' && (
|
|
<div className="space-y-6">
|
|
<input
|
|
type="file"
|
|
ref={docInputRef}
|
|
className="hidden"
|
|
onChange={handleUploadDocument}
|
|
/>
|
|
<div
|
|
onClick={() => docInputRef.current?.click()}
|
|
className="bg-slate-50 border-2 border-dashed border-slate-200 rounded-2xl p-8 text-center hover:bg-slate-100 transition-colors cursor-pointer group"
|
|
>
|
|
<UploadCloud size={48} className="mx-auto text-slate-300 group-hover:text-primary-400 mb-2 transition-colors" />
|
|
<h4 className="font-bold text-slate-600">Arraste arquivos aqui ou clique para anexar</h4>
|
|
<p className="text-xs text-slate-400 mt-1">PDFs, Contratos (DOCX), Briefings.</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
{selectedCompany.documents.map(doc => (
|
|
<div key={doc.id} className="flex items-center justify-between p-4 bg-white rounded-xl border border-slate-100 hover:shadow-sm transition-shadow">
|
|
<div className="flex items-center gap-3">
|
|
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${doc.type === 'contract' ? 'bg-blue-50 text-blue-500' : 'bg-orange-50 text-orange-500'}`}>
|
|
<FileText size={20} />
|
|
</div>
|
|
<div>
|
|
<h4 className="font-bold text-slate-800 text-sm">{doc.title}</h4>
|
|
<p className="text-xs text-slate-400">{doc.date} • {doc.size}</p>
|
|
</div>
|
|
</div>
|
|
<button className="p-2 text-slate-400 hover:text-primary-500"><Download size={18}/></button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'services' && (
|
|
<div className="space-y-4">
|
|
<div className="flex justify-between items-center bg-white p-4 rounded-2xl border border-slate-100">
|
|
<div>
|
|
<p className="text-xs text-slate-400 uppercase font-bold">Faturamento Total</p>
|
|
<p className="text-xl font-bold text-slate-800">R$ {clientRevenue.toLocaleString('pt-BR')} <span className="text-xs font-normal text-slate-400">/mês</span></p>
|
|
</div>
|
|
<button onClick={() => setIsServiceModalOpen(true)} className="px-4 py-2 bg-primary-500 text-white text-sm font-bold rounded-xl hover:bg-primary-600">Adicionar Serviço</button>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
{selectedCompany.activeServices.map((service, idx) => (
|
|
<div key={idx} className="flex justify-between items-center p-4 bg-white rounded-xl border border-slate-100">
|
|
<div>
|
|
<h4 className="font-bold text-slate-800 text-sm">{service.name}</h4>
|
|
<span className="text-xs bg-slate-100 text-slate-500 px-2 py-0.5 rounded">{service.billingType === 'recurring' ? 'Recorrente' : 'Pontual'}</span>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<span className="font-bold text-slate-700">R$ {service.price.toLocaleString('pt-BR')}</span>
|
|
<button onClick={() => handleRemoveService(idx)} className="text-slate-300 hover:text-red-500"><X size={16}/></button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
};
|