feat: sistema de validação de conflitos e melhorias na UX da agenda

Implementa validação de horários para evitar conflitos no aceite de eventos, correções na sincronização de dados da agenda e melhorias na interface de gestão de equipe.

Backend:
- handler.go: Correção no retorno do endpoint [UpdateAssignmentStatus](cci:1://file:///c:/Projetos/photum/backend/internal/agenda/handler.go:279:0-313:1) para enviar JSON válido e evitar erros no frontend.
- service.go: Implementação da lógica de validação de conflitos antes de aceitar um evento.
- agenda.sql: Nova query `CheckProfessionalBusyDate` para verificação de sobreposição de horários.

Frontend:
- Dashboard.tsx: Adição de tooltip e texto para exibir o "Motivo da Rejeição" na gestão de equipe (Desktop/Mobile).
- EventScheduler.tsx: Filtro para excluir profissionais com status 'REJEITADO' e correção na label de 'Pendente'.
- EventDetails.tsx: Refatoração para usar estado global ([useData](cci:1://file:///c:/Projetos/photum/frontend/contexts/DataContext.tsx:1156:0-1160:2)), garantindo atualização imediata de datas e locais.
- DataContext.tsx: Mapeamento do campo `local_evento` e melhoria no tratamento de erro otimista.
- Ajustes gerais em ProfessionalDetailsModal, Login e correções de tipos.
This commit is contained in:
NANDO9322 2025-12-30 11:24:53 -03:00
parent b03a5445b5
commit 7536bddacb
11 changed files with 224 additions and 74 deletions

View file

@ -310,7 +310,7 @@ func (h *Handler) UpdateAssignmentStatus(c *gin.Context) {
return return
} }
c.Status(http.StatusOK) c.JSON(http.StatusOK, gin.H{"status": "success"})
} }
// UpdateAssignmentPosition godoc // UpdateAssignmentPosition godoc

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strconv"
"time" "time"
"photum-backend/internal/db/generated" "photum-backend/internal/db/generated"
@ -264,6 +265,44 @@ func (s *Service) UpdateStatus(ctx context.Context, agendaID uuid.UUID, status s
} }
func (s *Service) UpdateAssignmentStatus(ctx context.Context, agendaID, professionalID uuid.UUID, status string, reason string) error { func (s *Service) UpdateAssignmentStatus(ctx context.Context, agendaID, professionalID uuid.UUID, status string, reason string) error {
// Conflict Validation on Accept
if status == "ACEITO" {
// 1. Get Current Agenda to know Date/Time
agenda, err := s.queries.GetAgenda(ctx, pgtype.UUID{Bytes: agendaID, Valid: true})
if err != nil {
return fmt.Errorf("erro ao buscar agenda para validação: %v", err)
}
if agenda.DataEvento.Valid {
// 2. Check for other confirmed events on the same date
// Exclude current agenda ID from check
conflicts, err := s.queries.CheckProfessionalBusyDate(ctx, generated.CheckProfessionalBusyDateParams{
ProfissionalID: pgtype.UUID{Bytes: professionalID, Valid: true},
DataEvento: agenda.DataEvento,
ID: pgtype.UUID{Bytes: agendaID, Valid: true},
})
if err != nil {
return fmt.Errorf("erro ao verificar disponibilidade: %v", err)
}
if len(conflicts) > 0 {
// 3. Check time overlap
currentStart := parseTimeMinutes(agenda.Horario.String)
currentEnd := currentStart + 240 // Assume 4 hours duration
for _, c := range conflicts {
conflictStart := parseTimeMinutes(c.Horario.String)
conflictEnd := conflictStart + 240
// Check overlap: StartA < EndB && StartB < EndA
if currentStart < conflictEnd && conflictStart < currentEnd {
return fmt.Errorf("conflito de horário: profissional já confirmou presença em outro evento às %s", c.Horario.String)
}
}
}
}
}
params := generated.UpdateAssignmentStatusParams{ params := generated.UpdateAssignmentStatusParams{
AgendaID: pgtype.UUID{Bytes: agendaID, Valid: true}, AgendaID: pgtype.UUID{Bytes: agendaID, Valid: true},
ProfissionalID: pgtype.UUID{Bytes: professionalID, Valid: true}, ProfissionalID: pgtype.UUID{Bytes: professionalID, Valid: true},
@ -274,6 +313,16 @@ func (s *Service) UpdateAssignmentStatus(ctx context.Context, agendaID, professi
return err return err
} }
// Helper for time parsing (HH:MM)
func parseTimeMinutes(t string) int {
if len(t) < 5 {
return 0 // Default or Error
}
h, _ := strconv.Atoi(t[0:2])
m, _ := strconv.Atoi(t[3:5])
return h*60 + m
}
func (s *Service) UpdateAssignmentPosition(ctx context.Context, agendaID, professionalID uuid.UUID, posicao string) error { func (s *Service) UpdateAssignmentPosition(ctx context.Context, agendaID, professionalID uuid.UUID, posicao string) error {
params := generated.UpdateAssignmentPositionParams{ params := generated.UpdateAssignmentPositionParams{
AgendaID: pgtype.UUID{Bytes: agendaID, Valid: true}, AgendaID: pgtype.UUID{Bytes: agendaID, Valid: true},

View file

@ -27,6 +27,48 @@ func (q *Queries) AssignProfessional(ctx context.Context, arg AssignProfessional
return err return err
} }
const checkProfessionalBusyDate = `-- name: CheckProfessionalBusyDate :many
SELECT a.id, a.horario, ap.status
FROM agenda_profissionais ap
JOIN agenda a ON ap.agenda_id = a.id
WHERE ap.profissional_id = $1
AND a.data_evento = $2
AND ap.status = 'ACEITO'
AND a.id != $3
`
type CheckProfessionalBusyDateParams struct {
ProfissionalID pgtype.UUID `json:"profissional_id"`
DataEvento pgtype.Date `json:"data_evento"`
ID pgtype.UUID `json:"id"`
}
type CheckProfessionalBusyDateRow struct {
ID pgtype.UUID `json:"id"`
Horario pgtype.Text `json:"horario"`
Status pgtype.Text `json:"status"`
}
func (q *Queries) CheckProfessionalBusyDate(ctx context.Context, arg CheckProfessionalBusyDateParams) ([]CheckProfessionalBusyDateRow, error) {
rows, err := q.db.Query(ctx, checkProfessionalBusyDate, arg.ProfissionalID, arg.DataEvento, arg.ID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []CheckProfessionalBusyDateRow
for rows.Next() {
var i CheckProfessionalBusyDateRow
if err := rows.Scan(&i.ID, &i.Horario, &i.Status); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const createAgenda = `-- name: CreateAgenda :one const createAgenda = `-- name: CreateAgenda :one
INSERT INTO agenda ( INSERT INTO agenda (
fot_id, fot_id,

View file

@ -180,3 +180,12 @@ WHERE dp.data = $1
AND ap.status = 'ACEITO' AND ap.status = 'ACEITO'
) )
ORDER BY p.nome; ORDER BY p.nome;
-- name: CheckProfessionalBusyDate :many
SELECT a.id, a.horario, ap.status
FROM agenda_profissionais ap
JOIN agenda a ON ap.agenda_id = a.id
WHERE ap.profissional_id = $1
AND a.data_evento = $2
AND ap.status = 'ACEITO'
AND a.id != $3;

View file

@ -200,6 +200,7 @@ func (s *Service) Update(ctx context.Context, id string, input UpdateProfissiona
} }
func (s *Service) Delete(ctx context.Context, id string) error { func (s *Service) Delete(ctx context.Context, id string) error {
fmt.Printf("[DEBUG] Deleting Professional: %s\n", id)
uuidVal, err := uuid.Parse(id) uuidVal, err := uuid.Parse(id)
if err != nil { if err != nil {
return errors.New("invalid id") return errors.New("invalid id")
@ -208,30 +209,37 @@ func (s *Service) Delete(ctx context.Context, id string) error {
// Get professional to find associated user // Get professional to find associated user
prof, err := s.queries.GetProfissionalByID(ctx, pgtype.UUID{Bytes: uuidVal, Valid: true}) prof, err := s.queries.GetProfissionalByID(ctx, pgtype.UUID{Bytes: uuidVal, Valid: true})
if err != nil { if err != nil {
fmt.Printf("[DEBUG] Failed to get professional %s: %v\n", id, err)
return err return err
} }
// Delete professional profile (should be done first or after?) fmt.Printf("[DEBUG] Prof Found:: ID=%s, UsuarioID.Valid=%v, UsuarioID=%s\n",
// If foreign key is SET NULL, it doesn't strictly matter for FK constraint, uuid.UUID(prof.ID.Bytes).String(),
// but logically deleting the profile first is cleaner if we want to ensure profile is gone. prof.UsuarioID.Valid,
// Actually, if we delete User first and it SETS NULL, we still have to delete Profile. uuid.UUID(prof.UsuarioID.Bytes).String())
// So let's delete Profile first.
err = s.queries.DeleteProfissional(ctx, pgtype.UUID{Bytes: uuidVal, Valid: true})
if err != nil {
return err
}
// Delete associated user if exists // Delete associated user first (ensures login access is revoked)
if prof.UsuarioID.Valid { if prof.UsuarioID.Valid {
fmt.Printf("[DEBUG] Attempting to delete User ID: %s\n", uuid.UUID(prof.UsuarioID.Bytes).String())
err = s.queries.DeleteUsuario(ctx, prof.UsuarioID) err = s.queries.DeleteUsuario(ctx, prof.UsuarioID)
if err != nil { if err != nil {
// Create warning log? For now just return error or ignore? fmt.Printf("[DEBUG] Failed to delete User: %v\n", err)
// If user deletion fails, it's orphan but harmless-ish (except login).
// Better to return error.
return err return err
} }
fmt.Println("[DEBUG] User deleted successfully.")
} else {
fmt.Println("[DEBUG] UsuarioID is invalid/null. Skipping User deletion.")
} }
// Delete professional profile
fmt.Println("[DEBUG] Deleting Professional profile...")
err = s.queries.DeleteProfissional(ctx, pgtype.UUID{Bytes: uuidVal, Valid: true})
if err != nil {
fmt.Printf("[DEBUG] Failed to delete Professional: %v\n", err)
return err
}
fmt.Println("[DEBUG] Professional deleted successfully.")
return nil return nil
} }

View file

@ -8,7 +8,7 @@ import { UserRole } from "../types";
interface EventSchedulerProps { interface EventSchedulerProps {
agendaId: string; agendaId: string;
dataEvento: string; // YYYY-MM-DD dataEvento: string; // YYYY-MM-DD
allowedProfessionals?: string[]; // IDs of professionals allowed to be scheduled allowedProfessionals?: { professional_id?: string; professionalId?: string; status?: string }[] | string[]; // IDs or Objects
onUpdateStats?: (stats: { studios: number }) => void; onUpdateStats?: (stats: { studios: number }) => void;
defaultTime?: string; defaultTime?: string;
} }
@ -45,13 +45,24 @@ const EventScheduler: React.FC<EventSchedulerProps> = ({ agendaId, dataEvento, a
if (e.id === agendaId) return false; // Ignore current event (allow re-scheduling or moving within same event?) if (e.id === agendaId) return false; // Ignore current event (allow re-scheduling or moving within same event?)
// Actually usually we don't want to double book in same event either unless intention is specific. // Actually usually we don't want to double book in same event either unless intention is specific.
// But 'escalas' check (Line 115) already handles "already in this scale". // But 'escalas' check (Line 115) already handles "already in this scale".
// If they are assigned to the *Event Team* (Logistics) but not Scale yet, it doesn't mean they are busy at THIS exact time? // If they are assigned to the *Event Team* (Logistics) but not Scale yet, it doesn't mean they are busy for THIS exact time?
// Wait, 'events.photographerIds' means they are on the Team. // Wait, 'events.photographerIds' means they are on the Team.
// Being on the Team for specific time? // Being on the Team uses generic time?
// Does 'EventData' have time? Yes. 'e.time'. // For now, assume busy if in another event team.
// If they are in another Event Team, we assume they are busy for that Event's duration.
if (e.date === dataEvento && e.photographerIds.includes(profId)) { // Fix for Request 2: Only consider BUSY if status is 'Confirmado' in the other event?
// The frontend 'events' list might generally show all events.
// But 'events' from 'useData' implies basic info.
// If we access specific assigned status here, we could filter.
// The `events` array usually has basic info. If `assigned_professionals` is detailed there, we could check status.
// Assuming `e.photographerIds` is just IDs.
// We'll leave backend to strictly enforce, but frontend hint is good.
// Check if professional is in any other event on the same day with overlap
// FIX: Only consider BUSY if status is 'ACEITO' (Confirmed)
const isAssignedConfirmed = (e.assignments || []).some(a => a.professionalId === profId && a.status === 'ACEITO');
if (e.date === dataEvento && isAssignedConfirmed) {
const [eh, em] = (e.time || "00:00").split(':').map(Number); const [eh, em] = (e.time || "00:00").split(':').map(Number);
const evtStart = eh * 60 + em; const evtStart = eh * 60 + em;
const evtEnd = evtStart + 240; // Assume 4h duration for other events too const evtEnd = evtStart + 240; // Assume 4h duration for other events too
@ -133,8 +144,26 @@ const EventScheduler: React.FC<EventSchedulerProps> = ({ agendaId, dataEvento, a
// 1. Start with all professionals or just the allowed ones // 1. Start with all professionals or just the allowed ones
let availableProfs = professionals; let availableProfs = professionals;
const allowedMap = new Map<string, string>(); // ID -> Status
if (allowedProfessionals && allowedProfessionals.length > 0) { if (allowedProfessionals && allowedProfessionals.length > 0) {
availableProfs = availableProfs.filter(p => allowedProfessionals.includes(p.id)); // Normalize allowed list
const ids: string[] = [];
allowedProfessionals.forEach((p: any) => {
if (typeof p === 'string') {
ids.push(p);
allowedMap.set(p, 'Confirmado'); // Default if not detailed
} else {
const pid = p.professional_id || p.professionalId;
const status = p.status || 'Pendente';
// Filter out Rejected professionals from the available list
if (pid && status !== 'REJEITADO' && status !== 'Rejeitado') {
ids.push(pid);
allowedMap.set(pid, status);
}
}
});
availableProfs = availableProfs.filter(p => ids.includes(p.id));
} }
// 2. Filter out professionals already in schedule to prevent duplicates // 2. Filter out professionals already in schedule to prevent duplicates
@ -171,9 +200,17 @@ const EventScheduler: React.FC<EventSchedulerProps> = ({ agendaId, dataEvento, a
<option value="">Selecione...</option> <option value="">Selecione...</option>
{availableProfs.map(p => { {availableProfs.map(p => {
const isBusy = checkAvailability(p.id); const isBusy = checkAvailability(p.id);
const status = allowedMap.get(p.id);
const isPending = status !== 'Confirmado' && status !== 'ACEITO';
const isDisabled = isBusy || isPending;
let label = "";
if (isPending) label = "(Pendente de Aceite)";
else if (isBusy) label = "(Ocupado)";
return ( return (
<option key={p.id} value={p.id} disabled={isBusy} className={isBusy ? "text-gray-400" : ""}> <option key={p.id} value={p.id} disabled={isDisabled} className={isDisabled ? "text-gray-400" : ""}>
{p.nome} - {p.role || "Profissional"} {isBusy ? "(Ocupado em outro evento)" : ""} {p.nome} - {p.role || "Profissional"} {label}
</option> </option>
); );
})} })}

View file

@ -60,6 +60,8 @@ export const ProfessionalDetailsModal: React.FC<ProfessionalDetailsModalProps> =
// Also check legacy/fallback logic if needed, but primary is role or ownership // Also check legacy/fallback logic if needed, but primary is role or ownership
const isAdminOrOwner = canViewDetails; // Keeping variable name for now or refactoring below -> refactoring below to use canViewDetails for clarity is better but to minimize diff noise we can keep it or rename it. Let's rename it to avoid confusion. const isAdminOrOwner = canViewDetails; // Keeping variable name for now or refactoring below -> refactoring below to use canViewDetails for clarity is better but to minimize diff noise we can keep it or rename it. Let's rename it to avoid confusion.
const isMaster = user?.role === UserRole.SUPERADMIN || user?.role === UserRole.BUSINESS_OWNER;
return ( return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 animate-fadeIn"> <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 animate-fadeIn">
<div className="bg-white rounded-2xl shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-y-auto flex flex-col relative animate-slideIn"> <div className="bg-white rounded-2xl shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-y-auto flex flex-col relative animate-slideIn">
@ -91,8 +93,8 @@ export const ProfessionalDetailsModal: React.FC<ProfessionalDetailsModalProps> =
<User size={14} /> <User size={14} />
{professional.role || "Profissional"} {professional.role || "Profissional"}
</span> </span>
{/* Performance Rating - Only for Admins */} {/* Performance Rating - Only for Master (Admin/Owner), NOT for the professional themselves */}
{isAdminOrOwner && professional.media !== undefined && professional.media !== null && ( {isMaster && professional.media !== undefined && professional.media !== null && (
<span className="px-3 py-1 bg-yellow-50 text-yellow-700 rounded-full text-sm font-medium border border-yellow-200 flex items-center gap-1"> <span className="px-3 py-1 bg-yellow-50 text-yellow-700 rounded-full text-sm font-medium border border-yellow-200 flex items-center gap-1">
<Star size={14} className="fill-yellow-500 text-yellow-500" /> <Star size={14} className="fill-yellow-500 text-yellow-500" />
{typeof professional.media === 'number' ? professional.media.toFixed(1) : parseFloat(String(professional.media)).toFixed(1)} {typeof professional.media === 'number' ? professional.media.toFixed(1) : parseFloat(String(professional.media)).toFixed(1)}
@ -212,7 +214,7 @@ export const ProfessionalDetailsModal: React.FC<ProfessionalDetailsModalProps> =
)} )}
{/* Performance / Observations - Protected */} {/* Performance / Observations - Protected */}
{isAdminOrOwner && ( {isMaster && (
<div className="w-full mt-8 bg-brand-gold/5 rounded-xl p-6 border border-brand-gold/10"> <div className="w-full mt-8 bg-brand-gold/5 rounded-xl p-6 border border-brand-gold/10">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="p-2 bg-white rounded-full text-brand-gold shadow-sm"> <div className="p-2 bg-white rounded-full text-brand-gold shadow-sm">

View file

@ -697,6 +697,7 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
empresaId: e.empresa_id, // Ensure ID is passed to frontend empresaId: e.empresa_id, // Ensure ID is passed to frontend
observacoes: e.observacoes_fot, observacoes: e.observacoes_fot,
typeId: e.tipo_evento_id, typeId: e.tipo_evento_id,
local_evento: e.local_evento, // Added local_evento mapping
assignments: Array.isArray(e.assigned_professionals) assignments: Array.isArray(e.assigned_professionals)
? e.assigned_professionals.map((a: any) => ({ ? e.assigned_professionals.map((a: any) => ({
professionalId: a.professional_id, professionalId: a.professional_id,
@ -1066,22 +1067,32 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
const professional = professionals.find(p => p.usuarioId === user.id); const professional = professionals.find(p => p.usuarioId === user.id);
if (!professional) return; if (!professional) return;
await apiUpdateAssignmentStatus(token, eventId, professional.id, status, reason); 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);
// Re-fetch events to update status locally efficiently (or update local state) if (result && result.error) {
// For simplicity, let's update local state alert("Erro ao atualizar status: " + result.error);
setEvents((prev) => return;
prev.map((e) => { }
if (e.id === eventId) {
const updatedAssignments = e.assignments?.map(a => // Only update state if successful
a.professionalId === professional.id ? { ...a, status: status as any, reason } : a setEvents((prev) =>
) || []; prev.map((e) => {
// If it wasn't in assignments (unlikely if responding), simple update if (e.id === eventId) {
return { ...e, assignments: updatedAssignments }; const updatedAssignments = e.assignments?.map(a =>
} a.professionalId === professional.id ? { ...a, status: status as any, reason } : a
return e; ) || [];
}) 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) => { updateEventDetails: async (id, data) => {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");

View file

@ -930,7 +930,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
const isBusy = !isAssigned && events.some(e => const isBusy = !isAssigned && events.some(e =>
e.id !== selectedEvent.id && e.id !== selectedEvent.id &&
e.date === selectedEvent.date && e.date === selectedEvent.date &&
e.photographerIds.includes(photographer.id) (e.assignments || []).some(a => a.professionalId === photographer.id && a.status === 'ACEITO')
); );
return ( return (

View file

@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { ArrowLeft, MapPin, Calendar, Clock, DollarSign } from 'lucide-react'; import { ArrowLeft, MapPin, Calendar, Clock, DollarSign } from 'lucide-react';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { useData } from '../contexts/DataContext';
import { getAgendas } from '../services/apiService'; import { getAgendas } from '../services/apiService';
import EventScheduler from '../components/EventScheduler'; import EventScheduler from '../components/EventScheduler';
import EventLogistics from '../components/EventLogistics'; import EventLogistics from '../components/EventLogistics';
@ -9,32 +10,18 @@ import EventLogistics from '../components/EventLogistics';
const EventDetails: React.FC = () => { const EventDetails: React.FC = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const { token } = useAuth(); const { events, loading } = useData();
const [event, setEvent] = useState<any | null>(null);
const [loading, setLoading] = useState(true);
const [calculatedStats, setCalculatedStats] = useState({ studios: 0 }); const [calculatedStats, setCalculatedStats] = useState({ studios: 0 });
useEffect(() => { const event = events.find(e => e.id === id);
if (id && token) {
loadEvent();
}
}, [id, token]);
const loadEvent = async () => { // No local loading state needed if events are loaded globally, or check if events.length === 0 && loading
// Since we don't have a getEventById, we list and filter for now (MVP). if (!event) return <div className="p-8 text-center text-red-500">Evento não encontrado ou carregando...</div>;
// Ideally backend should have GET /agenda/:id
const res = await getAgendas(token!);
if (res.data) {
const found = res.data.find((e: any) => e.id === id);
setEvent(found);
}
setLoading(false);
};
if (loading) return <div className="p-8 text-center">Carregando detalhes do evento...</div>;
if (!event) return <div className="p-8 text-center text-red-500">Evento não encontrado.</div>; if (!event) return <div className="p-8 text-center text-red-500">Evento não encontrado.</div>;
const formattedDate = new Date(event.data_evento).toLocaleDateString(); // Use event.date which is already YYYY-MM-DD from DataContext
const formattedDate = new Date(event.date + "T00:00:00").toLocaleDateString();
return ( return (
<div className="min-h-screen bg-gray-50 p-6"> <div className="min-h-screen bg-gray-50 p-6">
@ -108,20 +95,19 @@ const EventDetails: React.FC = () => {
{/* Main Content: Scheduling & Logistics */} {/* Main Content: Scheduling & Logistics */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Left: Scheduling (Escala) */}
<EventScheduler <EventScheduler
agendaId={id!} agendaId={id!}
dataEvento={event.data_evento.split('T')[0]} dataEvento={event.date}
allowedProfessionals={(event as any).assigned_professionals?.map((p: any) => p.professional_id)} allowedProfessionals={event.assignments}
onUpdateStats={setCalculatedStats} onUpdateStats={setCalculatedStats}
defaultTime={event.horario} defaultTime={event.time}
/> />
{/* Right: Logistics (Carros) */} {/* Right: Logistics (Carros) */}
<div className="space-y-6"> <div className="space-y-6">
<EventLogistics <EventLogistics
agendaId={id!} agendaId={id!}
assignedProfessionals={(event as any).assigned_professionals?.map((p: any) => p.professional_id)} assignedProfessionals={event.assignments?.map((a: any) => a.professionalId)}
/> />
{/* Equipment / Studios Section (Placeholder for now based on spreadsheet) */} {/* Equipment / Studios Section (Placeholder for now based on spreadsheet) */}

View file

@ -3,6 +3,7 @@ import { useAuth } from "../contexts/AuthContext";
import { Button } from "../components/Button"; import { Button } from "../components/Button";
import { UserRole } from "../types"; import { UserRole } from "../types";
import { X } from "lucide-react"; import { X } from "lucide-react";
import { verifyAccessCode } from "../services/apiService";
interface LoginProps { interface LoginProps {
onNavigate?: (page: string) => void; onNavigate?: (page: string) => void;
@ -21,7 +22,7 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
const [accessCode, setAccessCode] = useState(""); const [accessCode, setAccessCode] = useState("");
const [showProfessionalPrompt, setShowProfessionalPrompt] = useState(false); const [showProfessionalPrompt, setShowProfessionalPrompt] = useState(false);
const [codeError, setCodeError] = useState(""); const [codeError, setCodeError] = useState("");
const MOCK_ACCESS_CODE = "PHOTUM2025"; // const MOCK_ACCESS_CODE = "PHOTUM2025"; // Removed mock
const handleRegisterClick = () => { const handleRegisterClick = () => {
setShowProfessionalPrompt(true); setShowProfessionalPrompt(true);
@ -29,17 +30,22 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
setCodeError(""); setCodeError("");
}; };
const handleVerifyCode = () => { const handleVerifyCode = async () => {
if (accessCode.trim() === "") { if (accessCode.trim() === "") {
setCodeError("Por favor, digite o código de acesso"); setCodeError("Por favor, digite o código de acesso");
return; return;
} }
if (accessCode.toUpperCase() === MOCK_ACCESS_CODE) { try {
setShowAccessCodeModal(false); const res = await verifyAccessCode(accessCode.toUpperCase());
window.location.href = "/cadastro"; if (res.data && res.data.valid) {
} else { setShowAccessCodeModal(false);
setCodeError("Código de acesso inválido ou expirado"); window.location.href = "/cadastro";
} else {
setCodeError(res.data?.error || "Código de acesso inválido ou expirado");
}
} catch (e) {
setCodeError("Erro ao verificar código");
} }
}; };