feat: Ajusta a edição de eventos no gerenciamento de agenda, incluindo atribuição de profissionais e status

This commit is contained in:
NANDO9322 2025-12-29 19:55:50 -03:00
parent cd00fb53f9
commit 002dee832d
8 changed files with 248 additions and 102 deletions

View file

@ -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
}

View file

@ -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{

View file

@ -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

View file

@ -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,

View file

@ -148,39 +148,64 @@ export const EventForm: React.FC<EventFormProps> = ({
// 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<EventFormProps> = ({
}
// 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<EventFormProps> = ({
// 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<EventFormProps> = ({
};
// 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);
}
};

View file

@ -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<void>;
updateEventDetails: (id: string, data: any) => Promise<void>;
}
const DataContext = createContext<DataContextType | undefined>(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,

View file

@ -34,6 +34,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
}) => {
const { user } = useAuth();
const navigate = useNavigate();
// Extract updateEventDetails from useData
const {
events,
getEventsByRole,
@ -44,7 +45,48 @@ export const Dashboard: React.FC<DashboardProps> = ({
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<DashboardProps> = ({
);
});
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(() => {

View file

@ -529,6 +529,31 @@ export const getAgendas = async (token: string): Promise<ApiResponse<any[]>> =>
}
};
// 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`, {