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:
parent
3430d6bab5
commit
a1d5434414
10 changed files with 937 additions and 78 deletions
|
|
@ -85,6 +85,16 @@ export const EventForm: React.FC<EventFormProps> = ({
|
||||||
attendees: "",
|
attendees: "",
|
||||||
courseId: "", // Legacy
|
courseId: "", // Legacy
|
||||||
fotId: "", // New field for FOT linkage
|
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")
|
name: mappedObservacoes, // Map Observacoes to Name field (displayed as "Observacoes do Evento")
|
||||||
briefing: mappedObservacoes, // Sync briefing
|
briefing: mappedObservacoes, // Sync briefing
|
||||||
address: addressData,
|
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
|
// 2. Populate derived dropdowns if data exists
|
||||||
|
|
@ -381,6 +401,14 @@ export const EventForm: React.FC<EventFormProps> = ({
|
||||||
// Validation
|
// Validation
|
||||||
if (!formData.name) return alert("Preencha o tipo de evento");
|
if (!formData.name) return alert("Preencha o tipo de evento");
|
||||||
if (!formData.date) return alert("Preencha a data");
|
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
|
// Ensure typeId is valid
|
||||||
let finalTypeId = formData.typeId;
|
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}`,
|
endereco: `${formData.address.street}, ${formData.address.number} - ${formData.address.city}/${formData.address.state}`,
|
||||||
qtd_formandos: parseInt(formData.attendees) || 0,
|
qtd_formandos: parseInt(formData.attendees) || 0,
|
||||||
|
|
||||||
// Default integer values
|
// Campos de gestão de equipe
|
||||||
qtd_fotografos: 0,
|
qtd_fotografos: parseInt(formData.qtdFotografos) || 0,
|
||||||
qtd_recepcionistas: 0,
|
qtd_recepcionistas: parseInt(formData.qtdRecepcionistas) || 0,
|
||||||
qtd_cinegrafistas: 0,
|
qtd_cinegrafistas: parseInt(formData.qtdCinegrafistas) || 0,
|
||||||
qtd_estudios: 0,
|
qtd_estudios: parseInt(formData.qtdEstudios) || 0,
|
||||||
qtd_ponto_foto: 0,
|
qtd_pontos_foto: parseInt(formData.qtdPontosFoto) || 0,
|
||||||
qtd_ponto_id: 0,
|
qtd_pontos_decorados: parseInt(formData.qtdPontosDecorados) || 0,
|
||||||
qtd_ponto_decorado: 0,
|
qtd_pontos_led: parseInt(formData.qtdPontosLed) || 0,
|
||||||
qtd_pontos_led: 0,
|
|
||||||
qtd_plataforma_360: 0,
|
|
||||||
|
|
||||||
status_profissionais: "PENDING",
|
status_profissionais: "PENDING",
|
||||||
foto_faltante: 0,
|
|
||||||
recep_faltante: 0,
|
|
||||||
cine_faltante: 0,
|
cine_faltante: 0,
|
||||||
logistica_observacoes: "",
|
logistica_observacoes: "",
|
||||||
pre_venda: true
|
pre_venda: true
|
||||||
|
|
@ -658,19 +682,121 @@ export const EventForm: React.FC<EventFormProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
label="Número de Formandos"
|
label="Número de Formandos*"
|
||||||
placeholder="Ex: 50"
|
placeholder="Ex: 50"
|
||||||
value={formData.attendees}
|
value={formData.attendees}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
if (value === "" || /^\d+$/.test(value)) {
|
if (value === "" || /^\d+$/.test(value)) {
|
||||||
setFormData({ ...formData, attendees: value });
|
setFormData({ ...formData, attendees: value, qtdFormandos: value });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="numeric"
|
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 */}
|
{/* Dynamic FOT Selection */}
|
||||||
<div className="bg-gray-50 p-4 rounded-md border border-gray-200">
|
<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>
|
<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 */}
|
{/* Mapa Interativo */}
|
||||||
<div className="mt-6">
|
<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">
|
<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 && (
|
{isGeocoding && (
|
||||||
<span className="text-xs text-brand-gold flex items-center normal-case">
|
<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>
|
<div className="animate-spin h-3 w-3 border-2 border-brand-gold rounded-full border-t-transparent mr-2"></div>
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,12 @@ interface EventTableProps {
|
||||||
events: EventData[];
|
events: EventData[];
|
||||||
onEventClick: (event: EventData) => void;
|
onEventClick: (event: EventData) => void;
|
||||||
onApprove?: (e: React.MouseEvent, eventId: string) => void;
|
onApprove?: (e: React.MouseEvent, eventId: string) => void;
|
||||||
|
onReject?: (e: React.MouseEvent, eventId: string, reason?: string) => void;
|
||||||
userRole: UserRole;
|
userRole: UserRole;
|
||||||
currentProfessionalId?: string;
|
currentProfessionalId?: string;
|
||||||
onAssignmentResponse?: (e: React.MouseEvent, eventId: string, status: string, reason?: string) => void;
|
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 =
|
type SortField =
|
||||||
|
|
@ -27,14 +30,64 @@ export const EventTable: React.FC<EventTableProps> = ({
|
||||||
events,
|
events,
|
||||||
onEventClick,
|
onEventClick,
|
||||||
onApprove,
|
onApprove,
|
||||||
|
onReject,
|
||||||
userRole,
|
userRole,
|
||||||
currentProfessionalId,
|
currentProfessionalId,
|
||||||
onAssignmentResponse,
|
onAssignmentResponse,
|
||||||
|
isManagingTeam = false,
|
||||||
|
professionals = [],
|
||||||
}) => {
|
}) => {
|
||||||
const canApprove =
|
const canApprove = isManagingTeam && (userRole === UserRole.BUSINESS_OWNER || userRole === UserRole.SUPERADMIN);
|
||||||
userRole === UserRole.BUSINESS_OWNER || userRole === UserRole.SUPERADMIN;
|
const canReject = userRole === UserRole.BUSINESS_OWNER || userRole === UserRole.SUPERADMIN;
|
||||||
const isPhotographer = userRole === UserRole.PHOTOGRAPHER;
|
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 [sortField, setSortField] = useState<SortField | null>(null);
|
||||||
const [sortOrder, setSortOrder] = useState<SortOrder>(null);
|
const [sortOrder, setSortOrder] = useState<SortOrder>(null);
|
||||||
|
|
||||||
|
|
@ -185,7 +238,7 @@ export const EventTable: React.FC<EventTableProps> = ({
|
||||||
{getStatusDisplay(event.status)}
|
{getStatusDisplay(event.status)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{(canApprove || isPhotographer) && (
|
{(canApprove || canReject || isPhotographer) && (
|
||||||
<div onClick={(e) => e.stopPropagation()} className="flex items-center gap-2">
|
<div onClick={(e) => e.stopPropagation()} className="flex items-center gap-2">
|
||||||
{canApprove && event.status === EventStatus.PENDING_APPROVAL && (
|
{canApprove && event.status === EventStatus.PENDING_APPROVAL && (
|
||||||
<button
|
<button
|
||||||
|
|
@ -196,6 +249,19 @@ export const EventTable: React.FC<EventTableProps> = ({
|
||||||
<CheckCircle size={16} />
|
<CheckCircle size={16} />
|
||||||
</button>
|
</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 && (
|
{isPhotographer && photographerAssignment && (
|
||||||
<>
|
<>
|
||||||
|
|
@ -209,8 +275,8 @@ export const EventTable: React.FC<EventTableProps> = ({
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
const reason = prompt("Motivo da rejeição:");
|
const reason = prompt("Motivo da rejeição (opcional):");
|
||||||
if (reason) onAssignmentResponse?.(e, event.id, "REJEITADO", reason);
|
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"
|
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")}
|
{getSortIcon("status")}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</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) && (
|
{(canApprove || isPhotographer) && (
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider w-32">
|
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider w-32">
|
||||||
Ações
|
Ações
|
||||||
|
|
@ -372,7 +479,85 @@ export const EventTable: React.FC<EventTableProps> = ({
|
||||||
{getStatusDisplay(event.status)}
|
{getStatusDisplay(event.status)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</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
|
<td
|
||||||
className="px-4 py-3"
|
className="px-4 py-3"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
|
@ -388,6 +573,19 @@ export const EventTable: React.FC<EventTableProps> = ({
|
||||||
Aprovar
|
Aprovar
|
||||||
</button>
|
</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 && (
|
{isPhotographer && photographerAssignment && (
|
||||||
<>
|
<>
|
||||||
|
|
@ -402,8 +600,8 @@ export const EventTable: React.FC<EventTableProps> = ({
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
const reason = prompt("Motivo da rejeição:");
|
const reason = prompt("Motivo da rejeição (opcional):");
|
||||||
if (reason) onAssignmentResponse?.(e, event.id, "REJEITADO", reason);
|
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"
|
className="bg-red-500 text-white px-2 py-1 rounded text-xs font-semibold hover:bg-red-600 transition-colors"
|
||||||
title="Rejeitar"
|
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>
|
<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" && (
|
{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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import mapboxgl from "mapbox-gl";
|
import mapboxgl from "mapbox-gl";
|
||||||
import "mapbox-gl/dist/mapbox-gl.css";
|
import "mapbox-gl/dist/mapbox-gl.css";
|
||||||
import { MapPin, Target } from "lucide-react";
|
import { MapPin, Target, Lightbulb } from "lucide-react";
|
||||||
|
|
||||||
interface MapboxMapProps {
|
interface MapboxMapProps {
|
||||||
initialLat?: number;
|
initialLat?: number;
|
||||||
|
|
@ -185,8 +185,8 @@ export const MapboxMap: React.FC<MapboxMapProps> = ({
|
||||||
|
|
||||||
{/* Card de Instruções */}
|
{/* Card de Instruções */}
|
||||||
<div className="bg-blue-50 rounded-lg shadow border border-blue-200 p-3">
|
<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">
|
<p className="font-medium mb-1.5 text-xs text-blue-800 flex items-center gap-1.5">
|
||||||
💡 Como usar:
|
<Lightbulb className="w-4 h-4" /> Como usar:
|
||||||
</p>
|
</p>
|
||||||
<ul className="text-xs space-y-1 text-blue-700">
|
<ul className="text-xs space-y-1 text-blue-700">
|
||||||
<li className="flex items-start">
|
<li className="flex items-start">
|
||||||
|
|
|
||||||
|
|
@ -497,14 +497,16 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<Input
|
<Input
|
||||||
label="Banco"
|
label="Banco *"
|
||||||
type="text"
|
type="text"
|
||||||
|
required
|
||||||
value={formData.banco}
|
value={formData.banco}
|
||||||
onChange={(e) => handleChange("banco", e.target.value)}
|
onChange={(e) => handleChange("banco", e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="Agência"
|
label="Agência *"
|
||||||
type="text"
|
type="text"
|
||||||
|
required
|
||||||
value={formData.agencia}
|
value={formData.agencia}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value.replace(/\D/g, "");
|
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">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<Input
|
<Input
|
||||||
label="Conta"
|
label="Conta *"
|
||||||
type="text"
|
type="text"
|
||||||
|
required
|
||||||
value={formData.conta}
|
value={formData.conta}
|
||||||
onChange={(e) => handleChange("conta", e.target.value)}
|
onChange={(e) => handleChange("conta", e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -952,7 +952,14 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
const professional = professionals.find((p) => p.usuarioId === userId);
|
const professional = professionals.find((p) => p.usuarioId === userId);
|
||||||
if (!professional) return [];
|
if (!professional) return [];
|
||||||
const professionalId = professional.id;
|
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 [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
|
import { useData } from "../contexts/DataContext";
|
||||||
import { UserRole } from "../types";
|
import { UserRole } from "../types";
|
||||||
import { Button } from "../components/Button";
|
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 { Briefcase, AlertTriangle, Plus, Edit, Trash2, Search, Filter } from "lucide-react";
|
||||||
import { FotForm } from "../components/FotForm";
|
import { FotForm } from "../components/FotForm";
|
||||||
|
|
||||||
|
|
@ -25,6 +26,7 @@ interface FotData {
|
||||||
|
|
||||||
export const CourseManagement: React.FC = () => {
|
export const CourseManagement: React.FC = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const { events } = useData();
|
||||||
const [fotList, setFotList] = useState<FotData[]>([]);
|
const [fotList, setFotList] = useState<FotData[]>([]);
|
||||||
const [filteredList, setFilteredList] = useState<FotData[]>([]);
|
const [filteredList, setFilteredList] = useState<FotData[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
@ -96,12 +98,42 @@ export const CourseManagement: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (id: string, fotNumber: number) => {
|
const handleDelete = async (id: string, fotNumber: number) => {
|
||||||
if (!window.confirm(`Tem certeza que deseja excluir o FOT ${fotNumber}?`)) return;
|
|
||||||
|
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|
||||||
try {
|
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);
|
const res = await deleteCadastroFot(id, token);
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
alert(res.error);
|
alert(res.error);
|
||||||
|
|
@ -325,8 +357,21 @@ export const CourseManagement: React.FC = () => {
|
||||||
className="hover:bg-gray-50 transition-colors"
|
className="hover:bg-gray-50 transition-colors"
|
||||||
>
|
>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="text-sm font-medium text-gray-900">
|
<div className="flex items-center gap-2">
|
||||||
{item.fot || "-"}
|
<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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
|
@ -393,13 +438,26 @@ export const CourseManagement: React.FC = () => {
|
||||||
>
|
>
|
||||||
<Edit size={16} />
|
<Edit size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
{(() => {
|
||||||
onClick={() => handleDelete(item.id, item.fot)}
|
const hasEvents = events.some(event =>
|
||||||
className="p-1.5 text-red-600 hover:bg-red-50 rounded transition-colors"
|
event.fotId === item.id ||
|
||||||
title="Excluir"
|
(typeof event.fot === 'number' && event.fot === item.fot)
|
||||||
>
|
);
|
||||||
<Trash2 size={16} />
|
return (
|
||||||
</button>
|
<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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,12 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
});
|
});
|
||||||
const [isTeamModalOpen, setIsTeamModalOpen] = useState(false);
|
const [isTeamModalOpen, setIsTeamModalOpen] = useState(false);
|
||||||
const [viewingProfessional, setViewingProfessional] = useState<Professional | null>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
if (initialView) {
|
if (initialView) {
|
||||||
|
|
@ -112,6 +118,129 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
setViewingProfessional(professional);
|
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
|
// Guard Clause for basic security
|
||||||
if (!user)
|
if (!user)
|
||||||
return <div className="p-10 text-center">Acesso Negado. Faça login.</div>;
|
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);
|
updateEventStatus(eventId, EventStatus.CONFIRMED);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleReject = (e: React.MouseEvent, eventId: string, reason?: string) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
updateEventStatus(eventId, EventStatus.ARCHIVED, reason);
|
||||||
|
};
|
||||||
|
|
||||||
const handleAssignmentResponse = async (
|
const handleAssignmentResponse = async (
|
||||||
e: React.MouseEvent,
|
e: React.MouseEvent,
|
||||||
eventId: string,
|
eventId: string,
|
||||||
|
|
@ -344,9 +478,12 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
setView("details");
|
setView("details");
|
||||||
}}
|
}}
|
||||||
onApprove={handleApprove}
|
onApprove={handleApprove}
|
||||||
|
onReject={handleReject}
|
||||||
userRole={user.role}
|
userRole={user.role}
|
||||||
currentProfessionalId={currentProfessionalId}
|
currentProfessionalId={currentProfessionalId}
|
||||||
onAssignmentResponse={handleAssignmentResponse}
|
onAssignmentResponse={handleAssignmentResponse}
|
||||||
|
isManagingTeam={false} // Na gestão geral, não está gerenciando equipe
|
||||||
|
professionals={professionals} // Adicionar lista de profissionais
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -607,6 +744,141 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
{selectedEvent.address.zip && ` | CEP: ${selectedEvent.address.zip}`}
|
{selectedEvent.address.zip && ` | CEP: ${selectedEvent.address.zip}`}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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">
|
<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">
|
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
|
||||||
Horário
|
Horário
|
||||||
|
|
@ -874,7 +1146,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsTeamModalOpen(false)}
|
onClick={closeTeamModal}
|
||||||
className="text-white hover:bg-white/20 rounded-full p-2 transition-colors"
|
className="text-white hover:bg-white/20 rounded-full p-2 transition-colors"
|
||||||
>
|
>
|
||||||
<X size={24} />
|
<X size={24} />
|
||||||
|
|
@ -895,6 +1167,77 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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) */}
|
{/* Tabela de Profissionais (Desktop) */}
|
||||||
<div className="hidden md:block overflow-x-auto">
|
<div className="hidden md:block overflow-x-auto">
|
||||||
<table className="w-full border-collapse">
|
<table className="w-full border-collapse">
|
||||||
|
|
@ -918,7 +1261,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{professionals.map((photographer) => {
|
{getFilteredTeamProfessionals().map((photographer) => {
|
||||||
const assignment = (selectedEvent.assignments || []).find(
|
const assignment = (selectedEvent.assignments || []).find(
|
||||||
(a) => a.professionalId === photographer.id
|
(a) => a.professionalId === photographer.id
|
||||||
);
|
);
|
||||||
|
|
@ -986,7 +1329,10 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{status === "REJEITADO" && (
|
{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} />
|
<X size={14} />
|
||||||
Recusado
|
Recusado
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -1017,17 +1363,22 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{professionals.length === 0 && (
|
{getFilteredTeamProfessionals().length === 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={5} className="p-8 text-center">
|
<td colSpan={5} className="p-8 text-center">
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<UserX size={48} className="text-gray-300" />
|
<UserX size={48} className="text-gray-300" />
|
||||||
<p className="text-gray-500 font-medium">
|
<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>
|
||||||
<p className="text-sm text-gray-400">
|
<p className="text-sm text-gray-400">
|
||||||
Tente selecionar outra data ou entre em contato
|
{professionals.length === 0
|
||||||
com a equipe
|
? "Tente selecionar outra data ou entre em contato com a equipe"
|
||||||
|
: "Tente ajustar os filtros de busca"
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -1039,7 +1390,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
|
|
||||||
{/* Lista de Cards (Mobile) */}
|
{/* Lista de Cards (Mobile) */}
|
||||||
<div className="md:hidden space-y-4">
|
<div className="md:hidden space-y-4">
|
||||||
{professionals.map((photographer) => {
|
{getFilteredTeamProfessionals().map((photographer) => {
|
||||||
const assignment = (selectedEvent.assignments || []).find(
|
const assignment = (selectedEvent.assignments || []).find(
|
||||||
(a) => a.professionalId === photographer.id
|
(a) => a.professionalId === photographer.id
|
||||||
);
|
);
|
||||||
|
|
@ -1084,7 +1435,10 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{status === "REJEITADO" && (
|
{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
|
<X size={12} /> Recusado
|
||||||
</span>
|
</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">
|
<div className="border-t border-gray-200 p-6 bg-gray-50 flex justify-end gap-3">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setIsTeamModalOpen(false)}
|
onClick={closeTeamModal}
|
||||||
>
|
>
|
||||||
Fechar
|
Fechar
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ export const TeamPage: React.FC = () => {
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [roleFilter, setRoleFilter] = useState("all");
|
const [roleFilter, setRoleFilter] = useState("all");
|
||||||
const [statusFilter, setStatusFilter] = useState("all");
|
const [statusFilter, setStatusFilter] = useState("all");
|
||||||
|
const [ratingFilter, setRatingFilter] = useState("all");
|
||||||
|
|
||||||
// Selection & Modals
|
// Selection & Modals
|
||||||
const [selectedProfessional, setSelectedProfessional] = useState<Professional | null>(null);
|
const [selectedProfessional, setSelectedProfessional] = useState<Professional | null>(null);
|
||||||
|
|
@ -419,10 +420,26 @@ export const TeamPage: React.FC = () => {
|
||||||
const roleName = getRoleName(p.funcao_profissional_id);
|
const roleName = getRoleName(p.funcao_profissional_id);
|
||||||
const matchesRole = roleFilter === "all" || roleName === roleFilter;
|
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
|
// Hide users with unknown roles
|
||||||
if (roleName === "Desconhecido") return false;
|
if (roleName === "Desconhecido") return false;
|
||||||
|
|
||||||
return matchesSearch && matchesRole;
|
return matchesSearch && matchesRole && matchesRating;
|
||||||
});
|
});
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
|
|
@ -474,25 +491,61 @@ export const TeamPage: React.FC = () => {
|
||||||
|
|
||||||
{/* Filters and Search */}
|
{/* Filters and Search */}
|
||||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-6">
|
<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="flex flex-col gap-4">
|
||||||
<div className="relative w-full md:w-96">
|
{/* Search and Add Button Row */}
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={20} />
|
<div className="flex flex-col md:flex-row gap-4 items-center justify-between">
|
||||||
<input
|
<div className="relative w-full md:w-96">
|
||||||
type="text"
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={20} />
|
||||||
placeholder="Buscar por nome ou email..."
|
<input
|
||||||
value={searchTerm}
|
type="text"
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
placeholder="Buscar por nome ou email..."
|
||||||
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"
|
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>
|
</div>
|
||||||
|
|
||||||
<Button onClick={() => {
|
{/* Filters Row */}
|
||||||
resetForm();
|
<div className="flex flex-col md:flex-row gap-4 items-center">
|
||||||
setShowAddModal(true);
|
<div className="flex items-center gap-2">
|
||||||
}}>
|
<Filter size={16} className="text-gray-400" />
|
||||||
<Plus size={20} className="mr-2" />
|
<span className="text-sm font-medium text-gray-700">Filtros:</span>
|
||||||
Adicionar Profissional
|
</div>
|
||||||
</Button>
|
|
||||||
|
<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>
|
||||||
</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>
|
<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 className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">Banco</label>
|
<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" />
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">Agência</label>
|
<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" />
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">Chave Pix / Conta</label>
|
<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" />
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">Tipo Cartão</label>
|
<label className="block text-sm font-medium text-gray-700">Tipo Cartão</label>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
// Serviço para comunicação com o backend
|
// Serviço para comunicação com o backend
|
||||||
const API_BASE_URL =
|
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> {
|
interface ApiResponse<T> {
|
||||||
data: T | null;
|
data: T | null;
|
||||||
|
|
@ -420,6 +423,39 @@ export async function updateCadastroFot(id: string, data: any, token: string): P
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica se há 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
|
* Remove um cadastro FOT
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,16 @@ export interface EventData {
|
||||||
fotId?: string; // ID da Turma (FOT)
|
fotId?: string; // ID da Turma (FOT)
|
||||||
typeId?: string; // ID do Tipo de Evento (UUID)
|
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)
|
// Fields populated from backend joins (ListAgendas)
|
||||||
fot?: string; // Nome/Número da Turma (FOT)
|
fot?: string; // Nome/Número da Turma (FOT)
|
||||||
curso?: string; // Nome do Curso
|
curso?: string; // Nome do Curso
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue