Este commit introduz o módulo financeiro completo e refatora o sistema de profissionais para suportar múltiplas funções, corrigindo a contabilização e validação de equipes. Principais alterações: - **Módulo Financeiro:** - Criação da tabela `financial_transactions` e queries associadas. - Implementação do backend (Handler/Service) para gerenciar transações. - Nova página [Finance.tsx](cci:7://file:///c:/Projetos/photum/frontend/pages/Finance.tsx:0:0-0:0) com listagem, edição, filtros avançados e agrupamento por FOT. - Correção na busca de FOTs e formatação de datas. - **Gestão de Equipe e Profissionais:** - Refatoração para suportar múltiplas funções por profissional (Backend & Frontend). - Atualização do [Dashboard](cci:1://file:///c:/Projetos/photum/frontend/pages/Dashboard.tsx:31:0-1663:2) e [EventTable](cci:1://file:///c:/Projetos/photum/frontend/components/EventTable.tsx:28:0-659:2) para contabilizar corretamente profissionais (Fotografo, Cinegrafista, Recepcionista) verificando a lista de funções. - Implementação de validação de cota no aceite de convites (bloqueia se a equipe da função específica já estiver completa). - Ajuste visual nos indicadores de "Equipe Completa" e contadores de faltantes na listagem de eventos. - **Geral:** - Atualização da documentação Swagger. - Ajustes de tipagem e migrações de banco de dados.
320 lines
20 KiB
TypeScript
320 lines
20 KiB
TypeScript
import React from 'react';
|
|
import { Professional } from '../types';
|
|
import { Button } from './Button';
|
|
import {
|
|
X, Mail, Phone, MapPin, Building, Star, Camera, DollarSign, Award,
|
|
User, Car, CreditCard, AlertTriangle, Calendar, Clock, Edit2
|
|
} from 'lucide-react';
|
|
import { getAgendas } from '../services/apiService';
|
|
|
|
interface ProfessionalDetailsModalProps {
|
|
professional: Professional;
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onEdit?: () => void;
|
|
}
|
|
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
import { UserRole } from '../types';
|
|
|
|
// ... (imports remain)
|
|
|
|
export const ProfessionalDetailsModal: React.FC<ProfessionalDetailsModalProps> = ({
|
|
professional,
|
|
isOpen,
|
|
onClose,
|
|
onEdit
|
|
}) => {
|
|
const { user, token } = useAuth();
|
|
const [assignedEvents, setAssignedEvents] = React.useState<any[]>([]);
|
|
const [loadingEvents, setLoadingEvents] = React.useState(false);
|
|
|
|
React.useEffect(() => {
|
|
if (isOpen && user && token && (user.role === UserRole.SUPERADMIN || user.role === UserRole.BUSINESS_OWNER || user.id === professional.usuarioId)) {
|
|
fetchAssignments();
|
|
}
|
|
}, [isOpen, user, token, professional.id]);
|
|
|
|
const fetchAssignments = async () => {
|
|
setLoadingEvents(true);
|
|
const res = await getAgendas(token!);
|
|
if (res.data) {
|
|
// Filter events where professional is assigned
|
|
const filtered = res.data.filter((evt: any) =>
|
|
evt.assigned_professionals?.some((ap: any) => ap.professional_id === professional.id)
|
|
);
|
|
// Sort by date desc
|
|
filtered.sort((a: any, b: any) => new Date(b.data_evento).getTime() - new Date(a.data_evento).getTime());
|
|
setAssignedEvents(filtered);
|
|
}
|
|
setLoadingEvents(false);
|
|
};
|
|
|
|
if (!isOpen) return null;
|
|
|
|
const canViewDetails =
|
|
user?.role === UserRole.SUPERADMIN ||
|
|
user?.role === UserRole.BUSINESS_OWNER ||
|
|
(user?.id && professional.usuarioId && user.id === professional.usuarioId);
|
|
|
|
// Also check legacy/fallback logic if needed, but primary is role or ownership
|
|
const isAdminOrOwner = canViewDetails; // Keeping variable name for now or refactoring below -> refactoring below to use canViewDetails for clarity is better but to minimize diff noise we can keep it or rename it. Let's rename it to avoid confusion.
|
|
|
|
const isMaster = user?.role === UserRole.SUPERADMIN || user?.role === UserRole.BUSINESS_OWNER;
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 animate-fadeIn">
|
|
<div className="bg-white rounded-2xl shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-y-auto flex flex-col relative animate-slideIn">
|
|
{/* Header... (remains same) */}
|
|
<div className="h-32 bg-gradient-to-r from-brand-purple to-brand-purple/80 relative shrink-0">
|
|
<button
|
|
onClick={onClose}
|
|
className="absolute top-4 right-4 text-white hover:bg-white/20 p-2 rounded-full transition-colors"
|
|
>
|
|
<X size={24} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="px-8 pb-8 -mt-16 flex flex-col items-center sm:items-start relative z-10">
|
|
{/* Avatar... (remains same) */}
|
|
<div
|
|
className="w-32 h-32 rounded-full border-4 border-white bg-white shadow-lg mb-4 bg-gray-200"
|
|
style={{
|
|
backgroundImage: `url(${professional.avatar_url || professional.avatar || `https://ui-avatars.com/api/?name=${encodeURIComponent(professional.name || professional.nome)}&background=random`})`,
|
|
backgroundSize: 'cover',
|
|
backgroundPosition: 'center'
|
|
}}
|
|
/>
|
|
|
|
<div className="text-center sm:text-left w-full">
|
|
<h2 className="text-2xl font-serif font-bold text-brand-black">{professional.name || professional.nome}</h2>
|
|
<div className="flex flex-wrap justify-center sm:justify-start gap-2 mt-2">
|
|
<div className="flex flex-wrap items-center justify-center sm:justify-start gap-2">
|
|
{professional.functions && professional.functions.length > 0 ? (
|
|
professional.functions.map(f => (
|
|
<span key={f.id} className="px-3 py-1 bg-brand-gold/10 text-brand-black rounded-full text-sm font-medium border border-brand-gold/20 flex items-center gap-2">
|
|
<User size={14} />
|
|
{f.nome}
|
|
</span>
|
|
))
|
|
) : (
|
|
<span className="px-3 py-1 bg-brand-gold/10 text-brand-black rounded-full text-sm font-medium border border-brand-gold/20 flex items-center gap-2">
|
|
<User size={14} />
|
|
{professional.role || "Profissional"}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{/* Performance Rating - Only for Master (Admin/Owner), NOT for the professional themselves */}
|
|
{isMaster && professional.media !== undefined && professional.media !== null && (
|
|
<span className="px-3 py-1 bg-yellow-50 text-yellow-700 rounded-full text-sm font-medium border border-yellow-200 flex items-center gap-1">
|
|
<Star size={14} className="fill-yellow-500 text-yellow-500" />
|
|
{typeof professional.media === 'number' ? professional.media.toFixed(1) : parseFloat(String(professional.media)).toFixed(1)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="w-full grid grid-cols-1 md:grid-cols-2 gap-8 mt-8">
|
|
{/* Dados Pessoais - Protected */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-bold text-gray-900 flex items-center gap-2 mb-4 border-b pb-2">
|
|
<User size={20} className="text-brand-gold" />
|
|
Dados Pessoais
|
|
</h3>
|
|
|
|
{isAdminOrOwner ? (
|
|
<div className="space-y-4 text-sm">
|
|
{professional.email && (
|
|
<div className="flex items-start gap-3 text-gray-600">
|
|
<Mail size={18} className="mt-1 shrink-0 text-gray-400" />
|
|
<span className="break-all">{professional.email}</span>
|
|
</div>
|
|
)}
|
|
{professional.whatsapp && (
|
|
<div className="flex items-start gap-3 text-gray-600">
|
|
<Phone size={18} className="mt-1 shrink-0 text-gray-400" />
|
|
<span>{professional.whatsapp}</span>
|
|
</div>
|
|
)}
|
|
{(professional.cidade || professional.uf) && (
|
|
<div className="flex items-start gap-3 text-gray-600">
|
|
<MapPin size={18} className="mt-1 shrink-0 text-gray-400" />
|
|
<span>{professional.cidade}{professional.cidade && professional.uf ? ", " : ""}{professional.uf}</span>
|
|
</div>
|
|
)}
|
|
{professional.endereco && (
|
|
<div className="flex items-start gap-3 text-gray-600">
|
|
<Building size={18} className="mt-1 shrink-0 text-gray-400" />
|
|
<span className="text-sm">{professional.endereco}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="p-4 bg-gray-50 rounded-lg border border-gray-100 text-center">
|
|
<AlertTriangle className="w-8 h-8 text-gray-400 mx-auto mb-2" />
|
|
<p className="text-gray-500 text-sm">Informações de contato restritas.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Equipamentos - Public */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-bold text-gray-900 flex items-center gap-2 mb-4 border-b pb-2">
|
|
<Camera size={20} className="text-brand-gold" />
|
|
Equipamentos
|
|
</h3>
|
|
|
|
<div className="bg-gray-50 rounded-lg p-4 text-sm text-gray-700 whitespace-pre-wrap leading-relaxed border border-gray-100">
|
|
{professional.equipamentos || "Nenhum equipamento listado."}
|
|
</div>
|
|
<div className="mt-4 flex flex-wrap gap-2">
|
|
{professional.carro_disponivel && (
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
<Car size={12} className="mr-1" /> Carro Próprio
|
|
</span>
|
|
)}
|
|
{professional.tem_estudio && (
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
|
|
<Building size={12} className="mr-1" /> Estúdio ({professional.qtd_estudio})
|
|
</span>
|
|
)}
|
|
{professional.extra_por_equipamento && (
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
|
<CreditCard size={12} className="mr-1" /> Extra p/ Equip.
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{/* Dados Financeiros & Performance - Protected */}
|
|
{isAdminOrOwner && (
|
|
<>
|
|
<div className="w-full mt-8">
|
|
<h3 className="text-lg font-bold text-gray-900 flex items-center gap-2 mb-4 border-b pb-2">
|
|
<DollarSign size={20} className="text-brand-gold" />
|
|
Dados Financeiros
|
|
</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm w-full">
|
|
<div className="bg-gray-50 p-3 rounded-lg border border-gray-100">
|
|
<span className="block text-gray-500 text-xs uppercase tracking-wider font-semibold mb-1">CPF/CNPJ Titular</span>
|
|
<span className="font-medium text-gray-900">{professional.cpf_cnpj_titular || "-"}</span>
|
|
</div>
|
|
<div className="bg-gray-50 p-3 rounded-lg border border-gray-100">
|
|
<span className="block text-gray-500 text-xs uppercase tracking-wider font-semibold mb-1">Chave Pix</span>
|
|
<span className="font-medium text-gray-900">{professional.conta_pix || "-"}</span>
|
|
</div>
|
|
<div className="bg-gray-50 p-3 rounded-lg border border-gray-100">
|
|
<span className="block text-gray-500 text-xs uppercase tracking-wider font-semibold mb-1">Banco / Agência</span>
|
|
<span className="font-medium text-gray-900">
|
|
{professional.banco || "-"}{professional.agencia ? ` / ${professional.agencia}` : ""}
|
|
</span>
|
|
</div>
|
|
<div className="bg-gray-50 p-3 rounded-lg border border-gray-100">
|
|
<span className="block text-gray-500 text-xs uppercase tracking-wider font-semibold mb-1">Tabela Free</span>
|
|
<span className="font-medium text-gray-900">R$ {professional.tabela_free || "0,00"}</span>
|
|
</div>
|
|
<div className="bg-gray-50 p-3 rounded-lg border border-gray-100">
|
|
<span className="block text-gray-500 text-xs uppercase tracking-wider font-semibold mb-1">Tipo de Cartão</span>
|
|
<span className="font-medium text-gray-900">{professional.tipo_cartao || "-"}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Performance / Observations - Protected */}
|
|
{isMaster && (
|
|
<div className="w-full mt-8 bg-brand-gold/5 rounded-xl p-6 border border-brand-gold/10">
|
|
<div className="flex items-start gap-4">
|
|
<div className="p-2 bg-white rounded-full text-brand-gold shadow-sm">
|
|
<Star size={24} className="text-brand-gold fill-brand-gold" />
|
|
</div>
|
|
<div className="w-full">
|
|
<h4 className="text-lg font-bold text-gray-900 mb-4">Performance & Avaliação</h4>
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
|
<div className="text-center bg-white p-2 rounded shadow-sm">
|
|
<div className="text-xs text-gray-500 mb-1">Técnica</div>
|
|
<div className="font-bold text-lg text-gray-800">{professional.qual_tec || 0}</div>
|
|
</div>
|
|
<div className="text-center bg-white p-2 rounded shadow-sm">
|
|
<div className="text-xs text-gray-500 mb-1">Simpatia</div>
|
|
<div className="font-bold text-lg text-gray-800">{professional.educacao_simpatia || 0}</div>
|
|
</div>
|
|
<div className="text-center bg-white p-2 rounded shadow-sm">
|
|
<div className="text-xs text-gray-500 mb-1">Desempenho</div>
|
|
<div className="font-bold text-lg text-gray-800">{professional.desempenho_evento || 0}</div>
|
|
</div>
|
|
<div className="text-center bg-white p-2 rounded shadow-sm">
|
|
<div className="text-xs text-gray-500 mb-1">Horário</div>
|
|
<div className="font-bold text-lg text-gray-800">{professional.disp_horario || 0}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-gray-600 text-sm leading-relaxed mb-2">
|
|
Média Geral: <strong>{professional.media ? (typeof professional.media === 'number' ? professional.media.toFixed(1) : parseFloat(String(professional.media)).toFixed(1)) : "N/A"}</strong>
|
|
</p>
|
|
{professional.observacao && (
|
|
<div className="mt-3 text-sm text-gray-500 italic border-t border-brand-gold/10 pt-2">
|
|
"{professional.observacao}"
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Assignments - Protected */}
|
|
{canViewDetails && (
|
|
<div className="w-full mt-8">
|
|
<h3 className="text-lg font-bold text-gray-900 flex items-center gap-2 mb-4 border-b pb-2">
|
|
<Calendar size={20} className="text-brand-gold" />
|
|
Eventos Atribuídos
|
|
</h3>
|
|
{loadingEvents ? (
|
|
<p className="text-sm text-gray-500">Carregando agenda...</p>
|
|
) : assignedEvents.length === 0 ? (
|
|
<p className="text-sm text-gray-500 italic">Nenhum evento atribuído.</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{assignedEvents.map((evt: any) => (
|
|
<div key={evt.id} className="bg-white border rounded-lg p-3 flex justify-between items-center shadow-sm">
|
|
<div>
|
|
<p className="font-bold text-gray-800 text-sm">{evt.empresa_nome || "Empresa"} - {evt.tipo_evento_nome || "Evento"}</p>
|
|
<div className="flex gap-3 text-xs text-gray-500 mt-1">
|
|
<span className="flex items-center gap-1"><Calendar size={12} /> {new Date(evt.data_evento).toLocaleDateString()}</span>
|
|
<span className="flex items-center gap-1"><Clock size={12} /> {evt.horario}</span>
|
|
{evt.local_evento && <span className="flex items-center gap-1"><MapPin size={12} /> {evt.local_evento}</span>}
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<span className={`text-xs px-2 py-1 rounded-full ${evt.status === 'Confirmado' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'}`}>
|
|
{evt.status || "Agendado"}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="w-full mt-8 flex justify-end gap-3">
|
|
<Button variant="outline" onClick={onClose}>
|
|
Fechar
|
|
</Button>
|
|
{onEdit && (
|
|
<Button onClick={onEdit}>
|
|
<Edit2 size={16} className="mr-2" />
|
|
Editar Dados
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|