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:
commit
9ccd28fc42
8 changed files with 579 additions and 239 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 */}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue