feat: Implementar sistema completo de gestão de equipe e restrições

- Adicionar restrições de exclusão de FOT quando há eventos associados
- Implementar tooltips para motivos de recusa de eventos por fotógrafos
- Filtrar eventos recusados das listas de fotógrafos
- Adicionar sistema de filtros avançados no modal de gerenciar equipe
- Implementar campos completos de gestão de equipe (fotógrafos, recepcionistas, cinegrafistas, estúdios, pontos de foto, pontos decorados, pontos LED)
- Adicionar colunas de gestão na tabela principal com cálculos automáticos de profissionais faltantes
- Implementar controle de visibilidade da seção de gestão apenas para empresas
- Adicionar status visual "Profissionais OK" com indicadores de completude
- Implementar sistema de cálculo em tempo real de equipe necessária vs confirmada
- Adicionar validações condicionais baseadas no tipo de usuário
This commit is contained in:
JoaoVitorMS0 2026-01-13 13:24:38 -03:00
parent 3430d6bab5
commit a1d5434414
10 changed files with 937 additions and 78 deletions

View file

@ -85,6 +85,16 @@ export const EventForm: React.FC<EventFormProps> = ({
attendees: "",
courseId: "", // Legacy
fotId: "", // New field for FOT linkage
// Novos campos de gestão de equipe
qtdFormandos: "",
qtdFotografos: "",
qtdRecepcionistas: "",
qtdCinegrafistas: "",
qtdEstudios: "",
qtdPontosFoto: "",
qtdPontosDecorados: "",
qtdPontosLed: "",
}
);
@ -193,6 +203,16 @@ export const EventForm: React.FC<EventFormProps> = ({
name: mappedObservacoes, // Map Observacoes to Name field (displayed as "Observacoes do Evento")
briefing: mappedObservacoes, // Sync briefing
address: addressData,
// Mapear campos de gestão de equipe
qtdFormandos: initialData.qtdFormandos || initialData.attendees || "",
qtdFotografos: initialData.qtdFotografos || "",
qtdRecepcionistas: initialData.qtdRecepcionistas || "",
qtdCinegrafistas: initialData.qtdCinegrafistas || "",
qtdEstudios: initialData.qtdEstudios || "",
qtdPontosFoto: initialData.qtdPontosFoto || "",
qtdPontosDecorados: initialData.qtdPontosDecorados || "",
qtdPontosLed: initialData.qtdPontosLed || "",
}));
// 2. Populate derived dropdowns if data exists
@ -381,6 +401,14 @@ export const EventForm: React.FC<EventFormProps> = ({
// Validation
if (!formData.name) return alert("Preencha o tipo de evento");
if (!formData.date) return alert("Preencha a data");
if (!formData.attendees || parseInt(formData.attendees) <= 0) return alert("Preencha o número de formandos");
// Validação condicional apenas para empresas
if ((user?.role === UserRole.BUSINESS_OWNER || user?.role === UserRole.SUPERADMIN)) {
if (!formData.qtdFotografos || parseInt(formData.qtdFotografos) <= 0) {
return alert("Preencha a quantidade de fotógrafos necessários");
}
}
// Ensure typeId is valid
let finalTypeId = formData.typeId;
@ -415,20 +443,16 @@ export const EventForm: React.FC<EventFormProps> = ({
endereco: `${formData.address.street}, ${formData.address.number} - ${formData.address.city}/${formData.address.state}`,
qtd_formandos: parseInt(formData.attendees) || 0,
// Default integer values
qtd_fotografos: 0,
qtd_recepcionistas: 0,
qtd_cinegrafistas: 0,
qtd_estudios: 0,
qtd_ponto_foto: 0,
qtd_ponto_id: 0,
qtd_ponto_decorado: 0,
qtd_pontos_led: 0,
qtd_plataforma_360: 0,
// Campos de gestão de equipe
qtd_fotografos: parseInt(formData.qtdFotografos) || 0,
qtd_recepcionistas: parseInt(formData.qtdRecepcionistas) || 0,
qtd_cinegrafistas: parseInt(formData.qtdCinegrafistas) || 0,
qtd_estudios: parseInt(formData.qtdEstudios) || 0,
qtd_pontos_foto: parseInt(formData.qtdPontosFoto) || 0,
qtd_pontos_decorados: parseInt(formData.qtdPontosDecorados) || 0,
qtd_pontos_led: parseInt(formData.qtdPontosLed) || 0,
status_profissionais: "PENDING",
foto_faltante: 0,
recep_faltante: 0,
cine_faltante: 0,
logistica_observacoes: "",
pre_venda: true
@ -658,19 +682,121 @@ export const EventForm: React.FC<EventFormProps> = ({
</div>
<Input
label="Número de Formandos"
label="Número de Formandos*"
placeholder="Ex: 50"
value={formData.attendees}
onChange={(e) => {
const value = e.target.value;
if (value === "" || /^\d+$/.test(value)) {
setFormData({ ...formData, attendees: value });
setFormData({ ...formData, attendees: value, qtdFormandos: value });
}
}}
type="text"
inputMode="numeric"
required
/>
{/* Seção de Gestão de Equipe - Apenas para Empresas */}
{(user?.role === UserRole.BUSINESS_OWNER || user?.role === UserRole.SUPERADMIN) && (
<div className="bg-blue-50 p-4 rounded-md border border-blue-200">
<h3 className="text-sm font-medium text-blue-700 mb-4 uppercase tracking-wider">Gestão de Equipe e Recursos</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Input
label="Qtd. Fotógrafos*"
placeholder="Ex: 2"
value={formData.qtdFotografos}
onChange={(e) => {
const value = e.target.value;
if (value === "" || /^\d+$/.test(value)) {
setFormData({ ...formData, qtdFotografos: value });
}
}}
type="text"
inputMode="numeric"
required
/>
<Input
label="Qtd. Recepcionistas"
placeholder="Ex: 1"
value={formData.qtdRecepcionistas}
onChange={(e) => {
const value = e.target.value;
if (value === "" || /^\d+$/.test(value)) {
setFormData({ ...formData, qtdRecepcionistas: value });
}
}}
type="text"
inputMode="numeric"
/>
<Input
label="Qtd. Cinegrafistas"
placeholder="Ex: 1"
value={formData.qtdCinegrafistas}
onChange={(e) => {
const value = e.target.value;
if (value === "" || /^\d+$/.test(value)) {
setFormData({ ...formData, qtdCinegrafistas: value });
}
}}
type="text"
inputMode="numeric"
/>
<Input
label="Qtd. Estúdios"
placeholder="Ex: 1"
value={formData.qtdEstudios}
onChange={(e) => {
const value = e.target.value;
if (value === "" || /^\d+$/.test(value)) {
setFormData({ ...formData, qtdEstudios: value });
}
}}
type="text"
inputMode="numeric"
/>
<Input
label="Qtd. Pontos de Foto"
placeholder="Ex: 3"
value={formData.qtdPontosFoto}
onChange={(e) => {
const value = e.target.value;
if (value === "" || /^\d+$/.test(value)) {
setFormData({ ...formData, qtdPontosFoto: value });
}
}}
type="text"
inputMode="numeric"
/>
<Input
label="Qtd. Pontos Decorados"
placeholder="Ex: 2"
value={formData.qtdPontosDecorados}
onChange={(e) => {
const value = e.target.value;
if (value === "" || /^\d+$/.test(value)) {
setFormData({ ...formData, qtdPontosDecorados: value });
}
}}
type="text"
inputMode="numeric"
/>
<Input
label="Qtd. Pontos LED"
placeholder="Ex: 1"
value={formData.qtdPontosLed}
onChange={(e) => {
const value = e.target.value;
if (value === "" || /^\d+$/.test(value)) {
setFormData({ ...formData, qtdPontosLed: value });
}
}}
type="text"
inputMode="numeric"
/>
</div>
</div>
)}
{/* Dynamic FOT Selection */}
<div className="bg-gray-50 p-4 rounded-md border border-gray-200">
<h3 className="text-sm font-medium text-gray-700 mb-4 uppercase tracking-wider">Seleção da Turma</h3>
@ -916,7 +1042,7 @@ export const EventForm: React.FC<EventFormProps> = ({
{/* Mapa Interativo */}
<div className="mt-6">
<label className="block text-sm font-medium text-gray-700 mb-3 tracking-wide uppercase text-xs flex items-center justify-between">
<span>📍 Mapa Interativo - Ajuste a Localização Exata</span>
<span className="flex items-center gap-2"><MapPin className="w-4 h-4 text-[#B9CF32]" /> Mapa Interativo - Ajuste a Localização Exata</span>
{isGeocoding && (
<span className="text-xs text-brand-gold flex items-center normal-case">
<div className="animate-spin h-3 w-3 border-2 border-brand-gold rounded-full border-t-transparent mr-2"></div>

View file

@ -7,9 +7,12 @@ interface EventTableProps {
events: EventData[];
onEventClick: (event: EventData) => void;
onApprove?: (e: React.MouseEvent, eventId: string) => void;
onReject?: (e: React.MouseEvent, eventId: string, reason?: string) => void;
userRole: UserRole;
currentProfessionalId?: string;
onAssignmentResponse?: (e: React.MouseEvent, eventId: string, status: string, reason?: string) => void;
isManagingTeam?: boolean; // Nova prop para determinar se está na tela de gerenciar equipe
professionals?: any[]; // Lista de profissionais para cálculos de gestão de equipe
}
type SortField =
@ -27,14 +30,64 @@ export const EventTable: React.FC<EventTableProps> = ({
events,
onEventClick,
onApprove,
onReject,
userRole,
currentProfessionalId,
onAssignmentResponse,
isManagingTeam = false,
professionals = [],
}) => {
const canApprove =
userRole === UserRole.BUSINESS_OWNER || userRole === UserRole.SUPERADMIN;
const canApprove = isManagingTeam && (userRole === UserRole.BUSINESS_OWNER || userRole === UserRole.SUPERADMIN);
const canReject = userRole === UserRole.BUSINESS_OWNER || userRole === UserRole.SUPERADMIN;
const isPhotographer = userRole === UserRole.PHOTOGRAPHER;
// Função para calcular status da equipe
const calculateTeamStatus = (event: EventData) => {
const assignments = event.assignments || [];
// Contadores de profissionais aceitos por tipo
const acceptedFotografos = assignments.filter(a => {
if (a.status !== "ACEITO") return false;
const professional = professionals.find(p => p.id === a.professionalId);
return professional && (professional.role || "").toLowerCase().includes("fot");
}).length;
const acceptedRecepcionistas = assignments.filter(a => {
if (a.status !== "ACEITO") return false;
const professional = professionals.find(p => p.id === a.professionalId);
return professional && (professional.role || "").toLowerCase().includes("recep");
}).length;
const acceptedCinegrafistas = assignments.filter(a => {
if (a.status !== "ACEITO") return false;
const professional = professionals.find(p => p.id === a.professionalId);
return professional && (professional.role || "").toLowerCase().includes("cine");
}).length;
// Quantidades necessárias
const qtdFotografos = event.qtdFotografos || 0;
const qtdRecepcionistas = event.qtdRecepcionistas || 0;
const qtdCinegrafistas = event.qtdCinegrafistas || 0;
// Calcular faltantes
const fotoFaltante = Math.max(0, qtdFotografos - acceptedFotografos);
const recepFaltante = Math.max(0, qtdRecepcionistas - acceptedRecepcionistas);
const cineFaltante = Math.max(0, qtdCinegrafistas - acceptedCinegrafistas);
// Verificar se todos os profissionais estão OK
const profissionaisOK = fotoFaltante === 0 && recepFaltante === 0 && cineFaltante === 0;
return {
acceptedFotografos,
acceptedRecepcionistas,
acceptedCinegrafistas,
fotoFaltante,
recepFaltante,
cineFaltante,
profissionaisOK
};
};
const [sortField, setSortField] = useState<SortField | null>(null);
const [sortOrder, setSortOrder] = useState<SortOrder>(null);
@ -185,7 +238,7 @@ export const EventTable: React.FC<EventTableProps> = ({
{getStatusDisplay(event.status)}
</span>
{(canApprove || isPhotographer) && (
{(canApprove || canReject || isPhotographer) && (
<div onClick={(e) => e.stopPropagation()} className="flex items-center gap-2">
{canApprove && event.status === EventStatus.PENDING_APPROVAL && (
<button
@ -196,6 +249,19 @@ export const EventTable: React.FC<EventTableProps> = ({
<CheckCircle size={16} />
</button>
)}
{canReject && !canApprove && event.status === EventStatus.PENDING_APPROVAL && (
<button
onClick={(e) => {
const reason = prompt("Motivo da rejeição (opcional):");
onReject?.(e, event.id, reason || undefined);
}}
className="bg-red-500 text-white p-2 rounded-full hover:bg-red-600 transition-colors shadow-sm"
title="Recusar evento"
>
</button>
)}
{isPhotographer && photographerAssignment && (
<>
@ -209,8 +275,8 @@ export const EventTable: React.FC<EventTableProps> = ({
</button>
<button
onClick={(e) => {
const reason = prompt("Motivo da rejeição:");
if (reason) onAssignmentResponse?.(e, event.id, "REJEITADO", reason);
const reason = prompt("Motivo da rejeição (opcional):");
onAssignmentResponse?.(e, event.id, "REJEITADO", reason || undefined);
}}
className="bg-red-100 text-red-700 px-3 py-1 rounded-md text-xs font-bold border border-red-200"
>
@ -310,6 +376,47 @@ export const EventTable: React.FC<EventTableProps> = ({
{getSortIcon("status")}
</div>
</th>
{/* Novas colunas de gestão de equipe */}
{(userRole === UserRole.BUSINESS_OWNER || userRole === UserRole.SUPERADMIN) && (
<>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider">
QTD Form.
</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider">
Fotóg.
</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider">
Recep.
</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider">
Cine.
</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider">
Estúd.
</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider">
Pts. Foto
</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider">
Pts. Dec.
</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider">
Pts. LED
</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider">
Prof. OK?
</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider">
Fot. Falt.
</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider">
Rec. Falt.
</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider">
Cin. Falt.
</th>
</>
)}
{(canApprove || isPhotographer) && (
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider w-32">
Ações
@ -372,7 +479,85 @@ export const EventTable: React.FC<EventTableProps> = ({
{getStatusDisplay(event.status)}
</span>
</td>
{(canApprove || isPhotographer) && (
{/* Novas células de gestão de equipe */}
{(userRole === UserRole.BUSINESS_OWNER || userRole === UserRole.SUPERADMIN) && (() => {
const teamStatus = calculateTeamStatus(event);
return (
<>
<td className="px-3 py-3 text-center">
<span className="text-xs text-gray-900">
{event.qtdFormandos || event.attendees || "-"}
</span>
</td>
<td className="px-3 py-3 text-center">
<span className="text-xs text-gray-900">
{event.qtdFotografos || "-"}
</span>
</td>
<td className="px-3 py-3 text-center">
<span className="text-xs text-gray-900">
{event.qtdRecepcionistas || "-"}
</span>
</td>
<td className="px-3 py-3 text-center">
<span className="text-xs text-gray-900">
{event.qtdCinegrafistas || "-"}
</span>
</td>
<td className="px-3 py-3 text-center">
<span className="text-xs text-gray-900">
{event.qtdEstudios || "-"}
</span>
</td>
<td className="px-3 py-3 text-center">
<span className="text-xs text-gray-900">
{event.qtdPontosFoto || "-"}
</span>
</td>
<td className="px-3 py-3 text-center">
<span className="text-xs text-gray-900">
{event.qtdPontosDecorados || "-"}
</span>
</td>
<td className="px-3 py-3 text-center">
<span className="text-xs text-gray-900">
{event.qtdPontosLed || "-"}
</span>
</td>
<td className="px-3 py-3 text-center">
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
teamStatus.profissionaisOK
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}>
{teamStatus.profissionaisOK ? "✓" : "✗"}
</span>
</td>
<td className="px-3 py-3 text-center">
<span className={`text-xs font-medium ${
teamStatus.fotoFaltante > 0 ? "text-red-600" : "text-green-600"
}`}>
{teamStatus.fotoFaltante}
</span>
</td>
<td className="px-3 py-3 text-center">
<span className={`text-xs font-medium ${
teamStatus.recepFaltante > 0 ? "text-red-600" : "text-green-600"
}`}>
{teamStatus.recepFaltante}
</span>
</td>
<td className="px-3 py-3 text-center">
<span className={`text-xs font-medium ${
teamStatus.cineFaltante > 0 ? "text-red-600" : "text-green-600"
}`}>
{teamStatus.cineFaltante}
</span>
</td>
</>
);
})()}
{(canApprove || canReject || isPhotographer) && (
<td
className="px-4 py-3"
onClick={(e) => e.stopPropagation()}
@ -388,6 +573,19 @@ export const EventTable: React.FC<EventTableProps> = ({
Aprovar
</button>
)}
{canReject && !canApprove && event.status === EventStatus.PENDING_APPROVAL && (
<button
onClick={(e) => {
const reason = prompt("Motivo da rejeição (opcional):");
onReject?.(e, event.id, reason || undefined);
}}
className="bg-red-500 text-white px-2 py-1 rounded text-xs font-semibold hover:bg-red-600 transition-colors flex items-center gap-1 whitespace-nowrap"
title="Recusar evento"
>
Recusar
</button>
)}
{isPhotographer && photographerAssignment && (
<>
@ -402,8 +600,8 @@ export const EventTable: React.FC<EventTableProps> = ({
</button>
<button
onClick={(e) => {
const reason = prompt("Motivo da rejeição:");
if (reason) onAssignmentResponse?.(e, event.id, "REJEITADO", reason);
const reason = prompt("Motivo da rejeição (opcional):");
onAssignmentResponse?.(e, event.id, "REJEITADO", reason || undefined);
}}
className="bg-red-500 text-white px-2 py-1 rounded text-xs font-semibold hover:bg-red-600 transition-colors"
title="Rejeitar"
@ -416,7 +614,21 @@ export const EventTable: React.FC<EventTableProps> = ({
<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" title={photographerAssignment.reason}>Rejeitado</span>
<div className="flex items-center gap-2">
<span className="text-red-600 text-xs font-bold border border-red-200 bg-red-50 px-2 py-1 rounded">Rejeitado</span>
{photographerAssignment.reason && (
<button
onClick={(e) => {
e.stopPropagation();
alert(`Motivo: ${photographerAssignment.reason}`);
}}
className="bg-gray-100 text-gray-600 px-2 py-1 rounded text-xs hover:bg-gray-200 transition-colors"
title="Ver motivo"
>
Motivo
</button>
)}
</div>
)}
</>
)}

View file

@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState } from "react";
import mapboxgl from "mapbox-gl";
import "mapbox-gl/dist/mapbox-gl.css";
import { MapPin, Target } from "lucide-react";
import { MapPin, Target, Lightbulb } from "lucide-react";
interface MapboxMapProps {
initialLat?: number;
@ -185,8 +185,8 @@ export const MapboxMap: React.FC<MapboxMapProps> = ({
{/* Card de Instruções */}
<div className="bg-blue-50 rounded-lg shadow border border-blue-200 p-3">
<p className="font-medium mb-1.5 text-xs text-blue-800">
💡 Como usar:
<p className="font-medium mb-1.5 text-xs text-blue-800 flex items-center gap-1.5">
<Lightbulb className="w-4 h-4" /> Como usar:
</p>
<ul className="text-xs space-y-1 text-blue-700">
<li className="flex items-start">

View file

@ -497,14 +497,16 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Input
label="Banco"
label="Banco *"
type="text"
required
value={formData.banco}
onChange={(e) => handleChange("banco", e.target.value)}
/>
<Input
label="Agência"
label="Agência *"
type="text"
required
value={formData.agencia}
onChange={(e) => {
const value = e.target.value.replace(/\D/g, "");
@ -515,8 +517,9 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Input
label="Conta"
label="Conta *"
type="text"
required
value={formData.conta}
onChange={(e) => handleChange("conta", e.target.value)}
/>

View file

@ -952,7 +952,14 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
const professional = professionals.find((p) => p.usuarioId === userId);
if (!professional) return [];
const professionalId = professional.id;
return events.filter((e) => e.photographerIds.includes(professionalId));
return events.filter((e) => {
// Incluir apenas eventos onde o fotógrafo está designado
if (!e.photographerIds.includes(professionalId)) return false;
// Excluir eventos que foram rejeitados pelo fotógrafo
const assignment = (e.assignments || []).find(a => a.professionalId === professionalId);
return !assignment || assignment.status !== "REJEITADO";
});
}
return [];
};

View file

@ -1,8 +1,9 @@
import React, { useState, useEffect } from "react";
import { useAuth } from "../contexts/AuthContext";
import { useData } from "../contexts/DataContext";
import { UserRole } from "../types";
import { Button } from "../components/Button";
import { getCadastroFot, deleteCadastroFot } from "../services/apiService";
import { getCadastroFot, deleteCadastroFot, checkFotHasEvents } from "../services/apiService";
import { Briefcase, AlertTriangle, Plus, Edit, Trash2, Search, Filter } from "lucide-react";
import { FotForm } from "../components/FotForm";
@ -25,6 +26,7 @@ interface FotData {
export const CourseManagement: React.FC = () => {
const { user } = useAuth();
const { events } = useData();
const [fotList, setFotList] = useState<FotData[]>([]);
const [filteredList, setFilteredList] = useState<FotData[]>([]);
const [isLoading, setIsLoading] = useState(true);
@ -96,12 +98,42 @@ export const CourseManagement: React.FC = () => {
};
const handleDelete = async (id: string, fotNumber: number) => {
if (!window.confirm(`Tem certeza que deseja excluir o FOT ${fotNumber}?`)) return;
const token = localStorage.getItem("token");
if (!token) return;
try {
// Primeiro, verificar se há eventos associados ao FOT
// Verificação local usando os eventos do DataContext
const associatedEvents = events.filter(event =>
event.fotId === id ||
(typeof event.fot === 'number' && event.fot === fotNumber)
);
if (associatedEvents.length > 0) {
alert(
`Não é possível excluir este FOT pois existem ${associatedEvents.length} evento(s) associado(s) a ele.\n\n` +
`Eventos associados:\n${associatedEvents.map(e => `- ${e.name} (${new Date(e.date + "T00:00:00").toLocaleDateString("pt-BR")})`).join('\n')}`
);
return;
}
// Tentar verificação adicional via API se disponível
try {
const checkResult = await checkFotHasEvents(id, token);
if (checkResult.data?.hasEvents) {
alert(
`Não é possível excluir este FOT pois existem ${checkResult.data.eventCount} evento(s) associado(s) a ele no sistema.`
);
return;
}
} catch (apiError) {
// Se a API não estiver disponível, continua com a verificação local
console.log("API check not available, using local verification");
}
// Se chegou até aqui, não há eventos associados
if (!window.confirm(`Tem certeza que deseja excluir o FOT ${fotNumber}?`)) return;
const res = await deleteCadastroFot(id, token);
if (res.error) {
alert(res.error);
@ -325,8 +357,21 @@ export const CourseManagement: React.FC = () => {
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 className="flex items-center gap-2">
<div className="text-sm font-medium text-gray-900">
{item.fot || "-"}
</div>
{(() => {
const hasEvents = events.some(event =>
event.fotId === item.id ||
(typeof event.fot === 'number' && event.fot === item.fot)
);
return hasEvents && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
Com eventos
</span>
);
})()}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
@ -393,13 +438,26 @@ export const CourseManagement: React.FC = () => {
>
<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>
{(() => {
const hasEvents = events.some(event =>
event.fotId === item.id ||
(typeof event.fot === 'number' && event.fot === item.fot)
);
return (
<button
onClick={() => handleDelete(item.id, item.fot)}
disabled={hasEvents}
className={`p-1.5 rounded transition-colors ${
hasEvents
? "text-gray-400 cursor-not-allowed"
: "text-red-600 hover:bg-red-50"
}`}
title={hasEvents ? "Não é possível excluir: FOT possui eventos associados" : "Excluir"}
>
<Trash2 size={16} />
</button>
);
})()}
</div>
</td>
</tr>

View file

@ -100,6 +100,12 @@ export const Dashboard: React.FC<DashboardProps> = ({
});
const [isTeamModalOpen, setIsTeamModalOpen] = useState(false);
const [viewingProfessional, setViewingProfessional] = useState<Professional | null>(null);
// Estados para filtros do modal de equipe
const [teamSearchTerm, setTeamSearchTerm] = useState("");
const [teamRoleFilter, setTeamRoleFilter] = useState("all");
const [teamStatusFilter, setTeamStatusFilter] = useState("all");
const [teamAvailabilityFilter, setTeamAvailabilityFilter] = useState("all");
useEffect(() => {
if (initialView) {
@ -112,6 +118,129 @@ export const Dashboard: React.FC<DashboardProps> = ({
setViewingProfessional(professional);
};
// Função para calcular profissionais faltantes e status
const calculateTeamStatus = (event: EventData) => {
const assignments = event.assignments || [];
// Contadores de profissionais aceitos por tipo
const acceptedFotografos = assignments.filter(a => {
if (a.status !== "ACEITO") return false;
const professional = professionals.find(p => p.id === a.professionalId);
return professional && (professional.role || "").toLowerCase().includes("fot");
}).length;
const acceptedRecepcionistas = assignments.filter(a => {
if (a.status !== "ACEITO") return false;
const professional = professionals.find(p => p.id === a.professionalId);
return professional && (professional.role || "").toLowerCase().includes("recep");
}).length;
const acceptedCinegrafistas = assignments.filter(a => {
if (a.status !== "ACEITO") return false;
const professional = professionals.find(p => p.id === a.professionalId);
return professional && (professional.role || "").toLowerCase().includes("cine");
}).length;
// Quantidades necessárias
const qtdFotografos = event.qtdFotografos || 0;
const qtdRecepcionistas = event.qtdRecepcionistas || 0;
const qtdCinegrafistas = event.qtdCinegrafistas || 0;
// Calcular faltantes
const fotoFaltante = Math.max(0, qtdFotografos - acceptedFotografos);
const recepFaltante = Math.max(0, qtdRecepcionistas - acceptedRecepcionistas);
const cineFaltante = Math.max(0, qtdCinegrafistas - acceptedCinegrafistas);
// Verificar se todos os profissionais estão OK
const profissionaisOK = fotoFaltante === 0 && recepFaltante === 0 && cineFaltante === 0;
return {
acceptedFotografos,
acceptedRecepcionistas,
acceptedCinegrafistas,
fotoFaltante,
recepFaltante,
cineFaltante,
profissionaisOK
};
};
// Função para fechar modal de equipe e limpar filtros
const closeTeamModal = () => {
setIsTeamModalOpen(false);
setTeamSearchTerm("");
setTeamRoleFilter("all");
setTeamStatusFilter("all");
setTeamAvailabilityFilter("all");
};
// Função para filtrar profissionais no modal de equipe
const getFilteredTeamProfessionals = () => {
if (!selectedEvent) return professionals;
return professionals.filter((professional) => {
// Filtro por busca (nome ou email)
if (teamSearchTerm) {
const searchLower = teamSearchTerm.toLowerCase();
const nameMatch = (professional.name || professional.nome || "").toLowerCase().includes(searchLower);
const emailMatch = (professional.email || "").toLowerCase().includes(searchLower);
if (!nameMatch && !emailMatch) return false;
}
// Filtro por função/role
if (teamRoleFilter !== "all") {
const professionalRole = (professional.role || "").toLowerCase();
if (!professionalRole.includes(teamRoleFilter)) return false;
}
// Verificar status do assignment para este evento
const assignment = (selectedEvent.assignments || []).find(
(a) => a.professionalId === professional.id
);
const status = assignment ? assignment.status : null;
const isAssigned = !!status && status !== "REJEITADO";
// Verificar se está ocupado em outro evento na mesma data
const isBusy = !isAssigned && events.some(e =>
e.id !== selectedEvent.id &&
e.date === selectedEvent.date &&
(e.assignments || []).some(a => a.professionalId === professional.id && a.status === 'ACEITO')
);
// Filtro por status
if (teamStatusFilter !== "all") {
switch (teamStatusFilter) {
case "assigned":
if (!isAssigned) return false;
break;
case "available":
if (isAssigned || isBusy) return false;
break;
case "busy":
if (!isBusy) return false;
break;
case "rejected":
if (!status || status !== "REJEITADO") return false;
break;
}
}
// Filtro por disponibilidade
if (teamAvailabilityFilter !== "all") {
switch (teamAvailabilityFilter) {
case "available":
if (isAssigned || isBusy) return false;
break;
case "unavailable":
if (!isAssigned && !isBusy) return false;
break;
}
}
return true;
});
};
// Guard Clause for basic security
if (!user)
return <div className="p-10 text-center">Acesso Negado. Faça login.</div>;
@ -175,6 +304,11 @@ export const Dashboard: React.FC<DashboardProps> = ({
updateEventStatus(eventId, EventStatus.CONFIRMED);
};
const handleReject = (e: React.MouseEvent, eventId: string, reason?: string) => {
e.stopPropagation();
updateEventStatus(eventId, EventStatus.ARCHIVED, reason);
};
const handleAssignmentResponse = async (
e: React.MouseEvent,
eventId: string,
@ -344,9 +478,12 @@ export const Dashboard: React.FC<DashboardProps> = ({
setView("details");
}}
onApprove={handleApprove}
onReject={handleReject}
userRole={user.role}
currentProfessionalId={currentProfessionalId}
onAssignmentResponse={handleAssignmentResponse}
isManagingTeam={false} // Na gestão geral, não está gerenciando equipe
professionals={professionals} // Adicionar lista de profissionais
/>
</div>
)}
@ -607,6 +744,141 @@ export const Dashboard: React.FC<DashboardProps> = ({
{selectedEvent.address.zip && ` | CEP: ${selectedEvent.address.zip}`}
</td>
</tr>
{/* Seção de Gestão de Equipe */}
<tr className="bg-blue-50">
<td colSpan={2} className="px-4 py-3 text-xs font-bold text-blue-700 uppercase tracking-wider">
Gestão de Equipe e Recursos
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
QTD Formandos
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.qtdFormandos || selectedEvent.attendees || "-"}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Fotógrafo
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.qtdFotografos || "-"}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Recepcionista
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.qtdRecepcionistas || "-"}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Cinegrafista
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.qtdCinegrafistas || "-"}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Estúdio
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.qtdEstudios || "-"}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Ponto de Foto
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.qtdPontosFoto || "-"}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Ponto Decorado
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.qtdPontosDecorados || "-"}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Ponto LED
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.qtdPontosLed || "-"}
</td>
</tr>
{/* Status e Faltantes */}
{(() => {
const teamStatus = calculateTeamStatus(selectedEvent);
return (
<>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Profissionais OK?
</td>
<td className="px-4 py-3 text-sm">
<span className={`inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium ${
teamStatus.profissionaisOK
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}>
{teamStatus.profissionaisOK ? (
<>
<CheckCircle size={12} />
Completo
</>
) : (
<>
<X size={12} />
Incompleto
</>
)}
</span>
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Foto Faltante
</td>
<td className="px-4 py-3 text-sm text-gray-900">
<span className={`font-medium ${teamStatus.fotoFaltante > 0 ? "text-red-600" : "text-green-600"}`}>
{teamStatus.fotoFaltante}
</span>
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Recep. Faltante
</td>
<td className="px-4 py-3 text-sm text-gray-900">
<span className={`font-medium ${teamStatus.recepFaltante > 0 ? "text-red-600" : "text-green-600"}`}>
{teamStatus.recepFaltante}
</span>
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Cine Faltante
</td>
<td className="px-4 py-3 text-sm text-gray-900">
<span className={`font-medium ${teamStatus.cineFaltante > 0 ? "text-red-600" : "text-green-600"}`}>
{teamStatus.cineFaltante}
</span>
</td>
</tr>
</>
);
})()}
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Horário
@ -874,7 +1146,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
</p>
</div>
<button
onClick={() => setIsTeamModalOpen(false)}
onClick={closeTeamModal}
className="text-white hover:bg-white/20 rounded-full p-2 transition-colors"
>
<X size={24} />
@ -895,6 +1167,77 @@ export const Dashboard: React.FC<DashboardProps> = ({
</p>
</div>
{/* Filtros e Busca */}
<div className="mb-6 space-y-4">
{/* Barra de busca */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={20} />
<input
type="text"
placeholder="Buscar por nome ou email..."
value={teamSearchTerm}
onChange={(e) => setTeamSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-purple focus:border-transparent"
/>
</div>
{/* Filtros */}
<div className="flex flex-wrap gap-3">
{/* Filtro por função */}
<select
value={teamRoleFilter}
onChange={(e) => setTeamRoleFilter(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-purple focus:border-transparent text-sm"
>
<option value="all">Todas as funções</option>
<option value="fot">Fotógrafos</option>
<option value="video">Videomakers</option>
<option value="editor">Editores</option>
<option value="assist">Assistentes</option>
</select>
{/* Filtro por status */}
<select
value={teamStatusFilter}
onChange={(e) => setTeamStatusFilter(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-purple focus:border-transparent text-sm"
>
<option value="all">Todos os status</option>
<option value="assigned">Designados</option>
<option value="available">Disponíveis</option>
<option value="busy">Em outro evento</option>
<option value="rejected">Rejeitados</option>
</select>
{/* Filtro por disponibilidade */}
<select
value={teamAvailabilityFilter}
onChange={(e) => setTeamAvailabilityFilter(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-purple focus:border-transparent text-sm"
>
<option value="all">Todas disponibilidades</option>
<option value="available">Disponíveis</option>
<option value="unavailable">Indisponíveis</option>
</select>
{/* Botão limpar filtros */}
{(teamSearchTerm || teamRoleFilter !== "all" || teamStatusFilter !== "all" || teamAvailabilityFilter !== "all") && (
<button
onClick={() => {
setTeamSearchTerm("");
setTeamRoleFilter("all");
setTeamStatusFilter("all");
setTeamAvailabilityFilter("all");
}}
className="px-3 py-2 text-sm text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors flex items-center gap-1"
>
<X size={16} />
Limpar filtros
</button>
)}
</div>
</div>
{/* Tabela de Profissionais (Desktop) */}
<div className="hidden md:block overflow-x-auto">
<table className="w-full border-collapse">
@ -918,7 +1261,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
</tr>
</thead>
<tbody>
{professionals.map((photographer) => {
{getFilteredTeamProfessionals().map((photographer) => {
const assignment = (selectedEvent.assignments || []).find(
(a) => a.professionalId === photographer.id
);
@ -986,7 +1329,10 @@ export const Dashboard: React.FC<DashboardProps> = ({
</span>
)}
{status === "REJEITADO" && (
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-red-100 text-red-800 rounded-full text-xs font-medium">
<span
className="inline-flex items-center gap-1.5 px-3 py-1 bg-red-100 text-red-800 rounded-full text-xs font-medium cursor-help"
title={assignment?.reason ? `Motivo: ${assignment.reason}` : "Evento recusado pelo fotógrafo"}
>
<X size={14} />
Recusado
</span>
@ -1017,17 +1363,22 @@ export const Dashboard: React.FC<DashboardProps> = ({
</tr>
);
})}
{professionals.length === 0 && (
{getFilteredTeamProfessionals().length === 0 && (
<tr>
<td colSpan={5} className="p-8 text-center">
<div className="flex flex-col items-center gap-3">
<UserX size={48} className="text-gray-300" />
<p className="text-gray-500 font-medium">
Nenhum profissional disponível para esta data
{professionals.length === 0
? "Nenhum profissional disponível para esta data"
: "Nenhum profissional encontrado com os filtros aplicados"
}
</p>
<p className="text-sm text-gray-400">
Tente selecionar outra data ou entre em contato
com a equipe
{professionals.length === 0
? "Tente selecionar outra data ou entre em contato com a equipe"
: "Tente ajustar os filtros de busca"
}
</p>
</div>
</td>
@ -1039,7 +1390,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
{/* Lista de Cards (Mobile) */}
<div className="md:hidden space-y-4">
{professionals.map((photographer) => {
{getFilteredTeamProfessionals().map((photographer) => {
const assignment = (selectedEvent.assignments || []).find(
(a) => a.professionalId === photographer.id
);
@ -1084,7 +1435,10 @@ export const Dashboard: React.FC<DashboardProps> = ({
</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">
<span
className="inline-flex items-center gap-1 px-2 py-1 bg-red-100 text-red-800 rounded-full text-xs font-medium cursor-help"
title={assignment?.reason ? `Motivo: ${assignment.reason}` : "Evento recusado pelo fotógrafo"}
>
<X size={12} /> Recusado
</span>
)}
@ -1131,7 +1485,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
<div className="border-t border-gray-200 p-6 bg-gray-50 flex justify-end gap-3">
<Button
variant="outline"
onClick={() => setIsTeamModalOpen(false)}
onClick={closeTeamModal}
>
Fechar
</Button>

View file

@ -57,6 +57,7 @@ export const TeamPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState("");
const [roleFilter, setRoleFilter] = useState("all");
const [statusFilter, setStatusFilter] = useState("all");
const [ratingFilter, setRatingFilter] = useState("all");
// Selection & Modals
const [selectedProfessional, setSelectedProfessional] = useState<Professional | null>(null);
@ -419,10 +420,26 @@ export const TeamPage: React.FC = () => {
const roleName = getRoleName(p.funcao_profissional_id);
const matchesRole = roleFilter === "all" || roleName === roleFilter;
// Rating filter logic
const matchesRating = (() => {
if (ratingFilter === "all") return true;
const rating = p.media || 0;
switch (ratingFilter) {
case "5": return rating >= 4.5;
case "4": return rating >= 4 && rating < 4.5;
case "3": return rating >= 3 && rating < 4;
case "2": return rating >= 2 && rating < 3;
case "1": return rating >= 1 && rating < 2;
case "0": return rating < 1;
default: return true;
}
})();
// Hide users with unknown roles
if (roleName === "Desconhecido") return false;
return matchesSearch && matchesRole;
return matchesSearch && matchesRole && matchesRating;
});
const stats = {
@ -474,25 +491,61 @@ export const TeamPage: React.FC = () => {
{/* Filters and Search */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-6">
<div className="flex flex-col md:flex-row gap-4 items-center justify-between">
<div className="relative w-full md:w-96">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={20} />
<input
type="text"
placeholder="Buscar por nome ou email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
/>
<div className="flex flex-col gap-4">
{/* Search and Add Button Row */}
<div className="flex flex-col md:flex-row gap-4 items-center justify-between">
<div className="relative w-full md:w-96">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={20} />
<input
type="text"
placeholder="Buscar por nome ou email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
/>
</div>
<Button onClick={() => {
resetForm();
setShowAddModal(true);
}}>
<Plus size={20} className="mr-2" />
Adicionar Profissional
</Button>
</div>
<Button onClick={() => {
resetForm();
setShowAddModal(true);
}}>
<Plus size={20} className="mr-2" />
Adicionar Profissional
</Button>
{/* Filters Row */}
<div className="flex flex-col md:flex-row gap-4 items-center">
<div className="flex items-center gap-2">
<Filter size={16} className="text-gray-400" />
<span className="text-sm font-medium text-gray-700">Filtros:</span>
</div>
<select
value={roleFilter}
onChange={(e) => setRoleFilter(e.target.value)}
className="px-3 py-1 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-brand-gold"
>
<option value="all">Todas as Funções</option>
{roles.map(role => (
<option key={role.id} value={role.nome}>{role.nome}</option>
))}
</select>
<select
value={ratingFilter}
onChange={(e) => setRatingFilter(e.target.value)}
className="px-3 py-1 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-brand-gold"
>
<option value="all">Todas as Avaliações</option>
<option value="5"> 4.5+ Estrelas</option>
<option value="4"> 4.0 - 4.4 Estrelas</option>
<option value="3"> 3.0 - 3.9 Estrelas</option>
<option value="2"> 2.0 - 2.9 Estrelas</option>
<option value="1"> 1.0 - 1.9 Estrelas</option>
<option value="0"> Menos de 1.0</option>
</select>
</div>
</div>
</div>
@ -713,16 +766,16 @@ export const TeamPage: React.FC = () => {
<h3 className="text-lg font-medium text-gray-900 border-b pb-2 mt-4">Dados Bancários</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700">Banco</label>
<input type="text" value={formData.banco} onChange={e => setFormData({ ...formData, banco: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" />
<label className="block text-sm font-medium text-gray-700">Banco *</label>
<input type="text" required value={formData.banco} onChange={e => setFormData({ ...formData, banco: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Agência</label>
<input type="text" value={formData.agencia} onChange={e => setFormData({ ...formData, agencia: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" />
<label className="block text-sm font-medium text-gray-700">Agência *</label>
<input type="text" required value={formData.agencia} onChange={e => setFormData({ ...formData, agencia: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Chave Pix / Conta</label>
<input type="text" value={formData.conta_pix} onChange={e => setFormData({ ...formData, conta_pix: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" />
<label className="block text-sm font-medium text-gray-700">Chave Pix / Conta *</label>
<input type="text" required value={formData.conta_pix} onChange={e => setFormData({ ...formData, conta_pix: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Tipo Cartão</label>

View file

@ -1,6 +1,9 @@
// Serviço para comunicação com o backend
const API_BASE_URL =
import.meta.env.VITE_API_URL || "http://localhost:3000/api";
import.meta.env.VITE_API_URL || "http://localhost:8080";
console.log('API_BASE_URL:', API_BASE_URL);
console.log('VITE_API_URL:', import.meta.env.VITE_API_URL);
interface ApiResponse<T> {
data: T | null;
@ -420,6 +423,39 @@ export async function updateCadastroFot(id: string, data: any, token: string): P
}
}
/**
* Verifica se eventos associados a um FOT
*/
export async function checkFotHasEvents(fotId: string, token: string): Promise<ApiResponse<{ hasEvents: boolean; eventCount: number }>> {
try {
const response = await fetch(`${API_BASE_URL}/api/cadastro-fot/${fotId}/eventos`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
},
});
if (!response.ok) {
// Se o endpoint não existir, vamos usar a lista de eventos para verificar localmente
return { data: { hasEvents: false, eventCount: 0 }, error: null, isBackendDown: false };
}
const data = await response.json();
return {
data: { hasEvents: data.count > 0, eventCount: data.count },
error: null,
isBackendDown: false,
};
} catch (error) {
return {
data: null,
error: error instanceof Error ? error.message : "Erro ao verificar eventos",
isBackendDown: true,
};
}
}
/**
* Remove um cadastro FOT
*/

View file

@ -132,6 +132,16 @@ export interface EventData {
fotId?: string; // ID da Turma (FOT)
typeId?: string; // ID do Tipo de Evento (UUID)
// Campos de gestão de equipe e recursos
qtdFormandos?: number; // Quantidade de formandos
qtdFotografos?: number; // Quantidade de fotógrafos necessários
qtdRecepcionistas?: number; // Quantidade de recepcionistas necessários
qtdCinegrafistas?: number; // Quantidade de cinegrafistas necessários
qtdEstudios?: number; // Quantidade de estúdios necessários
qtdPontosFoto?: number; // Quantidade de pontos de foto necessários
qtdPontosDecorados?: number; // Quantidade de pontos decorados necessários
qtdPontosLed?: number; // Quantidade de pontos LED necessários
// Fields populated from backend joins (ListAgendas)
fot?: string; // Nome/Número da Turma (FOT)
curso?: string; // Nome do Curso