Merge pull request #35 from rede5/Front-back-integracao-task13

feat: aprimora responsividade mobile, form de eventos e persistência de dados

- Frontend:
  - Implementa visualização em cards mobile para lista de Eventos (/painel), Gestão de Cursos (/cursos) e modal de Equipe.
  - Corrige rolagem e layout do modal de detalhes do profissional em telas pequenas.
  - Unifica seleção de turma (Curso/Inst/Ano) no formulário de eventos para simplificar UX.
  - Adiciona botão "Voltar" no formulário de eventos.
  - Adiciona integração de busca de CEP e validação de "Qtd Estúdios".
  - Ajusta inputs de avaliação (estrelas) e exibição de disponibilidade de horário.
  - Atualiza interfaces (types.ts) para incluir campos novos (cep, email no profissional).

- Backend:
  - Adiciona persistência do campo "email" na tabela de profissionais.
  - Corrige bug de persistência nula no campo "media" (avaliação).
  - Atualiza queries SQL e gera novos modelos (sqlc) para refletir mudanças no schema.
  - Atualiza documentação Swagger.
This commit is contained in:
Andre F. Rodrigues 2025-12-25 12:24:30 -03:00 committed by GitHub
commit 9ccd28fc42
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 579 additions and 239 deletions

View file

@ -661,8 +661,6 @@ export const EventForm: React.FC<EventFormProps> = ({
value={selectedCompanyId} value={selectedCompanyId}
onChange={e => { onChange={e => {
setSelectedCompanyId(e.target.value); setSelectedCompanyId(e.target.value);
setSelectedCourseName("");
setSelectedInstitutionName("");
setFormData({ ...formData, fotId: "" }); setFormData({ ...formData, fotId: "" });
}} }}
> >
@ -688,65 +686,53 @@ export const EventForm: React.FC<EventFormProps> = ({
</div> </div>
)} )}
{/* 1. Curso */} {/* Consolidated Turma Selection */}
<div className="mb-4">
<label className="block text-sm text-gray-600 mb-1">Curso</label>
<select
className="w-full px-3 py-2 border border-gray-300 rounded focus:ring-brand-gold focus:border-brand-gold"
value={selectedCourseName}
onChange={e => {
setSelectedCourseName(e.target.value);
setSelectedInstitutionName("");
setFormData({ ...formData, fotId: "" });
}}
disabled={loadingFots || ((user?.role === UserRole.BUSINESS_OWNER || user?.role === UserRole.SUPERADMIN) && !selectedCompanyId)}
>
<option value="">Selecione o Curso</option>
{uniqueCourses.map(c => <option key={c} value={c}>{c}</option>)}
</select>
</div>
{/* 2. Instituição */}
<div className="mb-4">
<label className="block text-sm text-gray-600 mb-1">Instituição</label>
<select
className="w-full px-3 py-2 border border-gray-300 rounded focus:ring-brand-gold focus:border-brand-gold"
value={selectedInstitutionName}
onChange={e => {
setSelectedInstitutionName(e.target.value);
setFormData({ ...formData, fotId: "" });
}}
disabled={!selectedCourseName}
>
<option value="">Selecione a Instituição</option>
{filteredInstitutions.map(i => <option key={i} value={i}>{i}</option>)}
</select>
</div>
{/* 3. Ano/Turma (Final FOT Selection) */}
<div className="mb-0"> <div className="mb-0">
<label className="block text-sm text-gray-600 mb-1">Ano/Turma</label> <label className="block text-sm text-gray-600 mb-1">Selecione a Turma</label>
<select <select
className="w-full px-3 py-2 border border-gray-300 rounded focus:ring-brand-gold focus:border-brand-gold" className="w-full px-3 py-2 border border-gray-300 rounded focus:ring-brand-gold focus:border-brand-gold"
value={formData.fotId || ""} value={formData.fotId || ""}
onChange={e => setFormData({ ...formData, fotId: e.target.value })} onChange={e => {
disabled={!selectedInstitutionName} const selectedFotId = e.target.value;
const selectedFot = availableFots.find(f => f.id === selectedFotId);
if (selectedFot) {
setFormData({
...formData,
fotId: selectedFotId,
// Optional: You might want to store denormalized data if needed, but ID is usually enough
});
} else {
setFormData({ ...formData, fotId: "" });
}
}}
disabled={loadingFots || ((user?.role === UserRole.BUSINESS_OWNER || user?.role === UserRole.SUPERADMIN) && !selectedCompanyId)}
> >
<option value="">Selecione a Turma</option> <option value="">Selecione a Turma (Curso - Instituição - Ano)</option>
{filteredYears.map(f => ( {availableFots.map(f => (
<option key={f.id} value={f.id}>{f.label}</option> <option key={f.id} value={f.id}>
{f.curso_nome} - {f.instituicao} - {f.ano_formatura_label} (FOT {f.fot})
</option>
))} ))}
</select> </select>
{loadingFots && <p className="text-xs text-gray-500 mt-1">Carregando turmas...</p>}
</div> </div>
</> </>
)} )}
</div> </div>
</div> </div>
<div className="flex flex-col sm:flex-row justify-end gap-3 sm:gap-0 mt-8"> <div className="flex flex-col sm:flex-row justify-between gap-3 mt-8">
<Button
variant="outline"
onClick={onCancel}
className="w-full sm:w-auto order-2 sm:order-1"
>
Voltar
</Button>
<Button <Button
onClick={() => setActiveTab("location")} onClick={() => setActiveTab("location")}
className="w-full sm:w-auto" className="w-full sm:w-auto order-1 sm:order-2"
disabled={(!user?.empresaId && user?.role !== UserRole.SUPERADMIN && user?.role !== UserRole.BUSINESS_OWNER)} disabled={(!user?.empresaId && user?.role !== UserRole.SUPERADMIN && user?.role !== UserRole.BUSINESS_OWNER)}
> >
Próximo: Localização Próximo: Localização

View file

@ -144,7 +144,97 @@ export const EventTable: React.FC<EventTableProps> = ({
return ( return (
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden shadow-sm"> <div className="bg-white rounded-lg border border-gray-200 overflow-hidden shadow-sm">
<div className="overflow-x-auto"> {/* Mobile Card View */}
<div className="md:hidden divide-y divide-gray-100">
{sortedEvents.map((event) => {
let photographerAssignment = null;
if (isPhotographer && currentProfessionalId && event.assignments) {
photographerAssignment = event.assignments.find(a => a.professionalId === currentProfessionalId);
}
return (
<div
key={event.id}
className="p-4 hover:bg-gray-50 active:bg-gray-100 transition-colors cursor-pointer"
onClick={() => onEventClick(event)}
>
<div className="flex justify-between items-start mb-2">
<div>
<span className="font-bold text-gray-900 block">FOT {event.fot || "-"}</span>
<span className="text-xs text-gray-500">{event.type}</span>
</div>
<span className="text-sm font-medium text-gray-600 bg-gray-100 px-2 py-1 rounded">
{formatDate(event.date)}
</span>
</div>
<div className="mb-3 space-y-1">
<p className="text-sm text-gray-800 font-medium">{event.curso || "Sem curso defined"}</p>
<p className="text-sm text-gray-600">{event.instituicao || "-"}</p>
<div className="flex items-center gap-2 text-xs text-gray-500">
<span>{event.empresa || "-"}</span>
<span></span>
<span>{event.anoFormatura || "-"}</span>
</div>
</div>
<div className="flex justify-between items-center mt-3 pt-3 border-t border-gray-50">
<span
className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${STATUS_COLORS[event.status] || "bg-gray-100 text-gray-800"}`}
>
{getStatusDisplay(event.status)}
</span>
{(canApprove || isPhotographer) && (
<div onClick={(e) => e.stopPropagation()} className="flex items-center gap-2">
{canApprove && event.status === EventStatus.PENDING_APPROVAL && (
<button
onClick={(e) => onApprove?.(e, event.id)}
className="bg-green-500 text-white p-2 rounded-full hover:bg-green-600 transition-colors shadow-sm"
title="Aprovar evento"
>
<CheckCircle size={16} />
</button>
)}
{isPhotographer && photographerAssignment && (
<>
{photographerAssignment.status === "PENDENTE" && (
<div className="flex gap-2">
<button
onClick={(e) => onAssignmentResponse?.(e, event.id, "ACEITO")}
className="bg-green-100 text-green-700 px-3 py-1 rounded-md text-xs font-bold border border-green-200"
>
Aceitar
</button>
<button
onClick={(e) => {
const reason = prompt("Motivo da rejeição:");
if (reason) onAssignmentResponse?.(e, event.id, "REJEITADO", reason);
}}
className="bg-red-100 text-red-700 px-3 py-1 rounded-md text-xs font-bold border border-red-200"
>
Rejeitar
</button>
</div>
)}
{photographerAssignment.status === "ACEITO" && (
<span className="text-green-600 text-xs font-bold border border-green-200 bg-green-50 px-2 py-1 rounded">Aceito</span>
)}
{photographerAssignment.status === "REJEITADO" && (
<span className="text-red-600 text-xs font-bold border border-red-200 bg-red-50 px-2 py-1 rounded">Rejeitado</span>
)}
</>
)}
</div>
)}
</div>
</div>
);
})}
</div>
<div className="hidden md:block overflow-x-auto">
<table className="w-full"> <table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200"> <thead className="bg-gray-50 border-b border-gray-200">
<tr> <tr>

View file

@ -1,7 +1,10 @@
import React from 'react'; import React from 'react';
import { Professional } from '../types'; import { Professional } from '../types';
import { Button } from './Button'; import { Button } from './Button';
import { X, Mail, Phone, MapPin, Building, Star, Camera, DollarSign, Award } from 'lucide-react'; import {
X, Mail, Phone, MapPin, Building, Star, Camera, DollarSign, Award,
User, Car, CreditCard, AlertTriangle
} from 'lucide-react';
interface ProfessionalDetailsModalProps { interface ProfessionalDetailsModalProps {
professional: Professional; professional: Professional;
@ -18,10 +21,10 @@ export const ProfessionalDetailsModal: React.FC<ProfessionalDetailsModalProps> =
return ( return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 animate-fadeIn"> <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-2xl w-full overflow-hidden flex flex-col relative animate-slideIn"> <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 com Capa/Avatar Style */} {/* Header com Capa/Avatar Style */}
<div className="h-32 bg-gradient-to-r from-brand-purple to-brand-purple/80 relative"> <div className="h-32 bg-gradient-to-r from-brand-purple to-brand-purple/80 relative shrink-0">
<button <button
onClick={onClose} onClick={onClose}
className="absolute top-4 right-4 text-white hover:bg-white/20 p-2 rounded-full transition-colors" className="absolute top-4 right-4 text-white hover:bg-white/20 p-2 rounded-full transition-colors"
@ -35,77 +38,166 @@ export const ProfessionalDetailsModal: React.FC<ProfessionalDetailsModalProps> =
{/* Avatar Grande */} {/* Avatar Grande */}
<div <div
className="w-32 h-32 rounded-full border-4 border-white bg-white shadow-lg mb-4" className="w-32 h-32 rounded-full border-4 border-white bg-white shadow-lg mb-4 bg-gray-200"
style={{ style={{
backgroundImage: `url(${professional.avatar || `https://ui-avatars.com/api/?name=${encodeURIComponent(professional.name)}&background=random`})`, backgroundImage: `url(${professional.avatar || `https://ui-avatars.com/api/?name=${encodeURIComponent(professional.name || professional.nome)}&background=random`})`,
backgroundSize: 'cover', backgroundSize: 'cover',
backgroundPosition: 'center' backgroundPosition: 'center'
}} }}
/> />
<div className="text-center sm:text-left w-full"> <div className="text-center sm:text-left w-full">
<h2 className="text-2xl font-serif font-bold text-brand-black">{professional.name}</h2> <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 justify-center sm:justify-start gap-2 mt-2">
<span className="px-3 py-1 bg-brand-gold/10 text-brand-black rounded-full text-sm font-medium border border-brand-gold/20"> <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">
{professional.role} <User size={14} />
</span> {professional.role || "Profissional"}
{/* Mock de Avaliação */}
<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" /> 4.9
</span> </span>
{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> </div>
<div className="w-full grid grid-cols-1 md:grid-cols-2 gap-6 mt-8"> <div className="w-full grid grid-cols-1 md:grid-cols-2 gap-8 mt-8">
{/* Dados Pessoais */}
<div className="space-y-4"> <div className="space-y-4">
<h3 className="font-bold text-gray-900 border-b pb-2 flex items-center gap-2"> <h3 className="text-lg font-bold text-gray-900 flex items-center gap-2 mb-4 border-b pb-2">
<Building size={18} className="text-brand-gold" /> <User size={20} className="text-brand-gold" />
Dados Pessoais Dados Pessoais
</h3> </h3>
<div className="space-y-3 text-sm"> <div className="space-y-4 text-sm">
<div className="flex items-center gap-3 text-gray-700"> {professional.email && (
<Mail size={16} className="text-gray-400" /> <div className="flex items-start gap-3 text-gray-600">
<span>{professional.email}</span> <Mail size={18} className="mt-1 shrink-0 text-gray-400" />
</div> <span className="break-all">{professional.email}</span>
<div className="flex items-center gap-3 text-gray-700"> </div>
<Phone size={16} className="text-gray-400" /> )}
<span>{professional.phone || "Não informado"}</span> {professional.whatsapp && (
</div> <div className="flex items-start gap-3 text-gray-600">
{/* Endereço Mockado se não tiver no tipo, ou usar campos extras do backend se mapeados */} <Phone size={18} className="mt-1 shrink-0 text-gray-400" />
<div className="flex items-center gap-3 text-gray-700"> <span>{professional.whatsapp}</span>
<MapPin size={16} className="text-gray-400" /> </div>
<span>São Paulo, SP</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>
</div> </div>
{/* Equipamentos */}
<div className="space-y-4"> <div className="space-y-4">
<h3 className="font-bold text-gray-900 border-b pb-2 flex items-center gap-2"> <h3 className="text-lg font-bold text-gray-900 flex items-center gap-2 mb-4 border-b pb-2">
<Camera size={18} className="text-brand-gold" /> <Camera size={20} className="text-brand-gold" />
Equipamentos & Habilidades Equipamentos
</h3> </h3>
{/* Mock de Habilidades / Equipamentos (pois não está no type Professional simples ainda) */} <div className="bg-gray-50 rounded-lg p-4 text-sm text-gray-700 whitespace-pre-wrap leading-relaxed border border-gray-100">
<div className="text-sm text-gray-600 space-y-2"> {professional.equipamentos || "Nenhum equipamento listado."}
<p>Equipamento Profissional: <span className="text-gray-900">Canon R6, Lentes série L</span></p> </div>
<div className="flex flex-wrap gap-2 mt-2"> <div className="mt-4 flex flex-wrap gap-2">
{["Formatura", "Casamento", "Estúdio"].map(tag => ( {professional.carro_disponivel && (
<span key={tag} className="text-xs bg-gray-100 px-2 py-1 rounded text-gray-600">{tag}</span> <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
</div> </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> </div>
</div> </div>
<div className="w-full mt-8 bg-gray-50 p-4 rounded-lg border border-gray-100"> {/* Dados Financeiros */}
<div className="flex items-start gap-3"> <div className="w-full mt-8">
<Award className="text-brand-gold mt-1" size={20} /> <h3 className="text-lg font-bold text-gray-900 flex items-center gap-2 mb-4 border-b pb-2">
<div> <DollarSign size={20} className="text-brand-gold" />
<h4 className="font-bold text-gray-900 text-sm">Performance</h4> Dados Financeiros
<p className="text-sm text-gray-600 mt-1">Este profissional tem mantido uma taxa de 100% de presença e alta satisfação nos últimos eventos.</p> </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 */}
<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> </div>
</div> </div>

View file

@ -766,11 +766,41 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
const mappedProfs: Professional[] = result.data.map((p: any) => ({ const mappedProfs: Professional[] = result.data.map((p: any) => ({
id: p.id, id: p.id,
usuarioId: p.usuario_id, usuarioId: p.usuario_id,
name: p.nome, nome: p.nome,
name: p.nome, // Keep for legacy Dashboard usage
email: p.email || "", email: p.email || "",
role: p.funcao_nome || "Fotógrafo", funcao_profissional_id: p.funcao_profissional_id,
avatar: `https://ui-avatars.com/api/?name=${encodeURIComponent(p.nome)}&background=random`, // Fallback avatar role: p.funcao_profissional || p.funcao_nome || "Fotógrafo",
avatar: p.avatar_url || `https://ui-avatars.com/api/?name=${encodeURIComponent(p.nome)}&background=random`,
phone: p.whatsapp, phone: p.whatsapp,
// Detailed fields
endereco: p.endereco,
cidade: p.cidade,
uf: p.uf,
cep: p.cep,
whatsapp: p.whatsapp,
cpf_cnpj_titular: p.cpf_cnpj_titular,
banco: p.banco,
agencia: p.agencia,
conta_pix: p.conta_pix,
carro_disponivel: p.carro_disponivel,
tem_estudio: p.tem_estudio,
qtd_estudio: p.qtd_estudio,
tipo_cartao: p.tipo_cartao,
observacao: p.observacao,
// Ratings
qual_tec: p.qual_tec,
educacao_simpatia: p.educacao_simpatia,
desempenho_evento: p.desempenho_evento,
disp_horario: p.disp_horario,
media: p.media,
tabela_free: p.tabela_free,
extra_por_equipamento: p.extra_por_equipamento,
equipamentos: p.equipamentos,
availability: {}, // Default empty availability availability: {}, // Default empty availability
})); }));
setProfessionals(mappedProfs); setProfessionals(mappedProfs);

View file

@ -204,150 +204,211 @@ export const CourseManagement: React.FC = () => {
Carregando dados... Carregando dados...
</div> </div>
) : ( ) : (
<div className="overflow-x-auto"> <>
<table className="min-w-full divide-y divide-gray-200"> <div className="md:hidden divide-y divide-gray-100">
<thead className="bg-gray-50"> {filteredList.length === 0 ? (
<tr> <div className="p-8 text-center text-gray-500">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <Briefcase className="w-12 h-12 text-gray-300 mx-auto mb-3" />
FOT <p className="text-lg font-medium">Nenhuma turma FOT encontrada</p>
</th> </div>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> ) : (
Empresa filteredList.map((item) => (
</th> <div key={item.id} className="p-4 bg-white hover:bg-gray-50 transition-colors">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <div className="flex justify-between items-start mb-2">
Curso <div>
</th> <span className="font-bold text-gray-900 text-lg block">FOT {item.fot}</span>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <div className="text-sm font-medium text-gray-700">{item.empresa_nome}</div>
Instituição
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Ano Formatura
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Cidade
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Estado
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Observações
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Gastos Captação
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Pré Venda
</th>
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
Ações
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredList.length === 0 ? (
<tr>
<td
colSpan={11}
className="px-6 py-12 text-center text-gray-500"
>
<div className="flex flex-col items-center justify-center">
<Briefcase className="w-12 h-12 text-gray-300 mb-3" />
<p className="text-lg font-medium">
Nenhuma turma FOT encontrada
</p>
</div> </div>
</td> <span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${item.pre_venda ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"}`}>
</tr> {item.pre_venda ? "Pré-venda" : "Regular"}
) : ( </span>
filteredList.map((item) => ( </div>
<tr
key={item.id} <div className="space-y-1 mb-3 bg-gray-50 p-3 rounded-lg border border-gray-100">
className="hover:bg-gray-50 transition-colors" <p className="text-sm"><span className="font-semibold text-gray-500 w-20 inline-block">Curso:</span> {item.curso_nome}</p>
> <p className="text-sm"><span className="font-semibold text-gray-500 w-20 inline-block">Inst:</span> {item.instituicao}</p>
<td className="px-6 py-4 whitespace-nowrap"> <p className="text-sm flex items-center gap-1 text-gray-600 mt-1 pt-1 border-t border-gray-200">
<div className="text-sm font-medium text-gray-900"> {item.cidade} - {item.estado}
{item.fot || "-"} <span className="mx-1"></span>
</div> {item.ano_formatura_label}
</td> </p>
<td className="px-6 py-4 whitespace-nowrap"> </div>
<div className="text-sm text-gray-600">
{item.empresa_nome || "-"} {item.observacoes && (
</div> <p className="text-xs text-gray-500 italic mb-3 pl-2 border-l-2 border-brand-gold/30">
</td> {item.observacoes}
<td className="px-6 py-4 whitespace-nowrap"> </p>
<div className="text-sm text-gray-600"> )}
{item.curso_nome || "-"}
</div> <div className="flex items-center justify-between mt-3">
</td> <div className="text-sm font-bold text-gray-900 bg-green-50 px-2 py-1 rounded border border-green-100">
<td className="px-6 py-4 whitespace-nowrap"> {item.gastos_captacao ? item.gastos_captacao.toLocaleString("pt-BR", { style: "currency", currency: "BRL" }) : "R$ 0,00"}
<div className="text-sm text-gray-600"> </div>
{item.instituicao || "-"} <div className="flex gap-2">
</div> <button
</td> onClick={() => handleEdit(item)}
<td className="px-6 py-4 whitespace-nowrap"> className="flex items-center gap-1 px-3 py-1.5 text-blue-700 bg-blue-50 border border-blue-100 rounded-lg text-sm font-medium hover:bg-blue-100"
<div className="text-sm text-gray-600">
{item.ano_formatura_label || "-"}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{item.cidade || "-"}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{item.estado || "-"}
</div>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-600 max-w-xs truncate" title={item.observacoes}>
{item.observacoes || "-"}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{item.gastos_captacao
? item.gastos_captacao.toLocaleString("pt-BR", {
style: "currency",
currency: "BRL",
})
: "R$ 0,00"}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${item.pre_venda
? "bg-green-100 text-green-800"
: "bg-gray-100 text-gray-800"
}`}
> >
{item.pre_venda ? "Sim" : "Não"} <Edit size={16} /> Editar
</span> </button>
</td> <button
<td className="px-6 py-4 whitespace-nowrap text-center"> onClick={() => handleDelete(item.id, item.fot)}
<div className="flex items-center justify-center gap-2"> className="flex items-center gap-1 px-3 py-1.5 text-red-700 bg-red-50 border border-red-100 rounded-lg text-sm font-medium hover:bg-red-100"
<button >
onClick={() => handleEdit(item)} <Trash2 size={16} /> Excluir
className="p-1.5 text-blue-600 hover:bg-blue-50 rounded transition-colors" </button>
title="Editar" </div>
> </div>
<Edit size={16} /> </div>
</button> ))
<button )}
onClick={() => handleDelete(item.id, item.fot)} </div>
className="p-1.5 text-red-600 hover:bg-red-50 rounded transition-colors"
title="Excluir" <div className="hidden md:block overflow-x-auto">
> <table className="min-w-full divide-y divide-gray-200">
<Trash2 size={16} /> <thead className="bg-gray-50">
</button> <tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
FOT
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Empresa
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Curso
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Instituição
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Ano Formatura
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Cidade
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Estado
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Observações
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Gastos Captação
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Pré Venda
</th>
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
Ações
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredList.length === 0 ? (
<tr>
<td
colSpan={11}
className="px-6 py-12 text-center text-gray-500"
>
<div className="flex flex-col items-center justify-center">
<Briefcase className="w-12 h-12 text-gray-300 mb-3" />
<p className="text-lg font-medium">
Nenhuma turma FOT encontrada
</p>
</div> </div>
</td> </td>
</tr> </tr>
)) ) : (
)} filteredList.map((item) => (
</tbody> <tr
</table> key={item.id}
</div> className="hover:bg-gray-50 transition-colors"
>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{item.fot || "-"}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{item.empresa_nome || "-"}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{item.curso_nome || "-"}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{item.instituicao || "-"}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{item.ano_formatura_label || "-"}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{item.cidade || "-"}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{item.estado || "-"}
</div>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-600 max-w-xs truncate" title={item.observacoes}>
{item.observacoes || "-"}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{item.gastos_captacao
? item.gastos_captacao.toLocaleString("pt-BR", {
style: "currency",
currency: "BRL",
})
: "R$ 0,00"}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${item.pre_venda
? "bg-green-100 text-green-800"
: "bg-gray-100 text-gray-800"
}`}
>
{item.pre_venda ? "Sim" : "Não"}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-center">
<div className="flex items-center justify-center gap-2">
<button
onClick={() => handleEdit(item)}
className="p-1.5 text-blue-600 hover:bg-blue-50 rounded transition-colors"
title="Editar"
>
<Edit size={16} />
</button>
<button
onClick={() => handleDelete(item.id, item.fot)}
className="p-1.5 text-red-600 hover:bg-red-50 rounded transition-colors"
title="Excluir"
>
<Trash2 size={16} />
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</>
)} )}
</div> </div>
</div> </div>

View file

@ -849,8 +849,8 @@ export const Dashboard: React.FC<DashboardProps> = ({
</p> </p>
</div> </div>
{/* Tabela de Profissionais */} {/* Tabela de Profissionais (Desktop) */}
<div className="overflow-x-auto"> <div className="hidden md:block overflow-x-auto">
<table className="w-full border-collapse"> <table className="w-full border-collapse">
<thead> <thead>
<tr className="bg-gray-50 border-b-2 border-gray-200"> <tr className="bg-gray-50 border-b-2 border-gray-200">
@ -984,6 +984,86 @@ export const Dashboard: React.FC<DashboardProps> = ({
</tbody> </tbody>
</table> </table>
</div> </div>
{/* Lista de Cards (Mobile) */}
<div className="md:hidden space-y-4">
{professionals.map((photographer) => {
const assignment = (selectedEvent.assignments || []).find(
(a) => a.professionalId === photographer.id
);
const status = assignment ? assignment.status : null;
return (
<div key={photographer.id} className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
<div className="flex items-center gap-3 mb-3" onClick={() => handleViewProfessional(photographer)}>
<div
className="w-12 h-12 rounded-full border-2 border-gray-200 bg-gray-300 flex-shrink-0"
style={{
backgroundImage: `url(${photographer.avatar})`,
backgroundSize: "cover",
}}
/>
<div>
<h4 className="font-bold text-gray-900">{photographer.name || photographer.nome}</h4>
<p className="text-xs text-gray-500">ID: {photographer.id.substring(0, 8)}...</p>
</div>
</div>
<div className="flex flex-wrap gap-2 mb-4">
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
{photographer.role}
</span>
{status === "ACEITO" && (
<span className="inline-flex items-center gap-1 px-2 py-1 bg-green-100 text-green-800 rounded-full text-xs font-medium">
<CheckCircle size={12} /> Confirmado
</span>
)}
{status === "PENDENTE" && (
<span className="inline-flex items-center gap-1 px-2 py-1 bg-yellow-100 text-yellow-800 rounded-full text-xs font-medium">
<Clock size={12} /> Pendente
</span>
)}
{status === "REJEITADO" && (
<span className="inline-flex items-center gap-1 px-2 py-1 bg-red-100 text-red-800 rounded-full text-xs font-medium">
<X size={12} /> Recusado
</span>
)}
{!status && (
<span className="inline-flex items-center gap-1 px-2 py-1 bg-gray-100 text-gray-600 rounded-full text-xs font-medium">
<UserCheck size={12} /> Disponível
</span>
)}
</div>
<div className="flex gap-2">
<button
onClick={() => handleViewProfessional(photographer)}
className="flex-1 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 bg-white"
>
Ver Detalhes
</button>
<button
onClick={() => togglePhotographer(photographer.id)}
className={`flex-1 py-2 rounded-lg text-sm font-medium transition-colors ${status === "ACEITO" || status === "PENDENTE"
? "bg-red-100 text-red-700 hover:bg-red-200"
: "bg-brand-gold text-white hover:bg-[#a5bd2e]"
}`}
>
{status === "ACEITO" || status === "PENDENTE" ? "Remover" : "Adicionar"}
</button>
</div>
</div>
);
})}
{professionals.length === 0 && (
<div className="text-center p-8 text-gray-500">
<UserX size={48} className="mx-auto text-gray-300 mb-2" />
<p>Nenhum profissional disponível.</p>
</div>
)}
</div>
</div> </div>
{/* Footer */} {/* Footer */}

View file

@ -716,8 +716,8 @@ export const TeamPage: React.FC = () => {
{/* View Modal */} {/* View Modal */}
{viewProfessional && ( {viewProfessional && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 overflow-y-auto"> <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 animate-fadeIn">
<div className="bg-white rounded-xl max-w-2xl w-full p-0 overflow-hidden shadow-2xl"> <div className="bg-white rounded-xl max-w-2xl w-full p-0 overflow-hidden shadow-2xl max-h-[90vh] overflow-y-auto animate-slideIn">
{/* Header / Avatar Section */} {/* Header / Avatar Section */}
<div className="relative pt-12 pb-6 px-8 text-center bg-gradient-to-b from-gray-50 to-white"> <div className="relative pt-12 pb-6 px-8 text-center bg-gradient-to-b from-gray-50 to-white">
<button <button

View file

@ -149,6 +149,7 @@ export interface Professional {
id: string; id: string;
usuario_id?: string; usuario_id?: string;
nome: string; nome: string;
name?: string; // Restore for compatibility
email?: string; email?: string;
funcao_profissional_id: string; funcao_profissional_id: string;
role?: string; // Optional, for UI display if needed (e.g. from join) role?: string; // Optional, for UI display if needed (e.g. from join)