- Backend: Implementada lógica de importação de Agenda (Upsert) em `internal/agenda`. - Backend: Criadas queries SQL para busca de FOT e Tipos de Evento. - Frontend: Adicionada aba de Importação de Agenda em `ImportData.tsx`. - Frontend: Implementado Parser de Excel para Agenda com tratamento de datas. - UX: Adicionada Barra de Rolagem Superior Sincronizada na Tabela de Eventos. - UX: Implementado `LoadingScreen` global unificado (Auth + DataContext). - Perf: Adicionada Paginação no `EventTable` para resolver travamentos com grandes listas. - Security: Proteção de rotas de importação (RequireWriteAccess).
1200 lines
38 KiB
TypeScript
1200 lines
38 KiB
TypeScript
import React, { createContext, useContext, useState, ReactNode, useEffect } from "react";
|
|
import { useAuth } from "./AuthContext";
|
|
import { getPendingUsers, approveUser as apiApproveUser, getProfessionals, assignProfessional as apiAssignProfessional, removeProfessional as apiRemoveProfessional, updateEventStatus as apiUpdateStatus, updateAssignmentStatus as apiUpdateAssignmentStatus, updateAgenda as apiUpdateAgenda } from "../services/apiService";
|
|
import {
|
|
EventData,
|
|
EventStatus,
|
|
EventType,
|
|
Institution,
|
|
Course,
|
|
User,
|
|
UserApprovalStatus,
|
|
UserRole,
|
|
Professional,
|
|
} from "../types";
|
|
|
|
// Initial Mock Data
|
|
const INITIAL_INSTITUTIONS: Institution[] = [
|
|
{
|
|
id: "inst-1",
|
|
name: "Universidade Federal do Rio Grande do Sul",
|
|
type: "Universidade Pública",
|
|
phone: "(51) 3308-3333",
|
|
email: "eventos@ufrgs.br",
|
|
address: {
|
|
street: "Av. Paulo Gama",
|
|
number: "110",
|
|
city: "Porto Alegre",
|
|
state: "RS",
|
|
zip: "90040-060",
|
|
},
|
|
description:
|
|
"Campus Central - Principais eventos realizados no Salão de Atos",
|
|
ownerId: "client-1",
|
|
},
|
|
];
|
|
|
|
const INITIAL_EVENTS: EventData[] = [
|
|
{
|
|
id: "1",
|
|
name: "Formatura Engenharia Civil",
|
|
date: "2025-12-05",
|
|
time: "19:00",
|
|
type: EventType.GRADUATION,
|
|
status: EventStatus.CONFIRMED,
|
|
address: {
|
|
street: "Av. das Hortênsias",
|
|
number: "1200",
|
|
city: "Gramado",
|
|
state: "RS",
|
|
zip: "95670-000",
|
|
},
|
|
briefing:
|
|
"Cerimônia de formatura com 120 formandos. Foco em fotos individuais e da turma.",
|
|
coverImage: "https://picsum.photos/id/1059/800/400",
|
|
contacts: [
|
|
{
|
|
id: "c1",
|
|
name: "Comissão de Formatura",
|
|
role: "Organizador",
|
|
phone: "51 99999-1111",
|
|
email: "formatura@email.com",
|
|
},
|
|
],
|
|
checklist: [],
|
|
ownerId: "client-1",
|
|
photographerIds: ["photographer-1", "photographer-2"],
|
|
institutionId: "inst-1",
|
|
},
|
|
{
|
|
id: "2",
|
|
name: "Colação de Grau Medicina",
|
|
date: "2025-12-05",
|
|
time: "10:00",
|
|
type: EventType.COLATION,
|
|
status: EventStatus.CONFIRMED,
|
|
address: {
|
|
street: "Rua Olimpíadas",
|
|
number: "205",
|
|
city: "São Paulo",
|
|
state: "SP",
|
|
zip: "04551-000",
|
|
},
|
|
briefing:
|
|
"Colação de grau solene. Capturar juramento e entrega de diplomas.",
|
|
coverImage: "https://picsum.photos/id/3/800/400",
|
|
contacts: [
|
|
{
|
|
id: "c2",
|
|
name: "Secretaria Acadêmica",
|
|
role: "Coordenador",
|
|
phone: "11 98888-2222",
|
|
email: "academico@med.br",
|
|
},
|
|
],
|
|
checklist: [],
|
|
ownerId: "client-1",
|
|
photographerIds: ["photographer-1"],
|
|
},
|
|
{
|
|
id: "3",
|
|
name: "Semana Acadêmica Direito",
|
|
date: "2025-12-05",
|
|
time: "14:00",
|
|
type: EventType.ACADEMIC_WEEK,
|
|
status: EventStatus.IN_PROGRESS,
|
|
address: {
|
|
street: "Av. Paulista",
|
|
number: "1500",
|
|
city: "São Paulo",
|
|
state: "SP",
|
|
zip: "01310-100",
|
|
},
|
|
briefing: "Palestras e painéis durante toda a semana. Cobertura de 3 dias.",
|
|
coverImage: "https://picsum.photos/id/10/800/400",
|
|
contacts: [],
|
|
checklist: [],
|
|
ownerId: "client-2",
|
|
photographerIds: ["photographer-2"],
|
|
},
|
|
{
|
|
id: "4",
|
|
name: "Defesa de Doutorado - Maria Silva",
|
|
date: "2025-12-05",
|
|
time: "15:30",
|
|
type: EventType.DEFENSE,
|
|
status: EventStatus.CONFIRMED,
|
|
address: {
|
|
street: "Rua Ramiro Barcelos",
|
|
number: "2600",
|
|
city: "Porto Alegre",
|
|
state: "RS",
|
|
zip: "90035-003",
|
|
},
|
|
briefing:
|
|
"Defesa de tese em sala fechada. Fotos discretas da apresentação e banca.",
|
|
coverImage: "https://picsum.photos/id/20/800/400",
|
|
contacts: [
|
|
{
|
|
id: "c3",
|
|
name: "Prof. João Santos",
|
|
role: "Orientador",
|
|
phone: "51 97777-3333",
|
|
email: "joao@univ.br",
|
|
},
|
|
],
|
|
checklist: [],
|
|
ownerId: "client-1",
|
|
photographerIds: ["photographer-1"],
|
|
},
|
|
{
|
|
id: "5",
|
|
name: "Semana de Calouros 2026",
|
|
date: "2025-12-06",
|
|
time: "09:00",
|
|
type: EventType.FRESHMAN_WEEK,
|
|
status: EventStatus.PENDING_APPROVAL,
|
|
address: {
|
|
street: "Campus Universitário",
|
|
number: "s/n",
|
|
city: "Curitiba",
|
|
state: "PR",
|
|
zip: "80060-000",
|
|
},
|
|
briefing: "Recepção dos calouros com atividades de integração e gincanas.",
|
|
coverImage: "https://picsum.photos/id/30/800/400",
|
|
contacts: [],
|
|
checklist: [],
|
|
ownerId: "client-2",
|
|
photographerIds: [],
|
|
},
|
|
{
|
|
id: "6",
|
|
name: "Formatura Administração",
|
|
date: "2025-12-06",
|
|
time: "20:00",
|
|
type: EventType.GRADUATION,
|
|
status: EventStatus.CONFIRMED,
|
|
address: {
|
|
street: "Av. Ipiranga",
|
|
number: "6681",
|
|
city: "Porto Alegre",
|
|
state: "RS",
|
|
zip: "90619-900",
|
|
},
|
|
briefing: "Formatura noturna com jantar. Fotos da cerimônia e festa.",
|
|
coverImage: "https://picsum.photos/id/40/800/400",
|
|
contacts: [
|
|
{
|
|
id: "c4",
|
|
name: "Lucas Oliveira",
|
|
role: "Presidente da Comissão",
|
|
phone: "51 96666-4444",
|
|
email: "lucas@formatura.com",
|
|
},
|
|
],
|
|
checklist: [],
|
|
ownerId: "client-1",
|
|
photographerIds: ["photographer-2"],
|
|
},
|
|
{
|
|
id: "7",
|
|
name: "Congresso de Tecnologia",
|
|
date: "2025-12-06",
|
|
time: "08:30",
|
|
type: EventType.SYMPOSIUM,
|
|
status: EventStatus.CONFIRMED,
|
|
address: {
|
|
street: "Av. das Nações Unidas",
|
|
number: "12901",
|
|
city: "São Paulo",
|
|
state: "SP",
|
|
zip: "04578-000",
|
|
},
|
|
briefing:
|
|
"Congresso com múltiplas salas. Cobrir palestrantes principais e stands.",
|
|
coverImage: "https://picsum.photos/id/50/800/400",
|
|
contacts: [
|
|
{
|
|
id: "c5",
|
|
name: "Eventos Tech",
|
|
role: "Organizadora",
|
|
phone: "11 95555-5555",
|
|
email: "contato@eventostech.com",
|
|
},
|
|
],
|
|
checklist: [],
|
|
ownerId: "client-2",
|
|
photographerIds: ["photographer-1", "photographer-3"],
|
|
},
|
|
{
|
|
id: "8",
|
|
name: "Campeonato Universitário de Futsal",
|
|
date: "2025-12-06",
|
|
time: "16:00",
|
|
type: EventType.SPORTS_EVENT,
|
|
status: EventStatus.CONFIRMED,
|
|
address: {
|
|
street: "Rua dos Esportes",
|
|
number: "500",
|
|
city: "Gramado",
|
|
state: "RS",
|
|
zip: "95670-100",
|
|
},
|
|
briefing: "Final do campeonato. Fotos dinâmicas da partida e premiação.",
|
|
coverImage: "https://picsum.photos/id/60/800/400",
|
|
contacts: [],
|
|
checklist: [],
|
|
ownerId: "client-1",
|
|
photographerIds: ["photographer-3"],
|
|
},
|
|
{
|
|
id: "9",
|
|
name: "Colação de Grau Odontologia",
|
|
date: "2025-12-07",
|
|
time: "11:00",
|
|
type: EventType.COLATION,
|
|
status: EventStatus.PLANNING,
|
|
address: {
|
|
street: "Rua Voluntários da Pátria",
|
|
number: "89",
|
|
city: "Porto Alegre",
|
|
state: "RS",
|
|
zip: "90230-010",
|
|
},
|
|
briefing: "Cerimônia formal de colação. Fotos individuais e em grupo.",
|
|
coverImage: "https://picsum.photos/id/70/800/400",
|
|
contacts: [
|
|
{
|
|
id: "c6",
|
|
name: "Direção da Faculdade",
|
|
role: "Coordenador",
|
|
phone: "51 94444-6666",
|
|
email: "direcao@odonto.edu",
|
|
},
|
|
],
|
|
checklist: [],
|
|
ownerId: "client-1",
|
|
photographerIds: ["photographer-2"],
|
|
},
|
|
{
|
|
id: "10",
|
|
name: "Festival Cultural Universitário",
|
|
date: "2025-12-07",
|
|
time: "18:00",
|
|
type: EventType.CULTURAL_EVENT,
|
|
status: EventStatus.CONFIRMED,
|
|
address: {
|
|
street: "Praça da República",
|
|
number: "s/n",
|
|
city: "São Paulo",
|
|
state: "SP",
|
|
zip: "01045-000",
|
|
},
|
|
briefing:
|
|
"Festival com apresentações musicais e teatrais. Cobertura completa.",
|
|
coverImage: "https://picsum.photos/id/80/800/400",
|
|
contacts: [],
|
|
checklist: [],
|
|
ownerId: "client-2",
|
|
photographerIds: ["photographer-1"],
|
|
},
|
|
{
|
|
id: "11",
|
|
name: "Defesa de Mestrado - Pedro Costa",
|
|
date: "2025-12-07",
|
|
time: "14:00",
|
|
type: EventType.DEFENSE,
|
|
status: EventStatus.CONFIRMED,
|
|
address: {
|
|
street: "Av. Bento Gonçalves",
|
|
number: "9500",
|
|
city: "Porto Alegre",
|
|
state: "RS",
|
|
zip: "91509-900",
|
|
},
|
|
briefing:
|
|
"Defesa de dissertação. Registro da apresentação e momento da aprovação.",
|
|
coverImage: "https://picsum.photos/id/90/800/400",
|
|
contacts: [],
|
|
checklist: [],
|
|
ownerId: "client-1",
|
|
photographerIds: ["photographer-3"],
|
|
},
|
|
{
|
|
id: "12",
|
|
name: "Formatura Psicologia",
|
|
date: "2025-12-08",
|
|
time: "19:30",
|
|
type: EventType.GRADUATION,
|
|
status: EventStatus.CONFIRMED,
|
|
address: {
|
|
street: "Av. Protásio Alves",
|
|
number: "7000",
|
|
city: "Porto Alegre",
|
|
state: "RS",
|
|
zip: "91310-000",
|
|
},
|
|
briefing: "Formatura emotiva com homenagens. Foco em momentos especiais.",
|
|
coverImage: "https://picsum.photos/id/100/800/400",
|
|
contacts: [
|
|
{
|
|
id: "c7",
|
|
name: "Ana Paula",
|
|
role: "Formanda",
|
|
phone: "51 93333-7777",
|
|
email: "ana@email.com",
|
|
},
|
|
],
|
|
checklist: [],
|
|
ownerId: "client-1",
|
|
photographerIds: ["photographer-1", "photographer-2"],
|
|
},
|
|
{
|
|
id: "13",
|
|
name: "Simpósio de Engenharia",
|
|
date: "2025-12-08",
|
|
time: "09:00",
|
|
type: EventType.SYMPOSIUM,
|
|
status: EventStatus.CONFIRMED,
|
|
address: {
|
|
street: "Av. Sertório",
|
|
number: "6600",
|
|
city: "Porto Alegre",
|
|
state: "RS",
|
|
zip: "91040-000",
|
|
},
|
|
briefing: "Apresentações técnicas e workshops. Cobrir painéis principais.",
|
|
coverImage: "https://picsum.photos/id/110/800/400",
|
|
contacts: [],
|
|
checklist: [],
|
|
ownerId: "client-1",
|
|
photographerIds: ["photographer-2"],
|
|
},
|
|
{
|
|
id: "14",
|
|
name: "Torneio de Vôlei Universitário",
|
|
date: "2025-12-08",
|
|
time: "15:00",
|
|
type: EventType.SPORTS_EVENT,
|
|
status: EventStatus.IN_PROGRESS,
|
|
address: {
|
|
street: "Rua Faria Santos",
|
|
number: "100",
|
|
city: "Curitiba",
|
|
state: "PR",
|
|
zip: "80060-150",
|
|
},
|
|
briefing: "Semifinais e final. Fotos de ação e torcida.",
|
|
coverImage: "https://picsum.photos/id/120/800/400",
|
|
contacts: [],
|
|
checklist: [],
|
|
ownerId: "client-2",
|
|
photographerIds: ["photographer-3"],
|
|
},
|
|
{
|
|
id: "15",
|
|
name: "Colação de Grau Enfermagem",
|
|
date: "2025-12-09",
|
|
time: "10:30",
|
|
type: EventType.COLATION,
|
|
status: EventStatus.CONFIRMED,
|
|
address: {
|
|
street: "Rua São Manoel",
|
|
number: "963",
|
|
city: "São Paulo",
|
|
state: "SP",
|
|
zip: "01330-001",
|
|
},
|
|
briefing: "Colação com juramento de Florence Nightingale. Momento solene.",
|
|
coverImage: "https://picsum.photos/id/130/800/400",
|
|
contacts: [
|
|
{
|
|
id: "c8",
|
|
name: "Coordenação de Enfermagem",
|
|
role: "Coordenador",
|
|
phone: "11 92222-8888",
|
|
email: "coord@enf.br",
|
|
},
|
|
],
|
|
checklist: [],
|
|
ownerId: "client-2",
|
|
photographerIds: ["photographer-1"],
|
|
},
|
|
{
|
|
id: "16",
|
|
name: "Semana Acadêmica Biomedicina",
|
|
date: "2025-12-09",
|
|
time: "13:00",
|
|
type: EventType.ACADEMIC_WEEK,
|
|
status: EventStatus.PLANNING,
|
|
address: {
|
|
street: "Av. Independência",
|
|
number: "2293",
|
|
city: "Porto Alegre",
|
|
state: "RS",
|
|
zip: "90035-075",
|
|
},
|
|
briefing: "Palestras e atividades práticas. Cobertura de 2 dias.",
|
|
coverImage: "https://picsum.photos/id/140/800/400",
|
|
contacts: [],
|
|
checklist: [],
|
|
ownerId: "client-1",
|
|
photographerIds: [],
|
|
},
|
|
{
|
|
id: "17",
|
|
name: "Formatura Ciências Contábeis",
|
|
date: "2025-12-09",
|
|
time: "20:30",
|
|
type: EventType.GRADUATION,
|
|
status: EventStatus.CONFIRMED,
|
|
address: {
|
|
street: "Av. das Américas",
|
|
number: "3500",
|
|
city: "Gramado",
|
|
state: "RS",
|
|
zip: "95670-200",
|
|
},
|
|
briefing:
|
|
"Formatura elegante em hotel. Cobertura completa da cerimônia e recepção.",
|
|
coverImage: "https://picsum.photos/id/150/800/400",
|
|
contacts: [
|
|
{
|
|
id: "c9",
|
|
name: "Rodrigo Almeida",
|
|
role: "Tesoureiro",
|
|
phone: "51 91111-9999",
|
|
email: "rodrigo@turma.com",
|
|
},
|
|
],
|
|
checklist: [],
|
|
ownerId: "client-1",
|
|
photographerIds: ["photographer-2", "photographer-3"],
|
|
},
|
|
{
|
|
id: "18",
|
|
name: "Defesa de TCC - Turma 2025",
|
|
date: "2025-12-09",
|
|
time: "16:30",
|
|
type: EventType.DEFENSE,
|
|
status: EventStatus.CONFIRMED,
|
|
address: {
|
|
street: "Rua Marquês do Pombal",
|
|
number: "2000",
|
|
city: "Porto Alegre",
|
|
state: "RS",
|
|
zip: "90540-000",
|
|
},
|
|
briefing:
|
|
"Múltiplas defesas sequenciais. Fotos rápidas de cada apresentação.",
|
|
coverImage: "https://picsum.photos/id/160/800/400",
|
|
contacts: [],
|
|
checklist: [],
|
|
ownerId: "client-1",
|
|
photographerIds: ["photographer-1"],
|
|
},
|
|
{
|
|
id: "19",
|
|
name: "Festival de Música Universitária",
|
|
date: "2025-12-10",
|
|
time: "19:00",
|
|
type: EventType.CULTURAL_EVENT,
|
|
status: EventStatus.PENDING_APPROVAL,
|
|
address: {
|
|
street: "Parque da Redenção",
|
|
number: "s/n",
|
|
city: "Porto Alegre",
|
|
state: "RS",
|
|
zip: "90040-000",
|
|
},
|
|
briefing:
|
|
"Festival ao ar livre com várias bandas. Fotos de palco e público.",
|
|
coverImage: "https://picsum.photos/id/170/800/400",
|
|
contacts: [],
|
|
checklist: [],
|
|
ownerId: "client-2",
|
|
photographerIds: [],
|
|
},
|
|
{
|
|
id: "20",
|
|
name: "Colação de Grau Arquitetura",
|
|
date: "2025-12-10",
|
|
time: "11:30",
|
|
type: EventType.COLATION,
|
|
status: EventStatus.CONFIRMED,
|
|
address: {
|
|
street: "Av. Borges de Medeiros",
|
|
number: "1501",
|
|
city: "Gramado",
|
|
state: "RS",
|
|
zip: "95670-300",
|
|
},
|
|
briefing: "Cerimônia especial com exposição de projetos. Fotos criativas.",
|
|
coverImage: "https://picsum.photos/id/180/800/400",
|
|
contacts: [
|
|
{
|
|
id: "c10",
|
|
name: "Atelier Arquitetura",
|
|
role: "Escritório Parceiro",
|
|
phone: "51 90000-1010",
|
|
email: "contato@atelier.arq",
|
|
},
|
|
],
|
|
checklist: [],
|
|
ownerId: "client-1",
|
|
photographerIds: ["photographer-3"],
|
|
},
|
|
];
|
|
|
|
// Initial Mock Courses
|
|
const INITIAL_COURSES: Course[] = [
|
|
{
|
|
id: "course-1",
|
|
name: "Engenharia Civil 2025",
|
|
institutionId: "inst-1",
|
|
year: 2025,
|
|
semester: 2,
|
|
graduationType: "Bacharelado",
|
|
createdAt: new Date().toISOString(),
|
|
createdBy: "admin-1",
|
|
isActive: true,
|
|
},
|
|
{
|
|
id: "course-2",
|
|
name: "Medicina - Turma A 2025",
|
|
institutionId: "inst-1",
|
|
year: 2025,
|
|
semester: 1,
|
|
graduationType: "Bacharelado",
|
|
createdAt: new Date().toISOString(),
|
|
createdBy: "admin-1",
|
|
isActive: true,
|
|
},
|
|
{
|
|
id: "course-3",
|
|
name: "Direito Noturno 2025",
|
|
institutionId: "inst-1",
|
|
year: 2025,
|
|
semester: 2,
|
|
graduationType: "Bacharelado",
|
|
createdAt: new Date().toISOString(),
|
|
createdBy: "admin-1",
|
|
isActive: true,
|
|
},
|
|
];
|
|
|
|
interface DataContextType {
|
|
events: EventData[];
|
|
institutions: Institution[];
|
|
courses: Course[];
|
|
pendingUsers: User[];
|
|
addEvent: (event: EventData) => void;
|
|
updateEventStatus: (id: string, status: EventStatus) => void;
|
|
assignPhotographer: (eventId: string, photographerId: string) => void;
|
|
getEventsByRole: (userId: string, role: string) => EventData[];
|
|
addInstitution: (institution: Institution) => void;
|
|
updateInstitution: (id: string, institution: Partial<Institution>) => void;
|
|
getInstitutionsByUserId: (userId: string) => Institution[];
|
|
getInstitutionById: (id: string) => Institution | undefined;
|
|
addCourse: (course: Course) => void;
|
|
updateCourse: (id: string, course: Partial<Course>) => void;
|
|
getCoursesByInstitutionId: (institutionId: string) => Course[];
|
|
getActiveCoursesByInstitutionId: (institutionId: string) => Course[];
|
|
getCourseById: (id: string) => Course | undefined;
|
|
registerPendingUser: (userData: { id: string; name: string; email: string; phone: string; registeredInstitution?: string }) => void;
|
|
|
|
approveUser: (userId: string) => void;
|
|
rejectUser: (userId: string) => void;
|
|
professionals: Professional[];
|
|
respondToAssignment: (eventId: string, status: string, reason?: string) => Promise<void>;
|
|
updateEventDetails: (id: string, data: any) => Promise<void>;
|
|
functions: { id: string; nome: string }[];
|
|
isLoading: boolean;
|
|
}
|
|
|
|
const DataContext = createContext<DataContextType | undefined>(undefined);
|
|
|
|
|
|
export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
|
children,
|
|
}) => {
|
|
const { token, user } = useAuth(); // Consume Auth Context
|
|
const [events, setEvents] = useState<EventData[]>([]);
|
|
const [institutions, setInstitutions] =
|
|
useState<Institution[]>(INITIAL_INSTITUTIONS);
|
|
const [courses, setCourses] = useState<Course[]>(INITIAL_COURSES);
|
|
|
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
|
|
const [pendingUsers, setPendingUsers] = useState<User[]>([]);
|
|
const [professionals, setProfessionals] = useState<Professional[]>([]);
|
|
const [functions, setFunctions] = useState<{ id: string; nome: string }[]>([]);
|
|
|
|
// Fetch events from API
|
|
useEffect(() => {
|
|
const fetchEvents = async () => {
|
|
// Use token from context or fallback to localStorage if context not ready (though context is preferred sources of truth)
|
|
const visibleToken = token || localStorage.getItem("token");
|
|
|
|
if (visibleToken) {
|
|
setIsLoading(true);
|
|
try {
|
|
// Import dynamic to avoid circular dependency if any, or just use imported service
|
|
const { getAgendas, getFunctions } = await import("../services/apiService");
|
|
|
|
// Fetch Functions (Roles)
|
|
getFunctions().then(res => {
|
|
if (res.data) setFunctions(res.data);
|
|
});
|
|
|
|
const result = await getAgendas(visibleToken);
|
|
console.log("Raw Agenda Data:", result.data); // Debug logging
|
|
if (result.data) {
|
|
console.log("Sample event from backend:", result.data[0]); // DEBUG: Ver estrutura e status
|
|
|
|
// Map backend status to frontend EventStatus
|
|
const mapStatus = (backendStatus: string): EventStatus => {
|
|
const statusMap: Record<string, EventStatus> = {
|
|
"Pendente": EventStatus.PENDING_APPROVAL,
|
|
"Aguardando Aprovação": EventStatus.PENDING_APPROVAL,
|
|
"PENDING_APPROVAL": EventStatus.PENDING_APPROVAL,
|
|
"Confirmado": EventStatus.CONFIRMED,
|
|
"CONFIRMED": EventStatus.CONFIRMED,
|
|
"Em Planejamento": EventStatus.PLANNING,
|
|
"PLANNING": EventStatus.PLANNING,
|
|
"Em Execução": EventStatus.IN_PROGRESS,
|
|
"IN_PROGRESS": EventStatus.IN_PROGRESS,
|
|
"Entregue": EventStatus.DELIVERED,
|
|
"DELIVERED": EventStatus.DELIVERED,
|
|
"Arquivado": EventStatus.ARCHIVED,
|
|
"ARCHIVED": EventStatus.ARCHIVED,
|
|
};
|
|
return statusMap[backendStatus] || EventStatus.PENDING_APPROVAL;
|
|
};
|
|
|
|
const mappedEvents: EventData[] = result.data.map((e: any) => ({
|
|
id: e.id,
|
|
name: e.observacoes_evento || e.tipo_evento_nome || "Evento sem nome", // Fallback mapping
|
|
date: e.data_evento ? e.data_evento.split('T')[0] : "",
|
|
time: e.horario || "00:00",
|
|
type: (e.tipo_evento_nome || "Outro") as EventType, // Map string to enum if possible, or keep string
|
|
status: mapStatus(e.status), // Map from backend status with fallback
|
|
address: {
|
|
street: e.endereco ? e.endereco.split(',')[0] : "",
|
|
number: e.endereco ? e.endereco.split(',')[1]?.split('-')[0]?.trim() || "" : "",
|
|
city: e.endereco ? e.endereco.split('-')[1]?.split('/')[0]?.trim() || "" : "",
|
|
state: e.endereco ? e.endereco.split('/')[1]?.trim() || "" : "",
|
|
zip: "",
|
|
mapLink: e.local_evento?.startsWith('http') ? e.local_evento : undefined
|
|
},
|
|
briefing: e.observacoes_evento || "",
|
|
coverImage: "https://picsum.photos/id/10/800/400", // Placeholder
|
|
contacts: [], // TODO: fetch contacts if needed
|
|
checklist: [],
|
|
ownerId: e.user_id || "unknown",
|
|
photographerIds: Array.isArray(e.assigned_professionals)
|
|
? e.assigned_professionals.map((a: any) => a.professional_id)
|
|
: [],
|
|
institutionId: "", // TODO
|
|
attendees: e.qtd_formandos,
|
|
fotId: e.fot_id, // UUID
|
|
|
|
// Resource Mapping
|
|
qtdFormandos: e.qtd_formandos,
|
|
qtdFotografos: e.qtd_fotografos,
|
|
qtdRecepcionistas: e.qtd_recepcionistas,
|
|
qtdCinegrafistas: e.qtd_cinegrafistas,
|
|
qtdEstudios: e.qtd_estudios,
|
|
qtdPontosFoto: e.qtd_ponto_foto,
|
|
qtdPontosDecorados: e.qtd_ponto_decorado,
|
|
qtdPontosLed: e.qtd_pontos_led,
|
|
qtdPlataforma360: e.qtd_plataforma_360,
|
|
|
|
// Joined Fields
|
|
fot: e.fot_numero ?? e.fot_id, // Show Number if available (even 0), else ID
|
|
curso: e.curso_nome,
|
|
instituicao: e.instituicao,
|
|
anoFormatura: e.ano_semestre,
|
|
empresa: e.empresa_nome,
|
|
empresaId: e.empresa_id, // Ensure ID is passed to frontend
|
|
observacoes: e.observacoes_fot,
|
|
typeId: e.tipo_evento_id,
|
|
local_evento: e.local_evento, // Added local_evento mapping
|
|
assignments: Array.isArray(e.assigned_professionals)
|
|
? e.assigned_professionals.map((a: any) => ({
|
|
professionalId: a.professional_id,
|
|
status: a.status,
|
|
reason: a.motivo_rejeicao,
|
|
funcaoId: a.funcao_id
|
|
}))
|
|
: [],
|
|
logisticaNotificacaoEnviadaEm: e.logistica_notificacao_enviada_em,
|
|
}));
|
|
setEvents(mappedEvents);
|
|
} else {
|
|
setEvents([]);
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to fetch events", error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
};
|
|
fetchEvents();
|
|
}, [token]); // React to token change
|
|
|
|
// Fetch pending users from API
|
|
useEffect(() => {
|
|
const fetchUsers = async () => {
|
|
const token = localStorage.getItem('token');
|
|
if (token) {
|
|
try {
|
|
const result = await getPendingUsers(token);
|
|
if (result.data) {
|
|
const mappedUsers: User[] = result.data.map((u: any) => {
|
|
// Map backend roles to frontend enum
|
|
let mappedRole = UserRole.EVENT_OWNER;
|
|
if (u.role === 'profissional') mappedRole = UserRole.PHOTOGRAPHER;
|
|
else if (u.role === 'empresa') mappedRole = UserRole.BUSINESS_OWNER;
|
|
else if (u.role === 'admin') mappedRole = UserRole.SUPERADMIN;
|
|
else if (u.role === 'cliente') mappedRole = UserRole.EVENT_OWNER;
|
|
|
|
return {
|
|
id: u.id,
|
|
name: u.name || u.email.split('@')[0],
|
|
email: u.email,
|
|
phone: u.phone || '',
|
|
role: mappedRole,
|
|
approvalStatus: UserApprovalStatus.PENDING,
|
|
createdAt: u.created_at,
|
|
registeredInstitution: mappedRole === UserRole.EVENT_OWNER ? 'N/A' : undefined,
|
|
};
|
|
});
|
|
setPendingUsers(mappedUsers);
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to fetch pending users", error);
|
|
}
|
|
}
|
|
};
|
|
fetchUsers();
|
|
|
|
}, []);
|
|
|
|
// Fetch professionals
|
|
useEffect(() => {
|
|
const fetchProfs = async () => {
|
|
const token = localStorage.getItem('token');
|
|
console.log("[DEBUG] Fetching Professionals...", { token });
|
|
if (token) {
|
|
try {
|
|
const result = await getProfessionals(token);
|
|
console.log("[DEBUG] Fetch Professionals Result:", result);
|
|
if (result.data) {
|
|
const mappedProfs: Professional[] = result.data.map((p: any) => ({
|
|
id: p.id,
|
|
usuarioId: p.usuario_id,
|
|
nome: p.nome,
|
|
name: p.nome, // Keep for legacy Dashboard usage
|
|
email: p.email || "",
|
|
funcao_profissional_id: p.funcao_profissional_id,
|
|
role: p.funcao_profissional || p.funcao_nome || "Fotógrafo",
|
|
avatar: p.avatar_url || `https://ui-avatars.com/api/?name=${encodeURIComponent(p.nome)}&background=random`,
|
|
phone: p.whatsapp,
|
|
|
|
// Detailed fields
|
|
endereco: p.endereco,
|
|
cidade: p.cidade,
|
|
uf: p.uf,
|
|
cep: p.cep,
|
|
whatsapp: p.whatsapp,
|
|
cpf_cnpj_titular: p.cpf_cnpj_titular,
|
|
banco: p.banco,
|
|
agencia: p.agencia,
|
|
conta_pix: p.conta_pix,
|
|
carro_disponivel: p.carro_disponivel,
|
|
tem_estudio: p.tem_estudio,
|
|
qtd_estudio: p.qtd_estudio,
|
|
tipo_cartao: p.tipo_cartao,
|
|
observacao: p.observacao,
|
|
|
|
// Ratings
|
|
qual_tec: p.qual_tec,
|
|
educacao_simpatia: p.educacao_simpatia,
|
|
desempenho_evento: p.desempenho_evento,
|
|
disp_horario: p.disp_horario,
|
|
media: p.media,
|
|
|
|
tabela_free: p.tabela_free,
|
|
extra_por_equipamento: p.extra_por_equipamento,
|
|
equipamentos: p.equipamentos,
|
|
|
|
// Multi-function support
|
|
functions: p.functions || [],
|
|
|
|
availability: {}, // Default empty availability
|
|
}));
|
|
setProfessionals(mappedProfs);
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to fetch professionals", error);
|
|
}
|
|
} else {
|
|
console.warn("[DEBUG] No token found for fetching professionals");
|
|
}
|
|
};
|
|
fetchProfs();
|
|
}, [token]);
|
|
|
|
const addEvent = async (event: any) => {
|
|
const token = localStorage.getItem("token");
|
|
if (!token) {
|
|
console.error("No token found");
|
|
throw new Error("Usuário não autenticado");
|
|
}
|
|
|
|
try {
|
|
// Check if payload is already mapped (snake_case) or needs mapping (camelCase)
|
|
let payload;
|
|
if (event.fot_id && event.tipo_evento_id) {
|
|
// Already snake_case (from EventForm payload)
|
|
payload = event;
|
|
} else {
|
|
// Legacy camelCase mapping
|
|
payload = {
|
|
fot_id: event.fotId,
|
|
data_evento: event.date + "T" + (event.time || "00:00") + ":00Z",
|
|
tipo_evento_id: event.typeId,
|
|
observacoes_evento: event.name,
|
|
local_evento: event.address?.mapLink || "Local a definir",
|
|
endereco: event.address ? `${event.address.street}, ${event.address.number}, ${event.address.city} - ${event.address.state}` : "",
|
|
horario: event.startTime,
|
|
qtd_formandos: event.attendees ? parseInt(String(event.attendees)) : 0,
|
|
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,
|
|
status_profissionais: "AGUARDANDO",
|
|
foto_faltante: 0,
|
|
recep_faltante: 0,
|
|
cine_faltante: 0,
|
|
logistica_observacoes: "",
|
|
pre_venda: false
|
|
};
|
|
}
|
|
|
|
console.log("[DEBUG] addEvent payload:", payload);
|
|
|
|
const result = await import("../services/apiService").then(m => m.createAgenda(token, payload));
|
|
|
|
if (result.data) {
|
|
console.log("Agenda criada:", result.data);
|
|
// Force reload to ensure complete data consistency
|
|
window.location.href = '/painel';
|
|
} else {
|
|
console.error("Erro ao criar agenda API:", result.error);
|
|
throw new Error(result.error || "Erro ao criar agenda");
|
|
}
|
|
} catch (err: any) {
|
|
console.error("Exception creating agenda:", err);
|
|
throw err; // Re-throw so EventForm knows it failed
|
|
}
|
|
};
|
|
|
|
const updateEventStatus = async (id: string, status: EventStatus) => {
|
|
const token = localStorage.getItem('token');
|
|
if (token) {
|
|
try {
|
|
await apiUpdateStatus(token, id, status);
|
|
setEvents((prev) => prev.map((e) => (e.id === id ? { ...e, status } : e)));
|
|
} catch (error) {
|
|
console.error("Failed to update status", error);
|
|
}
|
|
} else {
|
|
// Fallback
|
|
setEvents((prev) => prev.map((e) => (e.id === id ? { ...e, status } : e)));
|
|
}
|
|
};
|
|
|
|
const assignPhotographer = async (eventId: string, photographerId: string, funcaoId?: string) => {
|
|
const token = localStorage.getItem('token');
|
|
const event = events.find(e => e.id === eventId);
|
|
if (!event) return;
|
|
|
|
const current = event.photographerIds || [];
|
|
const isRemoving = current.includes(photographerId);
|
|
|
|
if (token) {
|
|
try {
|
|
if (isRemoving) {
|
|
await apiRemoveProfessional(token, eventId, photographerId);
|
|
} else {
|
|
await apiAssignProfessional(token, eventId, photographerId, funcaoId);
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to assign/remove professional", error);
|
|
return; // Don't update state if API fails
|
|
}
|
|
}
|
|
|
|
setEvents((prev) =>
|
|
prev.map((e) => {
|
|
if (e.id === eventId) {
|
|
const current = e.photographerIds || [];
|
|
const currentAssignments = e.assignments || [];
|
|
if (current.includes(photographerId)) {
|
|
// Remove
|
|
return {
|
|
...e,
|
|
photographerIds: current.filter(id => id !== photographerId),
|
|
assignments: currentAssignments.filter(a => a.professionalId !== photographerId)
|
|
};
|
|
} else {
|
|
// Add
|
|
// Import AssignmentStatus if needed or use string "PENDENTE" matching the type
|
|
return {
|
|
...e,
|
|
photographerIds: [...current, photographerId],
|
|
assignments: [...currentAssignments, { professionalId: photographerId, status: "PENDENTE" as any, funcaoId }]
|
|
};
|
|
}
|
|
}
|
|
return e;
|
|
})
|
|
);
|
|
};
|
|
|
|
const getEventsByRole = (userId: string, role: string) => {
|
|
if (role === "SUPERADMIN" || role === "BUSINESS_OWNER" || role === "RESEARCHER") {
|
|
return events;
|
|
}
|
|
if (role === "EVENT_OWNER") {
|
|
return events.filter((e) => e.ownerId === userId);
|
|
}
|
|
if (role === "PHOTOGRAPHER") {
|
|
const professional = professionals.find((p) => p.usuarioId === userId);
|
|
if (!professional) return [];
|
|
const professionalId = professional.id;
|
|
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 [];
|
|
};
|
|
|
|
const addInstitution = (institution: Institution) => {
|
|
setInstitutions((prev) => [...prev, institution]);
|
|
};
|
|
|
|
const updateInstitution = (id: string, updatedData: Partial<Institution>) => {
|
|
setInstitutions((prev) =>
|
|
prev.map((inst) => (inst.id === id ? { ...inst, ...updatedData } : inst))
|
|
);
|
|
};
|
|
|
|
const getInstitutionsByUserId = (userId: string) => {
|
|
return institutions.filter((inst) => inst.ownerId === userId);
|
|
};
|
|
|
|
const getInstitutionById = (id: string) => {
|
|
return institutions.find((inst) => inst.id === id);
|
|
};
|
|
|
|
const addCourse = (course: Course) => {
|
|
setCourses((prev) => [...prev, course]);
|
|
};
|
|
|
|
const updateCourse = (id: string, updatedData: Partial<Course>) => {
|
|
setCourses((prev) =>
|
|
prev.map((course) =>
|
|
course.id === id ? { ...course, ...updatedData } : course
|
|
)
|
|
);
|
|
};
|
|
|
|
const getCoursesByInstitutionId = (institutionId: string) => {
|
|
return courses.filter((course) => course.institutionId === institutionId);
|
|
};
|
|
|
|
const getActiveCoursesByInstitutionId = (institutionId: string) => {
|
|
return courses.filter(
|
|
(course) => course.institutionId === institutionId && course.isActive
|
|
);
|
|
};
|
|
|
|
const getCourseById = (id: string) => {
|
|
return courses.find((course) => course.id === id);
|
|
};
|
|
|
|
// Funções para gerenciar usuários pendentes
|
|
const registerPendingUser = (userData: {
|
|
id: string;
|
|
name: string;
|
|
email: string;
|
|
phone: string;
|
|
registeredInstitution?: string
|
|
}) => {
|
|
const newUser: User = {
|
|
id: userData.id,
|
|
name: userData.name,
|
|
email: userData.email,
|
|
phone: userData.phone,
|
|
role: UserRole.EVENT_OWNER,
|
|
approvalStatus: UserApprovalStatus.PENDING,
|
|
registeredInstitution: userData.registeredInstitution,
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
setPendingUsers((prev) => [...prev, newUser]);
|
|
};
|
|
|
|
const approveUser = async (userId: string) => {
|
|
const token = localStorage.getItem('token');
|
|
if (token) {
|
|
try {
|
|
await apiApproveUser(userId, token);
|
|
setPendingUsers((prev) => prev.filter(user => user.id !== userId));
|
|
} catch (error) {
|
|
console.error("Failed to approve user", error);
|
|
}
|
|
} else {
|
|
// Fallback for mock/testing
|
|
setPendingUsers((prev) =>
|
|
prev.map((user) =>
|
|
user.id === userId
|
|
? { ...user, approvalStatus: UserApprovalStatus.APPROVED }
|
|
: user
|
|
)
|
|
);
|
|
}
|
|
};
|
|
|
|
const rejectUser = (userId: string) => {
|
|
setPendingUsers((prev) =>
|
|
prev.map((user) =>
|
|
user.id === userId
|
|
? { ...user, approvalStatus: UserApprovalStatus.REJECTED }
|
|
: user
|
|
)
|
|
);
|
|
};
|
|
|
|
return (
|
|
<DataContext.Provider
|
|
value={{
|
|
events,
|
|
institutions,
|
|
courses,
|
|
pendingUsers,
|
|
addEvent,
|
|
updateEventStatus,
|
|
assignPhotographer,
|
|
getEventsByRole,
|
|
addInstitution,
|
|
updateInstitution,
|
|
getInstitutionsByUserId,
|
|
getInstitutionById,
|
|
addCourse,
|
|
updateCourse,
|
|
getCoursesByInstitutionId,
|
|
getActiveCoursesByInstitutionId,
|
|
getCourseById,
|
|
registerPendingUser,
|
|
approveUser,
|
|
rejectUser,
|
|
professionals,
|
|
respondToAssignment: async (eventId, status, reason) => {
|
|
const token = localStorage.getItem('token');
|
|
if (!token || !user) return;
|
|
const professional = professionals.find(p => p.usuarioId === user.id);
|
|
if (!professional) return;
|
|
|
|
try {
|
|
// Check if `apiUpdateAssignmentStatus` returns { error } object or throws
|
|
// Based on other calls (e.g. line 1089), it likely returns an object.
|
|
const result = await apiUpdateAssignmentStatus(token, eventId, professional.id, status, reason);
|
|
|
|
if (result && result.error) {
|
|
alert("Erro ao atualizar status: " + result.error);
|
|
return;
|
|
}
|
|
|
|
// Only update state if successful
|
|
setEvents((prev) =>
|
|
prev.map((e) => {
|
|
if (e.id === eventId) {
|
|
const updatedAssignments = e.assignments?.map(a =>
|
|
a.professionalId === professional.id ? { ...a, status: status as any, reason } : a
|
|
) || [];
|
|
return { ...e, assignments: updatedAssignments };
|
|
}
|
|
return e;
|
|
})
|
|
);
|
|
} catch (error: any) {
|
|
console.error("Failed to update status", error);
|
|
alert("Erro de conexão ou servidor: " + (error.message || error));
|
|
}
|
|
},
|
|
updateEventDetails: async (id, data) => {
|
|
const token = localStorage.getItem("token");
|
|
if (!token) return;
|
|
const result = await apiUpdateAgenda(token, id, data);
|
|
if (result.error) {
|
|
throw new Error(result.error);
|
|
}
|
|
|
|
// Re-fetch logic to ensure state consistency
|
|
// Re-implementing simplified fetch logic here or we can trigger a reload.
|
|
// Since we are in DataContext, we can call a fetch function if we extract it.
|
|
// But to be safe and quick:
|
|
try {
|
|
const result = await import("../services/apiService").then(m => m.getAgendas(token));
|
|
if (result.data) {
|
|
// Re-map events logic from useEffect...
|
|
// This duplication is painful.
|
|
// Alternative: window.location.reload() in Dashboard.
|
|
// But let's assume the user navigates away or we do a simple local merge for Key Fields used in List.
|
|
setEvents(prev => prev.map(evt => {
|
|
if (evt.id === id) {
|
|
return {
|
|
...evt,
|
|
date: data.data_evento ? data.data_evento.split("T")[0] : evt.date,
|
|
time: data.horario || evt.time,
|
|
name: data.observacoes_evento || evt.name,
|
|
briefing: data.observacoes_evento || evt.briefing,
|
|
fotId: data.fot_id || evt.fotId,
|
|
empresaId: data.empresa_id || evt.empresaId, // If provided
|
|
// Address is hard to parse back to object from payload without logic
|
|
};
|
|
}
|
|
return evt;
|
|
}));
|
|
}
|
|
} catch (e) { console.error("Refresh failed", e); }
|
|
},
|
|
functions,
|
|
isLoading,
|
|
}}
|
|
>
|
|
{children}
|
|
</DataContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useData = () => {
|
|
const context = useContext(DataContext);
|
|
if (!context) throw new Error("useData must be used within a DataProvider");
|
|
return context;
|
|
};
|