From 002dee832d38bd0680a24e2b65eb5a15fd030fa4 Mon Sep 17 00:00:00 2001 From: NANDO9322 Date: Mon, 29 Dec 2025 19:55:50 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20Ajusta=20a=20edi=C3=A7=C3=A3o=20de=20ev?= =?UTF-8?q?entos=20no=20gerenciamento=20de=20agenda,=20incluindo=20atribui?= =?UTF-8?q?=C3=A7=C3=A3o=20de=20profissionais=20e=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/agenda/handler.go | 4 + backend/internal/agenda/service.go | 9 ++ backend/internal/db/generated/agenda.sql.go | 6 + backend/internal/db/queries/agenda.sql | 2 + frontend/components/EventForm.tsx | 120 +++++++++++++------- frontend/contexts/DataContext.tsx | 119 ++++++++++++------- frontend/pages/Dashboard.tsx | 65 +++++++---- frontend/services/apiService.ts | 25 ++++ 8 files changed, 248 insertions(+), 102 deletions(-) diff --git a/backend/internal/agenda/handler.go b/backend/internal/agenda/handler.go index 1495f8b..f0d57ee 100644 --- a/backend/internal/agenda/handler.go +++ b/backend/internal/agenda/handler.go @@ -1,6 +1,7 @@ package agenda import ( + "fmt" "net/http" "github.com/gin-gonic/gin" @@ -126,8 +127,11 @@ func (h *Handler) Update(c *gin.Context) { return } + fmt.Printf("Update Payload: %+v\n", req) + agenda, err := h.service.Update(c.Request.Context(), id, req) if err != nil { + fmt.Printf("Update Error for ID %s: %v\n", id, err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erro ao atualizar agenda: " + err.Error()}) return } diff --git a/backend/internal/agenda/service.go b/backend/internal/agenda/service.go index de78f95..c2eb415 100644 --- a/backend/internal/agenda/service.go +++ b/backend/internal/agenda/service.go @@ -3,6 +3,7 @@ package agenda import ( "context" "encoding/json" + "fmt" "time" "photum-backend/internal/db/generated" @@ -149,6 +150,7 @@ func (s *Service) List(ctx context.Context, userID uuid.UUID, role string) ([]Ag Instituicao: r.Instituicao, CursoNome: r.CursoNome, EmpresaNome: r.EmpresaNome, + EmpresaID: r.EmpresaID, AnoSemestre: r.AnoSemestre, ObservacoesFot: r.ObservacoesFot, TipoEventoNome: r.TipoEventoNome, @@ -191,6 +193,13 @@ func (s *Service) Get(ctx context.Context, id uuid.UUID) (generated.Agenda, erro } func (s *Service) Update(ctx context.Context, id uuid.UUID, req CreateAgendaRequest) (generated.Agenda, error) { + if req.FotID == uuid.Nil { + return generated.Agenda{}, fmt.Errorf("FOT ID inválido ou não informado") + } + if req.TipoEventoID == uuid.Nil { + return generated.Agenda{}, fmt.Errorf("Tipo de Evento ID inválido ou não informado") + } + status := s.CalculateStatus(req.FotoFaltante, req.RecepFaltante, req.CineFaltante) params := generated.UpdateAgendaParams{ diff --git a/backend/internal/db/generated/agenda.sql.go b/backend/internal/db/generated/agenda.sql.go index 2c30e21..e938b7c 100644 --- a/backend/internal/db/generated/agenda.sql.go +++ b/backend/internal/db/generated/agenda.sql.go @@ -302,6 +302,7 @@ SELECT af.ano_semestre, cf.observacoes as observacoes_fot, te.nome as tipo_evento_nome, + cf.empresa_id, COALESCE( (SELECT json_agg(json_build_object( 'professional_id', ap.profissional_id, @@ -357,6 +358,7 @@ type ListAgendasRow struct { AnoSemestre string `json:"ano_semestre"` ObservacoesFot pgtype.Text `json:"observacoes_fot"` TipoEventoNome string `json:"tipo_evento_nome"` + EmpresaID pgtype.UUID `json:"empresa_id"` AssignedProfessionals interface{} `json:"assigned_professionals"` } @@ -405,6 +407,7 @@ func (q *Queries) ListAgendas(ctx context.Context) ([]ListAgendasRow, error) { &i.AnoSemestre, &i.ObservacoesFot, &i.TipoEventoNome, + &i.EmpresaID, &i.AssignedProfessionals, ); err != nil { return nil, err @@ -427,6 +430,7 @@ SELECT af.ano_semestre, cf.observacoes as observacoes_fot, te.nome as tipo_evento_nome, + cf.empresa_id, COALESCE( (SELECT json_agg(json_build_object( 'professional_id', ap.profissional_id, @@ -483,6 +487,7 @@ type ListAgendasByUserRow struct { AnoSemestre string `json:"ano_semestre"` ObservacoesFot pgtype.Text `json:"observacoes_fot"` TipoEventoNome string `json:"tipo_evento_nome"` + EmpresaID pgtype.UUID `json:"empresa_id"` AssignedProfessionals interface{} `json:"assigned_professionals"` } @@ -531,6 +536,7 @@ func (q *Queries) ListAgendasByUser(ctx context.Context, userID pgtype.UUID) ([] &i.AnoSemestre, &i.ObservacoesFot, &i.TipoEventoNome, + &i.EmpresaID, &i.AssignedProfessionals, ); err != nil { return nil, err diff --git a/backend/internal/db/queries/agenda.sql b/backend/internal/db/queries/agenda.sql index fc1d269..0d73e97 100644 --- a/backend/internal/db/queries/agenda.sql +++ b/backend/internal/db/queries/agenda.sql @@ -42,6 +42,7 @@ SELECT af.ano_semestre, cf.observacoes as observacoes_fot, te.nome as tipo_evento_nome, + cf.empresa_id, COALESCE( (SELECT json_agg(json_build_object( 'professional_id', ap.profissional_id, @@ -70,6 +71,7 @@ SELECT af.ano_semestre, cf.observacoes as observacoes_fot, te.nome as tipo_evento_nome, + cf.empresa_id, COALESCE( (SELECT json_agg(json_build_object( 'professional_id', ap.profissional_id, diff --git a/frontend/components/EventForm.tsx b/frontend/components/EventForm.tsx index 5f5c1dc..5e9fb0e 100644 --- a/frontend/components/EventForm.tsx +++ b/frontend/components/EventForm.tsx @@ -148,39 +148,64 @@ export const EventForm: React.FC = ({ // Populate form with initialData useEffect(() => { if (initialData) { + console.log("EventForm received initialData:", initialData); + + // Robust mapping for API snake_case fields + const mapLink = (initialData as any).local_evento || (initialData.address && initialData.address.mapLink) || ""; + const mappedFotId = (initialData as any).fot_id || initialData.fotId || ""; + const mappedEmpresaId = (initialData as any).empresa_id || (initialData as any).empresaId || ""; + const mappedObservacoes = (initialData as any).observacoes_evento || initialData.name || ""; + + console.log("Mapped Values:", { mappedFotId, mappedEmpresaId, mappedObservacoes, mapLink }); + + // Parse Endereco String if Object is missing but String exists + // Format expected: "Rua, Numero - Cidade/UF" or similar + let addressData = initialData.address || { + street: "", number: "", city: "", state: "", zip: "", lat: -22.7394, lng: -47.3314, mapLink: "" + }; + + if (!initialData.address && (initialData as any).endereco) { + // Simple heuristic parser or just default. User might need to refine. + // Doing a best-effort copy to street if unstructured + addressData = { ...addressData, street: (initialData as any).endereco || "" }; + } + + // Safety: ensure all strings + addressData = { + street: addressData.street || "", + number: addressData.number || "", + city: addressData.city || "", + state: addressData.state || "", + zip: addressData.zip || "", + lat: addressData.lat || -22.7394, + lng: addressData.lng || -47.3314, + mapLink: addressData.mapLink || "" + }; + // 1. Populate standard form fields setFormData(prev => ({ ...prev, ...initialData, - startTime: initialData.time || "00:00", - locationName: (initialData as any).local_evento || (initialData as any).address?.mapLink?.includes('http') ? "" : (initialData as any).address?.mapLink || "", - fotId: initialData.fotId || "", + startTime: initialData.time || (initialData as any).horario || "00:00", + endTime: (initialData as any).horario_termino || prev.endTime || "", + locationName: mapLink.includes('http') ? "" : mapLink, // Avoid putting URL in name + fotId: mappedFotId, + name: mappedObservacoes, // Map Observacoes to Name field (displayed as "Observacoes do Evento") + briefing: mappedObservacoes, // Sync briefing + address: addressData, })); // 2. Populate derived dropdowns if data exists - // Check for empresa_id or empresaId in initialData - const initEmpresaId = (initialData as any).empresa_id || (initialData as any).empresaId; - if (initEmpresaId && (user?.role === UserRole.BUSINESS_OWNER || user?.role === UserRole.SUPERADMIN)) { - setSelectedCompanyId(initEmpresaId); + if (mappedEmpresaId && (user?.role === UserRole.BUSINESS_OWNER || user?.role === UserRole.SUPERADMIN)) { + console.log("Setting Selected Company:", mappedEmpresaId); + setSelectedCompanyId(mappedEmpresaId); } - if (initialData.curso) { - setSelectedCourseName(initialData.curso); + if (initialData.curso || (initialData as any).curso_nome) { + setSelectedCourseName(initialData.curso || (initialData as any).curso_nome || ""); } if (initialData.instituicao) { - setSelectedInstitutionName(initialData.instituicao); - } - - // 3. Populate address if available - if (initialData.address) { - setFormData(prev => ({ - ...prev, - address: { - ...prev.address, - ...initialData.address, - mapLink: initialData.address.mapLink || "" - } - })); + setSelectedInstitutionName(initialData.instituicao || ""); } } }, [initialData, user?.role]); @@ -204,16 +229,18 @@ export const EventForm: React.FC = ({ } // If we have a target company (or user is linked), fetch FOTs - if (targetEmpresaId || user?.role === UserRole.EVENT_OWNER) { // EventOwner might be linked differently or via getCadastroFot logic - // Verify logic: EventOwner (client) usually has strict link. - // If targetEmpresaId is still empty and user is NOT BusinessOwner/Superadmin, we might skip or let backend decide (if user has implicit link). - // But let's assume valid flow. - + if (targetEmpresaId || user?.role === UserRole.EVENT_OWNER) { setLoadingFots(true); + // Clear previous FOTs to force UI update and avoid stale data + setAvailableFots([]); + + console.log("Fetching FOTs for company:", targetEmpresaId); + const token = localStorage.getItem("token") || ""; const response = await getCadastroFot(token, targetEmpresaId); if (response.data) { + console.log("FOTs loaded:", response.data.length); setAvailableFots(response.data); } setLoadingFots(false); @@ -354,23 +381,36 @@ export const EventForm: React.FC = ({ // Validation if (!formData.name) return alert("Preencha o tipo de evento"); if (!formData.date) return alert("Preencha a data"); - if (user?.role === UserRole.BUSINESS_OWNER || user?.role === UserRole.EVENT_OWNER) { - if (!formData.fotId) { - alert("Por favor, selecione a Turma (Cadastro FOT) antes de continuar."); - return; + + // Ensure typeId is valid + let finalTypeId = formData.typeId; + if (!finalTypeId || finalTypeId === "00000000-0000-0000-0000-000000000000") { + // Try to match by name + const matchingType = eventTypes.find(t => t.nome === formData.type); + if (matchingType) { + finalTypeId = matchingType.id; + } else { + // If strictly required by DB, we must stop. + // But for legacy compatibility, maybe we prompt user + return alert("Tipo de evento inválido. Por favor, selecione novamente o tipo do evento."); } } + if (!formData.fotId) { + alert("ERRO CRÍTICO: Turma (FOT) não identificada. Por favor, selecione a Turma ou Empresa novamente."); + return; + } + try { setShowToast(true); // Prepare Payload for Agenda API const payload = { - fot_id: formData.fotId, - tipo_evento_id: formData.typeId || "00000000-0000-0000-0000-000000000000", + fot_id: formData.fotId, // Must be valid UUID + tipo_evento_id: finalTypeId, data_evento: new Date(formData.date).toISOString(), horario: formData.startTime || "", - observacoes_evento: formData.briefing || "", + observacoes_evento: formData.name || formData.briefing || "", local_evento: formData.locationName || "", endereco: `${formData.address.street}, ${formData.address.number} - ${formData.address.city}/${formData.address.state}`, qtd_formandos: parseInt(formData.attendees) || 0, @@ -395,15 +435,13 @@ export const EventForm: React.FC = ({ }; // Submit to parent handler - setTimeout(() => { - if (onSubmit) { - onSubmit(formData); - } - alert("Solicitação enviada com sucesso!"); - }, 1000); + if (onSubmit) { + await onSubmit(payload); + } + alert("Solicitação enviada com sucesso!"); } catch (e: any) { console.error(e); - alert("Erro inesperado: " + e.message); + alert("Erro ao salvar: " + (e.message || "Erro desconhecido")); setShowToast(false); } }; diff --git a/frontend/contexts/DataContext.tsx b/frontend/contexts/DataContext.tsx index a671ff0..7e45ece 100644 --- a/frontend/contexts/DataContext.tsx +++ b/frontend/contexts/DataContext.tsx @@ -1,6 +1,6 @@ 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 } from "../services/apiService"; +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, @@ -608,6 +608,7 @@ interface DataContextType { rejectUser: (userId: string) => void; professionals: Professional[]; respondToAssignment: (eventId: string, status: string, reason?: string) => Promise; + updateEventDetails: (id: string, data: any) => Promise; } const DataContext = createContext(undefined); @@ -693,6 +694,7 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({ 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, assignments: Array.isArray(e.assigned_professionals) @@ -815,59 +817,63 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({ fetchProfs(); }, [token]); - const addEvent = async (event: EventData) => { + const addEvent = async (event: any) => { const token = localStorage.getItem("token"); if (!token) { console.error("No token found"); - // Fallback for offline/mock - setEvents((prev) => [event, ...prev]); - return; + throw new Error("Usuário não autenticado"); } try { - // Map frontend fields (camelCase) to backend fields (snake_case) - const payload = { - fot_id: event.fotId, - data_evento: event.date + "T" + (event.time || "00:00") + ":00Z", // Backend expects full datetime with timezone - tipo_evento_id: event.typeId, - observacoes_evento: event.name, // "Observações do Evento" maps to name in EventForm - // local_evento: event.address.street + ", " + event.address.number, // Or map separate fields if needed - local_evento: event.address.mapLink || "Local a definir", // using mapLink or some string - endereco: `${event.address.street}, ${event.address.number}, ${event.address.city} - ${event.address.state}`, - horario: event.startTime, - // Defaulting missing counts to 0 for now as they are not in the simplified form - 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", // Will be calculated by backend anyway - foto_faltante: 0, - recep_faltante: 0, - cine_faltante: 0, - logistica_observacoes: "", - pre_venda: false - }; + // 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) { - // Success console.log("Agenda criada:", result.data); - // Force reload to ensure complete data consistency (FOT, joins, etc.) + // Force reload to ensure complete data consistency window.location.href = '/painel'; } else { console.error("Erro ao criar agenda API:", result.error); - // Fallback or Toast? - // We will optimistically add it locally or throw + throw new Error(result.error || "Erro ao criar agenda"); } - } catch (err) { + } catch (err: any) { console.error("Exception creating agenda:", err); + throw err; // Re-throw so EventForm knows it failed } }; @@ -1077,6 +1083,43 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({ }) ); }, + 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); } + }, addEvent, updateEventStatus, assignPhotographer, diff --git a/frontend/pages/Dashboard.tsx b/frontend/pages/Dashboard.tsx index 2262702..9eea165 100644 --- a/frontend/pages/Dashboard.tsx +++ b/frontend/pages/Dashboard.tsx @@ -34,6 +34,7 @@ export const Dashboard: React.FC = ({ }) => { const { user } = useAuth(); const navigate = useNavigate(); + // Extract updateEventDetails from useData const { events, getEventsByRole, @@ -44,7 +45,48 @@ export const Dashboard: React.FC = ({ getInstitutionById, getActiveCoursesByInstitutionId, respondToAssignment, + updateEventDetails, } = useData(); + + // ... (inside component) + + const handleSaveEvent = async (data: any) => { + const isClient = user.role === UserRole.EVENT_OWNER; + + if (view === "edit" && selectedEvent) { + if (updateEventDetails) { + await updateEventDetails(selectedEvent.id, data); + // Force reload of view to reflect changes (or rely on DataContext optimistic update) + // But DataContext optimistic update only touched generic fields. + // Address might still be old in 'selectedEvent' state if we don't update it. + // Updating selectedEvent manually as well to be safe: + const updatedEvent = { ...selectedEvent, ...data, date: data.date || data.data_evento?.split('T')[0] || selectedEvent.date }; + setSelectedEvent(updatedEvent); + setView("details"); + // Optional: Reload page safely if critical fields changed that DataContext map didn't catch? + // For now, trust DataContext + local state update. + // Actually, DataContext refetch logic was "try import...", so it might be async. + // Let's reload window to be 100% sure for the user as requested "mudei a data e não mudou". + window.location.reload(); + } else { + console.error("Update function not available"); + } + } else { + const initialStatus = isClient + ? EventStatus.PENDING_APPROVAL + : EventStatus.PLANNING; + const newEvent: EventData = { + ...data, + id: Math.random().toString(36).substr(2, 9), + status: initialStatus, + checklist: [], + ownerId: isClient ? user.id : "unknown", + photographerIds: [], + }; + addEvent(newEvent); + setView("list"); + } + }; const [view, setView] = useState<"list" | "create" | "edit" | "details">( initialView ); @@ -116,30 +158,7 @@ export const Dashboard: React.FC = ({ ); }); - const handleSaveEvent = (data: any) => { - const isClient = user.role === UserRole.EVENT_OWNER; - if (view === "edit" && selectedEvent) { - const updatedEvent = { ...selectedEvent, ...data }; - console.log("Updated", updatedEvent); - setSelectedEvent(updatedEvent); - setView("details"); - } else { - const initialStatus = isClient - ? EventStatus.PENDING_APPROVAL - : EventStatus.PLANNING; - const newEvent: EventData = { - ...data, - id: Math.random().toString(36).substr(2, 9), - status: initialStatus, - checklist: [], - ownerId: isClient ? user.id : "unknown", - photographerIds: [], - }; - addEvent(newEvent); - setView("list"); - } - }; // Keep selectedEvent in sync with global events state useEffect(() => { diff --git a/frontend/services/apiService.ts b/frontend/services/apiService.ts index e62b3e5..90329eb 100644 --- a/frontend/services/apiService.ts +++ b/frontend/services/apiService.ts @@ -529,6 +529,31 @@ export const getAgendas = async (token: string): Promise> => } }; +// Agenda - Update +export const updateAgenda = async (token: string, id: string, data: any) => { + try { + const response = await fetch(`${API_BASE_URL}/api/agenda/${id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}` + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.error || `HTTP error! status: ${response.status}`); + } + + const responseData = await response.json(); + return { data: responseData, error: null }; + } catch (error: any) { + console.error("Erro ao atualizar agenda:", error); + return { data: null, error: error.message || "Erro ao atualizar agenda" }; + } +}; + export const updateAssignmentStatus = async (token: string, eventId: string, professionalId: string, status: string, reason?: string) => { try { const response = await fetch(`${API_BASE_URL}/api/agenda/${eventId}/professionals/${professionalId}/status`, {