feat: implementa função de coordenador de eventos
- Adiciona coluna `is_coordinator` na tabela `agenda_profissionais` - Atualiza queries SQL e gera código com sqlc - Implementa endpoint `PUT /api/agenda/:id/professionals/:profId/coordinator` - Adiciona ícone de estrela no Dashboard para definir coordenadores - Restringe acesso à aba de Logística apenas para coordenadores e admins
This commit is contained in:
parent
050c164286
commit
1ba9499074
14 changed files with 228 additions and 32 deletions
|
|
@ -238,6 +238,7 @@ func main() {
|
||||||
api.DELETE("/agenda/:id/professionals/:profId", auth.RequireWriteAccess(), agendaHandler.RemoveProfessional)
|
api.DELETE("/agenda/:id/professionals/:profId", auth.RequireWriteAccess(), agendaHandler.RemoveProfessional)
|
||||||
api.PATCH("/agenda/:id/professionals/:profId/status", auth.RequireWriteAccess(), agendaHandler.UpdateAssignmentStatus)
|
api.PATCH("/agenda/:id/professionals/:profId/status", auth.RequireWriteAccess(), agendaHandler.UpdateAssignmentStatus)
|
||||||
api.PATCH("/agenda/:id/professionals/:profId/position", auth.RequireWriteAccess(), agendaHandler.UpdateAssignmentPosition)
|
api.PATCH("/agenda/:id/professionals/:profId/position", auth.RequireWriteAccess(), agendaHandler.UpdateAssignmentPosition)
|
||||||
|
api.PUT("/agenda/:id/professionals/:profId/coordinator", auth.RequireWriteAccess(), agendaHandler.SetCoordinator)
|
||||||
api.PATCH("/agenda/:id/status", auth.RequireWriteAccess(), agendaHandler.UpdateStatus)
|
api.PATCH("/agenda/:id/status", auth.RequireWriteAccess(), agendaHandler.UpdateStatus)
|
||||||
api.POST("/agenda/:id/notify-logistics", auth.RequireWriteAccess(), agendaHandler.NotifyLogistics)
|
api.POST("/agenda/:id/notify-logistics", auth.RequireWriteAccess(), agendaHandler.NotifyLogistics)
|
||||||
api.POST("/import/agenda", auth.RequireWriteAccess(), agendaHandler.Import)
|
api.POST("/import/agenda", auth.RequireWriteAccess(), agendaHandler.Import)
|
||||||
|
|
|
||||||
|
|
@ -550,3 +550,45 @@ func (h *Handler) Import(c *gin.Context) {
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Agenda importada com sucesso"})
|
c.JSON(http.StatusOK, gin.H{"message": "Agenda importada com sucesso"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetCoordinator godoc
|
||||||
|
// @Summary Set professional as coordinator
|
||||||
|
// @Tags agenda
|
||||||
|
// @Router /api/agenda/{id}/professionals/{profId}/coordinator [put]
|
||||||
|
func (h *Handler) SetCoordinator(c *gin.Context) {
|
||||||
|
idParam := c.Param("id")
|
||||||
|
agendaID, err := uuid.Parse(idParam)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "ID de agenda inválido"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
profIdParam := c.Param("profId")
|
||||||
|
profID, err := uuid.Parse(profIdParam)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "ID de profissional inválido"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
IsCoordinator bool `json:"is_coordinator"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Dados inválidos: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security: Block RESEARCHER
|
||||||
|
if c.GetString("role") == "RESEARCHER" {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "Acesso negado: Somente leitura"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// regiao := c.GetString("regiao") // Not needed for SetCoordinator
|
||||||
|
if err := h.service.SetCoordinator(c.Request.Context(), agendaID, profID, req.IsCoordinator); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erro ao definir coordenador: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ type Assignment struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
MotivoRejeicao *string `json:"motivo_rejeicao"`
|
MotivoRejeicao *string `json:"motivo_rejeicao"`
|
||||||
FuncaoID *string `json:"funcao_id"`
|
FuncaoID *string `json:"funcao_id"`
|
||||||
|
IsCoordinator bool `json:"is_coordinator"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgendaResponse struct {
|
type AgendaResponse struct {
|
||||||
|
|
@ -415,6 +416,14 @@ func (s *Service) AssignProfessional(ctx context.Context, agendaID uuid.UUID, pr
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) SetCoordinator(ctx context.Context, agendaID, profID uuid.UUID, isCoordinator bool) error {
|
||||||
|
return s.queries.SetCoordinator(ctx, generated.SetCoordinatorParams{
|
||||||
|
AgendaID: pgtype.UUID{Bytes: agendaID, Valid: true},
|
||||||
|
ProfissionalID: pgtype.UUID{Bytes: profID, Valid: true},
|
||||||
|
IsCoordinator: pgtype.Bool{Bool: isCoordinator, Valid: true},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) RemoveProfessional(ctx context.Context, agendaID uuid.UUID, profID uuid.UUID) error {
|
func (s *Service) RemoveProfessional(ctx context.Context, agendaID uuid.UUID, profID uuid.UUID) error {
|
||||||
params := generated.RemoveProfessionalParams{
|
params := generated.RemoveProfessionalParams{
|
||||||
AgendaID: pgtype.UUID{Bytes: agendaID, Valid: true},
|
AgendaID: pgtype.UUID{Bytes: agendaID, Valid: true},
|
||||||
|
|
|
||||||
|
|
@ -443,7 +443,8 @@ SELECT
|
||||||
'professional_id', ap.profissional_id,
|
'professional_id', ap.profissional_id,
|
||||||
'status', ap.status,
|
'status', ap.status,
|
||||||
'motivo_rejeicao', ap.motivo_rejeicao,
|
'motivo_rejeicao', ap.motivo_rejeicao,
|
||||||
'funcao_id', ap.funcao_id
|
'funcao_id', ap.funcao_id,
|
||||||
|
'is_coordinator', ap.is_coordinator
|
||||||
))
|
))
|
||||||
FROM agenda_profissionais ap
|
FROM agenda_profissionais ap
|
||||||
WHERE ap.agenda_id = a.id),
|
WHERE ap.agenda_id = a.id),
|
||||||
|
|
@ -579,7 +580,8 @@ SELECT
|
||||||
'professional_id', ap.profissional_id,
|
'professional_id', ap.profissional_id,
|
||||||
'status', ap.status,
|
'status', ap.status,
|
||||||
'motivo_rejeicao', ap.motivo_rejeicao,
|
'motivo_rejeicao', ap.motivo_rejeicao,
|
||||||
'funcao_id', ap.funcao_id
|
'funcao_id', ap.funcao_id,
|
||||||
|
'is_coordinator', ap.is_coordinator
|
||||||
))
|
))
|
||||||
FROM agenda_profissionais ap
|
FROM agenda_profissionais ap
|
||||||
WHERE ap.agenda_id = a.id),
|
WHERE ap.agenda_id = a.id),
|
||||||
|
|
@ -823,7 +825,8 @@ SELECT
|
||||||
'professional_id', ap.profissional_id,
|
'professional_id', ap.profissional_id,
|
||||||
'status', ap.status,
|
'status', ap.status,
|
||||||
'motivo_rejeicao', ap.motivo_rejeicao,
|
'motivo_rejeicao', ap.motivo_rejeicao,
|
||||||
'funcao_id', ap.funcao_id
|
'funcao_id', ap.funcao_id,
|
||||||
|
'is_coordinator', ap.is_coordinator
|
||||||
))
|
))
|
||||||
FROM agenda_profissionais ap
|
FROM agenda_profissionais ap
|
||||||
WHERE ap.agenda_id = a.id),
|
WHERE ap.agenda_id = a.id),
|
||||||
|
|
@ -1091,6 +1094,23 @@ func (q *Queries) RemoveProfessional(ctx context.Context, arg RemoveProfessional
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setCoordinator = `-- name: SetCoordinator :exec
|
||||||
|
UPDATE agenda_profissionais
|
||||||
|
SET is_coordinator = $3
|
||||||
|
WHERE agenda_id = $1 AND profissional_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type SetCoordinatorParams struct {
|
||||||
|
AgendaID pgtype.UUID `json:"agenda_id"`
|
||||||
|
ProfissionalID pgtype.UUID `json:"profissional_id"`
|
||||||
|
IsCoordinator pgtype.Bool `json:"is_coordinator"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) SetCoordinator(ctx context.Context, arg SetCoordinatorParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, setCoordinator, arg.AgendaID, arg.ProfissionalID, arg.IsCoordinator)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
const updateAgenda = `-- name: UpdateAgenda :one
|
const updateAgenda = `-- name: UpdateAgenda :one
|
||||||
UPDATE agenda
|
UPDATE agenda
|
||||||
SET
|
SET
|
||||||
|
|
@ -1274,7 +1294,7 @@ const updateAssignmentPosition = `-- name: UpdateAssignmentPosition :one
|
||||||
UPDATE agenda_profissionais
|
UPDATE agenda_profissionais
|
||||||
SET posicao = $3
|
SET posicao = $3
|
||||||
WHERE agenda_id = $1 AND profissional_id = $2
|
WHERE agenda_id = $1 AND profissional_id = $2
|
||||||
RETURNING id, agenda_id, profissional_id, status, motivo_rejeicao, funcao_id, posicao, criado_em
|
RETURNING id, agenda_id, profissional_id, status, motivo_rejeicao, funcao_id, posicao, criado_em, is_coordinator
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateAssignmentPositionParams struct {
|
type UpdateAssignmentPositionParams struct {
|
||||||
|
|
@ -1295,6 +1315,7 @@ func (q *Queries) UpdateAssignmentPosition(ctx context.Context, arg UpdateAssign
|
||||||
&i.FuncaoID,
|
&i.FuncaoID,
|
||||||
&i.Posicao,
|
&i.Posicao,
|
||||||
&i.CriadoEm,
|
&i.CriadoEm,
|
||||||
|
&i.IsCoordinator,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -1303,7 +1324,7 @@ const updateAssignmentStatus = `-- name: UpdateAssignmentStatus :one
|
||||||
UPDATE agenda_profissionais
|
UPDATE agenda_profissionais
|
||||||
SET status = $3, motivo_rejeicao = $4
|
SET status = $3, motivo_rejeicao = $4
|
||||||
WHERE agenda_id = $1 AND profissional_id = $2
|
WHERE agenda_id = $1 AND profissional_id = $2
|
||||||
RETURNING id, agenda_id, profissional_id, status, motivo_rejeicao, funcao_id, posicao, criado_em
|
RETURNING id, agenda_id, profissional_id, status, motivo_rejeicao, funcao_id, posicao, criado_em, is_coordinator
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateAssignmentStatusParams struct {
|
type UpdateAssignmentStatusParams struct {
|
||||||
|
|
@ -1330,6 +1351,7 @@ func (q *Queries) UpdateAssignmentStatus(ctx context.Context, arg UpdateAssignme
|
||||||
&i.FuncaoID,
|
&i.FuncaoID,
|
||||||
&i.Posicao,
|
&i.Posicao,
|
||||||
&i.CriadoEm,
|
&i.CriadoEm,
|
||||||
|
&i.IsCoordinator,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ type AgendaProfissionai struct {
|
||||||
FuncaoID pgtype.UUID `json:"funcao_id"`
|
FuncaoID pgtype.UUID `json:"funcao_id"`
|
||||||
Posicao pgtype.Text `json:"posicao"`
|
Posicao pgtype.Text `json:"posicao"`
|
||||||
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||||
|
IsCoordinator pgtype.Bool `json:"is_coordinator"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnosFormatura struct {
|
type AnosFormatura struct {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
-- Up
|
||||||
|
ALTER TABLE agenda_profissionais ADD COLUMN IF NOT EXISTS is_coordinator BOOLEAN DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- Down
|
||||||
|
ALTER TABLE agenda_profissionais DROP COLUMN IF EXISTS is_coordinator;
|
||||||
|
|
@ -50,7 +50,8 @@ SELECT
|
||||||
'professional_id', ap.profissional_id,
|
'professional_id', ap.profissional_id,
|
||||||
'status', ap.status,
|
'status', ap.status,
|
||||||
'motivo_rejeicao', ap.motivo_rejeicao,
|
'motivo_rejeicao', ap.motivo_rejeicao,
|
||||||
'funcao_id', ap.funcao_id
|
'funcao_id', ap.funcao_id,
|
||||||
|
'is_coordinator', ap.is_coordinator
|
||||||
))
|
))
|
||||||
FROM agenda_profissionais ap
|
FROM agenda_profissionais ap
|
||||||
WHERE ap.agenda_id = a.id),
|
WHERE ap.agenda_id = a.id),
|
||||||
|
|
@ -81,7 +82,8 @@ SELECT
|
||||||
'professional_id', ap.profissional_id,
|
'professional_id', ap.profissional_id,
|
||||||
'status', ap.status,
|
'status', ap.status,
|
||||||
'motivo_rejeicao', ap.motivo_rejeicao,
|
'motivo_rejeicao', ap.motivo_rejeicao,
|
||||||
'funcao_id', ap.funcao_id
|
'funcao_id', ap.funcao_id,
|
||||||
|
'is_coordinator', ap.is_coordinator
|
||||||
))
|
))
|
||||||
FROM agenda_profissionais ap
|
FROM agenda_profissionais ap
|
||||||
WHERE ap.agenda_id = a.id),
|
WHERE ap.agenda_id = a.id),
|
||||||
|
|
@ -228,7 +230,8 @@ SELECT
|
||||||
'professional_id', ap.profissional_id,
|
'professional_id', ap.profissional_id,
|
||||||
'status', ap.status,
|
'status', ap.status,
|
||||||
'motivo_rejeicao', ap.motivo_rejeicao,
|
'motivo_rejeicao', ap.motivo_rejeicao,
|
||||||
'funcao_id', ap.funcao_id
|
'funcao_id', ap.funcao_id,
|
||||||
|
'is_coordinator', ap.is_coordinator
|
||||||
))
|
))
|
||||||
FROM agenda_profissionais ap
|
FROM agenda_profissionais ap
|
||||||
WHERE ap.agenda_id = a.id),
|
WHERE ap.agenda_id = a.id),
|
||||||
|
|
@ -241,4 +244,9 @@ JOIN empresas e ON cf.empresa_id = e.id
|
||||||
JOIN anos_formaturas af ON cf.ano_formatura_id = af.id
|
JOIN anos_formaturas af ON cf.ano_formatura_id = af.id
|
||||||
JOIN tipos_eventos te ON a.tipo_evento_id = te.id
|
JOIN tipos_eventos te ON a.tipo_evento_id = te.id
|
||||||
WHERE cf.empresa_id = $1 AND a.regiao = @regiao
|
WHERE cf.empresa_id = $1 AND a.regiao = @regiao
|
||||||
ORDER BY a.data_evento;
|
ORDER BY a.data_evento;
|
||||||
|
|
||||||
|
-- name: SetCoordinator :exec
|
||||||
|
UPDATE agenda_profissionais
|
||||||
|
SET is_coordinator = $3
|
||||||
|
WHERE agenda_id = $1 AND profissional_id = $2;
|
||||||
|
|
@ -524,3 +524,6 @@ BEGIN
|
||||||
DO UPDATE SET valor = EXCLUDED.valor;
|
DO UPDATE SET valor = EXCLUDED.valor;
|
||||||
|
|
||||||
END $$;
|
END $$;
|
||||||
|
|
||||||
|
-- Migration 019: Add Coordinator Column
|
||||||
|
ALTER TABLE agenda_profissionais ADD COLUMN IF NOT EXISTS is_coordinator BOOLEAN DEFAULT FALSE;
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||||
bairro: backendUser.bairro,
|
bairro: backendUser.bairro,
|
||||||
cidade: backendUser.cidade,
|
cidade: backendUser.cidade,
|
||||||
estado: backendUser.estado,
|
estado: backendUser.estado,
|
||||||
|
professionalId: data.profissional?.id, // Map professional ID
|
||||||
};
|
};
|
||||||
console.log("AuthContext: restoreSession mapped user:", mappedUser);
|
console.log("AuthContext: restoreSession mapped user:", mappedUser);
|
||||||
if (!backendUser.ativo) {
|
if (!backendUser.ativo) {
|
||||||
|
|
@ -219,6 +220,7 @@ const login = async (email: string, password?: string) => {
|
||||||
bairro: backendUser.bairro,
|
bairro: backendUser.bairro,
|
||||||
cidade: backendUser.cidade,
|
cidade: backendUser.cidade,
|
||||||
estado: backendUser.estado,
|
estado: backendUser.estado,
|
||||||
|
professionalId: data.profissional?.id, // Map professional ID
|
||||||
};
|
};
|
||||||
|
|
||||||
setUser(mappedUser);
|
setUser(mappedUser);
|
||||||
|
|
|
||||||
|
|
@ -611,6 +611,7 @@ interface DataContextType {
|
||||||
updateEventDetails: (id: string, data: any) => Promise<void>;
|
updateEventDetails: (id: string, data: any) => Promise<void>;
|
||||||
functions: { id: string; nome: string }[];
|
functions: { id: string; nome: string }[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
refreshEvents: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DataContext = createContext<DataContextType | undefined>(undefined);
|
const DataContext = createContext<DataContextType | undefined>(undefined);
|
||||||
|
|
@ -630,6 +631,7 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
const [pendingUsers, setPendingUsers] = useState<User[]>([]);
|
const [pendingUsers, setPendingUsers] = useState<User[]>([]);
|
||||||
const [professionals, setProfessionals] = useState<Professional[]>([]);
|
const [professionals, setProfessionals] = useState<Professional[]>([]);
|
||||||
const [functions, setFunctions] = useState<{ id: string; nome: string }[]>([]);
|
const [functions, setFunctions] = useState<{ id: string; nome: string }[]>([]);
|
||||||
|
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||||
|
|
||||||
// Fetch events from API
|
// Fetch events from API
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -726,7 +728,8 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
professionalId: a.professional_id,
|
professionalId: a.professional_id,
|
||||||
status: a.status,
|
status: a.status,
|
||||||
reason: a.motivo_rejeicao,
|
reason: a.motivo_rejeicao,
|
||||||
funcaoId: a.funcao_id
|
funcaoId: a.funcao_id,
|
||||||
|
is_coordinator: a.is_coordinator
|
||||||
}))
|
}))
|
||||||
: [],
|
: [],
|
||||||
logisticaNotificacaoEnviadaEm: e.logistica_notificacao_enviada_em,
|
logisticaNotificacaoEnviadaEm: e.logistica_notificacao_enviada_em,
|
||||||
|
|
@ -743,7 +746,11 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchEvents();
|
fetchEvents();
|
||||||
}, [token]); // React to token change
|
}, [token, refreshTrigger]); // React to token change and manual refresh
|
||||||
|
|
||||||
|
const refreshEvents = async () => {
|
||||||
|
setRefreshTrigger(prev => prev + 1);
|
||||||
|
};
|
||||||
|
|
||||||
// Fetch pending users from API
|
// Fetch pending users from API
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -1202,6 +1209,7 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
},
|
},
|
||||||
functions,
|
functions,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
refreshEvents,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ import {
|
||||||
UserCheck,
|
UserCheck,
|
||||||
UserX,
|
UserX,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
|
Star,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { setCoordinator } from "../services/apiService";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import { useData } from "../contexts/DataContext";
|
import { useData } from "../contexts/DataContext";
|
||||||
import { STATUS_COLORS } from "../constants";
|
import { STATUS_COLORS } from "../constants";
|
||||||
|
|
@ -49,6 +51,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
updateEventDetails,
|
updateEventDetails,
|
||||||
functions,
|
functions,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
refreshEvents,
|
||||||
} = useData();
|
} = useData();
|
||||||
|
|
||||||
// ... (inside component)
|
// ... (inside component)
|
||||||
|
|
@ -244,6 +247,30 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSetCoordinator = async (professionalId: string, currentStatus: boolean) => {
|
||||||
|
if (!user || !selectedEvent) return;
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (!token) return;
|
||||||
|
|
||||||
|
const res = await setCoordinator(token, selectedEvent.id, professionalId, !currentStatus);
|
||||||
|
if (res.error) {
|
||||||
|
alert("Erro ao definir coordenador: " + res.error);
|
||||||
|
} else {
|
||||||
|
// Optimistic update
|
||||||
|
setSelectedEvent(prev => prev ? ({
|
||||||
|
...prev,
|
||||||
|
assignments: prev.assignments?.map(a =>
|
||||||
|
a.professionalId === professionalId ? { ...a, is_coordinator: !currentStatus } : a
|
||||||
|
)
|
||||||
|
}) : null);
|
||||||
|
|
||||||
|
// Force refresh to get data from server and persist color
|
||||||
|
setTimeout(() => {
|
||||||
|
refreshEvents();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Função para fechar modal de equipe e limpar filtros
|
// Função para fechar modal de equipe e limpar filtros
|
||||||
const closeTeamModal = () => {
|
const closeTeamModal = () => {
|
||||||
setIsTeamModalOpen(false);
|
setIsTeamModalOpen(false);
|
||||||
|
|
@ -1036,8 +1063,8 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{/* Seção de Gestão de Equipe - Ocultar para Fotógrafos */}
|
{/* Seção de Gestão de Equipe - Ocultar para Fotógrafos, mas mostrar para Coordenadores */}
|
||||||
{user.role !== UserRole.PHOTOGRAPHER && (
|
{(user.role !== UserRole.PHOTOGRAPHER || (selectedEvent.assignments || []).some((a: any) => a.professionalId === (user.professionalId || user.id) && a.is_coordinator)) && (
|
||||||
<>
|
<>
|
||||||
<tr className="bg-blue-50">
|
<tr className="bg-blue-50">
|
||||||
<td colSpan={2} className="px-4 py-3 text-xs font-bold text-blue-700 uppercase tracking-wider">
|
<td colSpan={2} className="px-4 py-3 text-xs font-bold text-blue-700 uppercase tracking-wider">
|
||||||
|
|
@ -1419,17 +1446,28 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
onClick={() => handleViewProfessional(photographer!)}
|
onClick={() => handleViewProfessional(photographer!)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="w-8 h-8 rounded-full border-2 border-gray-200 bg-gray-300 flex-shrink-0"
|
className="relative w-8 h-8 rounded-full border-2 border-gray-200 bg-gray-300 flex-shrink-0"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(${photographer?.avatar ||
|
backgroundImage: `url(${photographer?.avatar ||
|
||||||
`https://i.pravatar.cc/100?u=${assignment.professionalId}`
|
`https://i.pravatar.cc/100?u=${assignment.professionalId}`
|
||||||
})`,
|
})`,
|
||||||
backgroundSize: "cover",
|
backgroundSize: "cover",
|
||||||
}}
|
}}
|
||||||
></div>
|
>
|
||||||
|
{assignment.is_coordinator && (
|
||||||
|
<div className="absolute -top-1 -right-1 bg-white rounded-full p-0.5 shadow-sm border border-gray-100">
|
||||||
|
<Star size={10} className="text-yellow-500 fill-yellow-500" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-gray-700 font-medium">
|
<span className="text-gray-700 font-medium">
|
||||||
{photographer?.name || "Fotógrafo"}
|
{photographer?.name || "Fotógrafo"}
|
||||||
|
{assignment.is_coordinator && (
|
||||||
|
<span className="ml-1 text-xs text-yellow-600 font-bold border border-yellow-200 bg-yellow-50 px-1 rounded">
|
||||||
|
Coord.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-xs text-gray-500">
|
||||||
{assignment.status === "PENDENTE" ? "Convite Pendente" : "Confirmado"}
|
{assignment.status === "PENDENTE" ? "Convite Pendente" : "Confirmado"}
|
||||||
|
|
@ -1683,22 +1721,31 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
{/* Profissional */}
|
{/* Profissional */}
|
||||||
<td className="p-4 cursor-pointer" onClick={() => handleViewProfessional(photographer)}>
|
<td className="p-4 cursor-pointer" onClick={() => handleViewProfessional(photographer)}>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="relative">
|
||||||
|
{/* Avatar */}
|
||||||
<div
|
<div
|
||||||
className="w-10 h-10 rounded-full border-2 border-gray-200 bg-gray-300 flex-shrink-0"
|
className="w-10 h-10 rounded-full border border-gray-200 bg-gray-300 flex-shrink-0"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(${photographer.avatar})`,
|
backgroundImage: `url(${photographer.avatar})`,
|
||||||
backgroundSize: "cover",
|
backgroundSize: "cover",
|
||||||
}}
|
backgroundPosition: "center",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div>
|
{assignment?.is_coordinator && (
|
||||||
<p className="font-semibold text-gray-900">
|
<div className="absolute -top-1 -right-1 bg-white rounded-full p-0.5 shadow-sm border border-gray-100">
|
||||||
{photographer.name}
|
<Star size={12} className="text-yellow-500 fill-yellow-500" />
|
||||||
</p>
|
</div>
|
||||||
<p className="text-xs text-gray-500">
|
)}
|
||||||
ID: {photographer.id}
|
</div>
|
||||||
</p>
|
<div>
|
||||||
</div>
|
<p className="font-semibold text-gray-800 text-sm">
|
||||||
</div>
|
{photographer.name || photographer.nome}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
ID: {photographer.id.substring(0, 8)}...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* Função */}
|
{/* Função */}
|
||||||
|
|
@ -1751,6 +1798,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
|
|
||||||
{/* Ação */}
|
{/* Ação */}
|
||||||
<td className="p-4 text-center">
|
<td className="p-4 text-center">
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
togglePhotographer(photographer.id)
|
togglePhotographer(photographer.id)
|
||||||
|
|
@ -1764,6 +1812,19 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
{isProcessing && <div className="w-3 h-3 border-2 border-current border-t-transparent rounded-full animate-spin"></div>}
|
{isProcessing && <div className="w-3 h-3 border-2 border-current border-t-transparent rounded-full animate-spin"></div>}
|
||||||
{status === "ACEITO" || status === "PENDENTE" ? "Remover" : "Adicionar"}
|
{status === "ACEITO" || status === "PENDENTE" ? "Remover" : "Adicionar"}
|
||||||
</button>
|
</button>
|
||||||
|
{(status === "ACEITO" || status === "PENDENTE") && (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleSetCoordinator(photographer.id, !!assignment?.is_coordinator);
|
||||||
|
}}
|
||||||
|
className={`p-2 rounded-full hover:bg-gray-100 transition-colors ${assignment?.is_coordinator ? "bg-yellow-50" : ""}`}
|
||||||
|
title={assignment?.is_coordinator ? "Remover coordenação" : "Definir como coordenador"}
|
||||||
|
>
|
||||||
|
<Star size={18} className={assignment?.is_coordinator ? "text-yellow-500 fill-yellow-500" : "text-gray-400"} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|
@ -1818,15 +1879,22 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
<div key={photographer.id} className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
|
<div key={photographer.id} className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
|
||||||
<div className="flex items-center gap-3 mb-3" onClick={() => handleViewProfessional(photographer)}>
|
<div className="flex items-center gap-3 mb-3" onClick={() => handleViewProfessional(photographer)}>
|
||||||
<div
|
<div
|
||||||
className="w-12 h-12 rounded-full border-2 border-gray-200 bg-gray-300 flex-shrink-0"
|
className="relative w-12 h-12 rounded-full border-2 border-gray-200 bg-gray-300 flex-shrink-0"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(${photographer.avatar})`,
|
backgroundImage: `url(${photographer.avatar})`,
|
||||||
backgroundSize: "cover",
|
backgroundSize: "cover",
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
{/* Visual Indicator of Coordinator in the list */}
|
||||||
|
{assignment?.is_coordinator && (
|
||||||
|
<div className="absolute top-0 right-0 p-1">
|
||||||
|
<Star size={12} className="text-yellow-500 fill-yellow-500" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-bold text-gray-900">{photographer.name || photographer.nome}</h4>
|
<p className="font-medium text-gray-800 text-sm">{photographer.name || photographer.nome}</p>
|
||||||
<p className="text-xs text-gray-500">ID: {photographer.id.substring(0, 8)}...</p>
|
<p className="text-xs text-gray-500">{assignment?.status === "PENDENTE" ? "Convite Pendente" : assignment?.status}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ const EventDetails: React.FC = () => {
|
||||||
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>;
|
||||||
|
|
||||||
// Check if user can view logistics
|
// Check if user can view logistics
|
||||||
const canViewLogistics = user?.role !== UserRole.AGENDA_VIEWER && user?.role !== UserRole.RESEARCHER;
|
const isCoordinator = event?.assignments?.some(a => a.professionalId === (user?.professionalId || user?.id) && a.is_coordinator);
|
||||||
|
const canViewLogistics = isCoordinator || user?.role === UserRole.SUPERADMIN || user?.role === UserRole.EVENT_OWNER || user?.role === UserRole.BUSINESS_OWNER;
|
||||||
|
|
||||||
// Use event.date which is already YYYY-MM-DD from DataContext
|
// Use event.date which is already YYYY-MM-DD from DataContext
|
||||||
const formattedDate = new Date(event.date + "T00:00:00").toLocaleDateString();
|
const formattedDate = new Date(event.date + "T00:00:00").toLocaleDateString();
|
||||||
|
|
|
||||||
|
|
@ -1362,3 +1362,27 @@ export async function getProfessionalFinancialStatement(token: string): Promise<
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const setCoordinator = async (token: string, eventId: string, professionalId: string, isCoordinator: boolean) => {
|
||||||
|
try {
|
||||||
|
const region = localStorage.getItem("photum_selected_region") || "SP";
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/agenda/${eventId}/professionals/${professionalId}/coordinator`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-regiao": region
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ is_coordinator: isCoordinator }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
return { error: errorData.error || "Failed to set coordinator" };
|
||||||
|
}
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("API setCoordinator error:", error);
|
||||||
|
return { error: "Network error" };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ export interface User {
|
||||||
empresaId?: string; // ID da empresa vinculada (para Business Owners)
|
empresaId?: string; // ID da empresa vinculada (para Business Owners)
|
||||||
companyName?: string; // Nome da empresa vinculada
|
companyName?: string; // Nome da empresa vinculada
|
||||||
allowedRegions?: string[]; // Regiões permitidas para o usuário
|
allowedRegions?: string[]; // Regiões permitidas para o usuário
|
||||||
|
professionalId?: string; // ID do profissional vinculado (se houver)
|
||||||
|
|
||||||
// Client / Event Owner specific fields
|
// Client / Event Owner specific fields
|
||||||
cpf_cnpj?: string;
|
cpf_cnpj?: string;
|
||||||
|
|
@ -123,6 +124,7 @@ export interface Assignment {
|
||||||
status: AssignmentStatus;
|
status: AssignmentStatus;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
funcaoId?: string;
|
funcaoId?: string;
|
||||||
|
is_coordinator?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EventData {
|
export interface EventData {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue