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}
|
||||
onChange={e => {
|
||||
setSelectedCompanyId(e.target.value);
|
||||
setSelectedCourseName("");
|
||||
setSelectedInstitutionName("");
|
||||
setFormData({ ...formData, fotId: "" });
|
||||
}}
|
||||
>
|
||||
|
|
@ -688,65 +686,53 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 1. Curso */}
|
||||
<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) */}
|
||||
{/* Consolidated Turma Selection */}
|
||||
<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
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded focus:ring-brand-gold focus:border-brand-gold"
|
||||
value={formData.fotId || ""}
|
||||
onChange={e => setFormData({ ...formData, fotId: e.target.value })}
|
||||
disabled={!selectedInstitutionName}
|
||||
onChange={e => {
|
||||
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>
|
||||
{filteredYears.map(f => (
|
||||
<option key={f.id} value={f.id}>{f.label}</option>
|
||||
<option value="">Selecione a Turma (Curso - Instituição - Ano)</option>
|
||||
{availableFots.map(f => (
|
||||
<option key={f.id} value={f.id}>
|
||||
{f.curso_nome} - {f.instituicao} - {f.ano_formatura_label} (FOT {f.fot})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{loadingFots && <p className="text-xs text-gray-500 mt-1">Carregando turmas...</p>}
|
||||
</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
|
||||
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)}
|
||||
>
|
||||
Próximo: Localização
|
||||
|
|
|
|||
|
|
@ -144,7 +144,97 @@ export const EventTable: React.FC<EventTableProps> = ({
|
|||
|
||||
return (
|
||||
<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">
|
||||
<thead className="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import React from 'react';
|
||||
import { Professional } from '../types';
|
||||
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 {
|
||||
professional: Professional;
|
||||
|
|
@ -18,10 +21,10 @@ export const ProfessionalDetailsModal: React.FC<ProfessionalDetailsModalProps> =
|
|||
|
||||
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-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 */}
|
||||
<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
|
||||
onClick={onClose}
|
||||
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 */}
|
||||
<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={{
|
||||
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',
|
||||
backgroundPosition: 'center'
|
||||
}}
|
||||
/>
|
||||
|
||||
<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">
|
||||
<span className="px-3 py-1 bg-brand-gold/10 text-brand-black rounded-full text-sm font-medium border border-brand-gold/20">
|
||||
{professional.role}
|
||||
<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>
|
||||
{/* Mock de Avaliação */}
|
||||
{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" /> 4.9
|
||||
<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-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">
|
||||
<h3 className="font-bold text-gray-900 border-b pb-2 flex items-center gap-2">
|
||||
<Building size={18} className="text-brand-gold" />
|
||||
<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>
|
||||
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex items-center gap-3 text-gray-700">
|
||||
<Mail size={16} className="text-gray-400" />
|
||||
<span>{professional.email}</span>
|
||||
<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>
|
||||
<div className="flex items-center gap-3 text-gray-700">
|
||||
<Phone size={16} className="text-gray-400" />
|
||||
<span>{professional.phone || "Não informado"}</span>
|
||||
)}
|
||||
{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>
|
||||
{/* Endereço Mockado se não tiver no tipo, ou usar campos extras do backend se mapeados */}
|
||||
<div className="flex items-center gap-3 text-gray-700">
|
||||
<MapPin size={16} className="text-gray-400" />
|
||||
<span>São Paulo, SP</span>
|
||||
)}
|
||||
{(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>
|
||||
|
||||
{/* Equipamentos */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-bold text-gray-900 border-b pb-2 flex items-center gap-2">
|
||||
<Camera size={18} className="text-brand-gold" />
|
||||
Equipamentos & Habilidades
|
||||
<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>
|
||||
|
||||
{/* Mock de Habilidades / Equipamentos (pois não está no type Professional simples ainda) */}
|
||||
<div className="text-sm text-gray-600 space-y-2">
|
||||
<p>Equipamento Profissional: <span className="text-gray-900">Canon R6, Lentes série L</span></p>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{["Formatura", "Casamento", "Estúdio"].map(tag => (
|
||||
<span key={tag} className="text-xs bg-gray-100 px-2 py-1 rounded text-gray-600">{tag}</span>
|
||||
))}
|
||||
<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>
|
||||
|
||||
<div className="w-full mt-8 bg-gray-50 p-4 rounded-lg border border-gray-100">
|
||||
<div className="flex items-start gap-3">
|
||||
<Award className="text-brand-gold mt-1" size={20} />
|
||||
<div>
|
||||
<h4 className="font-bold text-gray-900 text-sm">Performance</h4>
|
||||
<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>
|
||||
{/* Dados Financeiros */}
|
||||
<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 */}
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -766,11 +766,41 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
|||
const mappedProfs: Professional[] = result.data.map((p: any) => ({
|
||||
id: p.id,
|
||||
usuarioId: p.usuario_id,
|
||||
name: p.nome,
|
||||
nome: p.nome,
|
||||
name: p.nome, // Keep for legacy Dashboard usage
|
||||
email: p.email || "",
|
||||
role: p.funcao_nome || "Fotógrafo",
|
||||
avatar: `https://ui-avatars.com/api/?name=${encodeURIComponent(p.nome)}&background=random`, // Fallback avatar
|
||||
funcao_profissional_id: p.funcao_profissional_id,
|
||||
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,
|
||||
|
||||
// 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
|
||||
}));
|
||||
setProfessionals(mappedProfs);
|
||||
|
|
|
|||
|
|
@ -204,7 +204,67 @@ export const CourseManagement: React.FC = () => {
|
|||
Carregando dados...
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<>
|
||||
<div className="md:hidden divide-y divide-gray-100">
|
||||
{filteredList.length === 0 ? (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
<Briefcase className="w-12 h-12 text-gray-300 mx-auto mb-3" />
|
||||
<p className="text-lg font-medium">Nenhuma turma FOT encontrada</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredList.map((item) => (
|
||||
<div key={item.id} className="p-4 bg-white hover:bg-gray-50 transition-colors">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<span className="font-bold text-gray-900 text-lg block">FOT {item.fot}</span>
|
||||
<div className="text-sm font-medium text-gray-700">{item.empresa_nome}</div>
|
||||
</div>
|
||||
<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"}`}>
|
||||
{item.pre_venda ? "Pré-venda" : "Regular"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 mb-3 bg-gray-50 p-3 rounded-lg border border-gray-100">
|
||||
<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>
|
||||
<p className="text-sm flex items-center gap-1 text-gray-600 mt-1 pt-1 border-t border-gray-200">
|
||||
{item.cidade} - {item.estado}
|
||||
<span className="mx-1">•</span>
|
||||
{item.ano_formatura_label}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{item.observacoes && (
|
||||
<p className="text-xs text-gray-500 italic mb-3 pl-2 border-l-2 border-brand-gold/30">
|
||||
{item.observacoes}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<div className="text-sm font-bold text-gray-900 bg-green-50 px-2 py-1 rounded border border-green-100">
|
||||
{item.gastos_captacao ? item.gastos_captacao.toLocaleString("pt-BR", { style: "currency", currency: "BRL" }) : "R$ 0,00"}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleEdit(item)}
|
||||
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"
|
||||
>
|
||||
<Edit size={16} /> Editar
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(item.id, item.fot)}
|
||||
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"
|
||||
>
|
||||
<Trash2 size={16} /> Excluir
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="hidden md:block overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
|
|
@ -348,6 +408,7 @@ export const CourseManagement: React.FC = () => {
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -849,8 +849,8 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tabela de Profissionais */}
|
||||
<div className="overflow-x-auto">
|
||||
{/* Tabela de Profissionais (Desktop) */}
|
||||
<div className="hidden md:block overflow-x-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-50 border-b-2 border-gray-200">
|
||||
|
|
@ -984,6 +984,86 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
|
||||
{/* Footer */}
|
||||
|
|
|
|||
|
|
@ -716,8 +716,8 @@ export const TeamPage: React.FC = () => {
|
|||
|
||||
{/* View Modal */}
|
||||
{viewProfessional && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 overflow-y-auto">
|
||||
<div className="bg-white rounded-xl max-w-2xl w-full p-0 overflow-hidden shadow-2xl">
|
||||
<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 max-h-[90vh] overflow-y-auto animate-slideIn">
|
||||
{/* Header / Avatar Section */}
|
||||
<div className="relative pt-12 pb-6 px-8 text-center bg-gradient-to-b from-gray-50 to-white">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ export interface Professional {
|
|||
id: string;
|
||||
usuario_id?: string;
|
||||
nome: string;
|
||||
name?: string; // Restore for compatibility
|
||||
email?: string;
|
||||
funcao_profissional_id: string;
|
||||
role?: string; // Optional, for UI display if needed (e.g. from join)
|
||||
|
|
|
|||
Loading…
Reference in a new issue