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:
648
components/CRMView.tsx
Normal file
648
components/CRMView.tsx
Normal file
@@ -0,0 +1,648 @@
|
||||
|
||||
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;
|
||||
};
|
||||
Reference in New Issue
Block a user