Merge pull request #41 from rede5/Front-back-integracao-task17
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:
commit
9ff55b36bd
11 changed files with 224 additions and 74 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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) */}
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue