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.PATCH("/agenda/:id/professionals/:profId/status", auth.RequireWriteAccess(), agendaHandler.UpdateAssignmentStatus)
|
||||
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.POST("/agenda/:id/notify-logistics", auth.RequireWriteAccess(), agendaHandler.NotifyLogistics)
|
||||
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"})
|
||||
}
|
||||
|
||||
// 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"`
|
||||
MotivoRejeicao *string `json:"motivo_rejeicao"`
|
||||
FuncaoID *string `json:"funcao_id"`
|
||||
IsCoordinator bool `json:"is_coordinator"`
|
||||
}
|
||||
|
||||
type AgendaResponse struct {
|
||||
|
|
@ -415,6 +416,14 @@ func (s *Service) AssignProfessional(ctx context.Context, agendaID uuid.UUID, pr
|
|||
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 {
|
||||
params := generated.RemoveProfessionalParams{
|
||||
AgendaID: pgtype.UUID{Bytes: agendaID, Valid: true},
|
||||
|
|
|
|||
|
|
@ -443,7 +443,8 @@ SELECT
|
|||
'professional_id', ap.profissional_id,
|
||||
'status', ap.status,
|
||||
'motivo_rejeicao', ap.motivo_rejeicao,
|
||||
'funcao_id', ap.funcao_id
|
||||
'funcao_id', ap.funcao_id,
|
||||
'is_coordinator', ap.is_coordinator
|
||||
))
|
||||
FROM agenda_profissionais ap
|
||||
WHERE ap.agenda_id = a.id),
|
||||
|
|
@ -579,7 +580,8 @@ SELECT
|
|||
'professional_id', ap.profissional_id,
|
||||
'status', ap.status,
|
||||
'motivo_rejeicao', ap.motivo_rejeicao,
|
||||
'funcao_id', ap.funcao_id
|
||||
'funcao_id', ap.funcao_id,
|
||||
'is_coordinator', ap.is_coordinator
|
||||
))
|
||||
FROM agenda_profissionais ap
|
||||
WHERE ap.agenda_id = a.id),
|
||||
|
|
@ -823,7 +825,8 @@ SELECT
|
|||
'professional_id', ap.profissional_id,
|
||||
'status', ap.status,
|
||||
'motivo_rejeicao', ap.motivo_rejeicao,
|
||||
'funcao_id', ap.funcao_id
|
||||
'funcao_id', ap.funcao_id,
|
||||
'is_coordinator', ap.is_coordinator
|
||||
))
|
||||
FROM agenda_profissionais ap
|
||||
WHERE ap.agenda_id = a.id),
|
||||
|
|
@ -1091,6 +1094,23 @@ func (q *Queries) RemoveProfessional(ctx context.Context, arg RemoveProfessional
|
|||
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
|
||||
UPDATE agenda
|
||||
SET
|
||||
|
|
@ -1274,7 +1294,7 @@ const updateAssignmentPosition = `-- name: UpdateAssignmentPosition :one
|
|||
UPDATE agenda_profissionais
|
||||
SET posicao = $3
|
||||
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 {
|
||||
|
|
@ -1295,6 +1315,7 @@ func (q *Queries) UpdateAssignmentPosition(ctx context.Context, arg UpdateAssign
|
|||
&i.FuncaoID,
|
||||
&i.Posicao,
|
||||
&i.CriadoEm,
|
||||
&i.IsCoordinator,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -1303,7 +1324,7 @@ const updateAssignmentStatus = `-- name: UpdateAssignmentStatus :one
|
|||
UPDATE agenda_profissionais
|
||||
SET status = $3, motivo_rejeicao = $4
|
||||
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 {
|
||||
|
|
@ -1330,6 +1351,7 @@ func (q *Queries) UpdateAssignmentStatus(ctx context.Context, arg UpdateAssignme
|
|||
&i.FuncaoID,
|
||||
&i.Posicao,
|
||||
&i.CriadoEm,
|
||||
&i.IsCoordinator,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ type AgendaProfissionai struct {
|
|||
FuncaoID pgtype.UUID `json:"funcao_id"`
|
||||
Posicao pgtype.Text `json:"posicao"`
|
||||
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||
IsCoordinator pgtype.Bool `json:"is_coordinator"`
|
||||
}
|
||||
|
||||
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,
|
||||
'status', ap.status,
|
||||
'motivo_rejeicao', ap.motivo_rejeicao,
|
||||
'funcao_id', ap.funcao_id
|
||||
'funcao_id', ap.funcao_id,
|
||||
'is_coordinator', ap.is_coordinator
|
||||
))
|
||||
FROM agenda_profissionais ap
|
||||
WHERE ap.agenda_id = a.id),
|
||||
|
|
@ -81,7 +82,8 @@ SELECT
|
|||
'professional_id', ap.profissional_id,
|
||||
'status', ap.status,
|
||||
'motivo_rejeicao', ap.motivo_rejeicao,
|
||||
'funcao_id', ap.funcao_id
|
||||
'funcao_id', ap.funcao_id,
|
||||
'is_coordinator', ap.is_coordinator
|
||||
))
|
||||
FROM agenda_profissionais ap
|
||||
WHERE ap.agenda_id = a.id),
|
||||
|
|
@ -228,7 +230,8 @@ SELECT
|
|||
'professional_id', ap.profissional_id,
|
||||
'status', ap.status,
|
||||
'motivo_rejeicao', ap.motivo_rejeicao,
|
||||
'funcao_id', ap.funcao_id
|
||||
'funcao_id', ap.funcao_id,
|
||||
'is_coordinator', ap.is_coordinator
|
||||
))
|
||||
FROM agenda_profissionais ap
|
||||
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 tipos_eventos te ON a.tipo_evento_id = te.id
|
||||
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;
|
||||
|
||||
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,
|
||||
cidade: backendUser.cidade,
|
||||
estado: backendUser.estado,
|
||||
professionalId: data.profissional?.id, // Map professional ID
|
||||
};
|
||||
console.log("AuthContext: restoreSession mapped user:", mappedUser);
|
||||
if (!backendUser.ativo) {
|
||||
|
|
@ -219,6 +220,7 @@ const login = async (email: string, password?: string) => {
|
|||
bairro: backendUser.bairro,
|
||||
cidade: backendUser.cidade,
|
||||
estado: backendUser.estado,
|
||||
professionalId: data.profissional?.id, // Map professional ID
|
||||
};
|
||||
|
||||
setUser(mappedUser);
|
||||
|
|
|
|||
|
|
@ -611,6 +611,7 @@ interface DataContextType {
|
|||
updateEventDetails: (id: string, data: any) => Promise<void>;
|
||||
functions: { id: string; nome: string }[];
|
||||
isLoading: boolean;
|
||||
refreshEvents: () => Promise<void>;
|
||||
}
|
||||
|
||||
const DataContext = createContext<DataContextType | undefined>(undefined);
|
||||
|
|
@ -630,6 +631,7 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
|||
const [pendingUsers, setPendingUsers] = useState<User[]>([]);
|
||||
const [professionals, setProfessionals] = useState<Professional[]>([]);
|
||||
const [functions, setFunctions] = useState<{ id: string; nome: string }[]>([]);
|
||||
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||
|
||||
// Fetch events from API
|
||||
useEffect(() => {
|
||||
|
|
@ -726,7 +728,8 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
|||
professionalId: a.professional_id,
|
||||
status: a.status,
|
||||
reason: a.motivo_rejeicao,
|
||||
funcaoId: a.funcao_id
|
||||
funcaoId: a.funcao_id,
|
||||
is_coordinator: a.is_coordinator
|
||||
}))
|
||||
: [],
|
||||
logisticaNotificacaoEnviadaEm: e.logistica_notificacao_enviada_em,
|
||||
|
|
@ -743,7 +746,11 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
|||
}
|
||||
};
|
||||
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
|
||||
useEffect(() => {
|
||||
|
|
@ -1202,6 +1209,7 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
|||
},
|
||||
functions,
|
||||
isLoading,
|
||||
refreshEvents,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ import {
|
|||
UserCheck,
|
||||
UserX,
|
||||
AlertCircle,
|
||||
Star,
|
||||
} from "lucide-react";
|
||||
import { setCoordinator } from "../services/apiService";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import { useData } from "../contexts/DataContext";
|
||||
import { STATUS_COLORS } from "../constants";
|
||||
|
|
@ -49,6 +51,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
updateEventDetails,
|
||||
functions,
|
||||
isLoading,
|
||||
refreshEvents,
|
||||
} = useData();
|
||||
|
||||
// ... (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
|
||||
const closeTeamModal = () => {
|
||||
setIsTeamModalOpen(false);
|
||||
|
|
@ -1036,8 +1063,8 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
</td>
|
||||
</tr>
|
||||
|
||||
{/* Seção de Gestão de Equipe - Ocultar para Fotógrafos */}
|
||||
{user.role !== UserRole.PHOTOGRAPHER && (
|
||||
{/* Seção de Gestão de Equipe - Ocultar para Fotógrafos, mas mostrar para Coordenadores */}
|
||||
{(user.role !== UserRole.PHOTOGRAPHER || (selectedEvent.assignments || []).some((a: any) => a.professionalId === (user.professionalId || user.id) && a.is_coordinator)) && (
|
||||
<>
|
||||
<tr className="bg-blue-50">
|
||||
<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!)}
|
||||
>
|
||||
<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={{
|
||||
backgroundImage: `url(${photographer?.avatar ||
|
||||
`https://i.pravatar.cc/100?u=${assignment.professionalId}`
|
||||
})`,
|
||||
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">
|
||||
<span className="text-gray-700 font-medium">
|
||||
{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 className="text-xs text-gray-500">
|
||||
{assignment.status === "PENDENTE" ? "Convite Pendente" : "Confirmado"}
|
||||
|
|
@ -1683,22 +1721,31 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
{/* Profissional */}
|
||||
<td className="p-4 cursor-pointer" onClick={() => handleViewProfessional(photographer)}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
{/* Avatar */}
|
||||
<div
|
||||
className="w-10 h-10 rounded-full border-2 border-gray-200 bg-gray-300 flex-shrink-0"
|
||||
style={{
|
||||
className="w-10 h-10 rounded-full border border-gray-200 bg-gray-300 flex-shrink-0"
|
||||
style={{
|
||||
backgroundImage: `url(${photographer.avatar})`,
|
||||
backgroundSize: "cover",
|
||||
}}
|
||||
backgroundPosition: "center",
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">
|
||||
{photographer.name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
ID: {photographer.id}
|
||||
</p>
|
||||
</div>
|
||||
</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={12} className="text-yellow-500 fill-yellow-500" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-800 text-sm">
|
||||
{photographer.name || photographer.nome}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
ID: {photographer.id.substring(0, 8)}...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Função */}
|
||||
|
|
@ -1751,6 +1798,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
|
||||
{/* Ação */}
|
||||
<td className="p-4 text-center">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<button
|
||||
onClick={() =>
|
||||
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>}
|
||||
{status === "ACEITO" || status === "PENDENTE" ? "Remover" : "Adicionar"}
|
||||
</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>
|
||||
</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 className="flex items-center gap-3 mb-3" onClick={() => handleViewProfessional(photographer)}>
|
||||
<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={{
|
||||
backgroundImage: `url(${photographer.avatar})`,
|
||||
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>
|
||||
<h4 className="font-bold text-gray-900">{photographer.name || photographer.nome}</h4>
|
||||
<p className="text-xs text-gray-500">ID: {photographer.id.substring(0, 8)}...</p>
|
||||
<p className="font-medium text-gray-800 text-sm">{photographer.name || photographer.nome}</p>
|
||||
<p className="text-xs text-gray-500">{assignment?.status === "PENDENTE" ? "Convite Pendente" : assignment?.status}</p>
|
||||
</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>;
|
||||
|
||||
// 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
|
||||
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)
|
||||
companyName?: string; // Nome da empresa vinculada
|
||||
allowedRegions?: string[]; // Regiões permitidas para o usuário
|
||||
professionalId?: string; // ID do profissional vinculado (se houver)
|
||||
|
||||
// Client / Event Owner specific fields
|
||||
cpf_cnpj?: string;
|
||||
|
|
@ -123,6 +124,7 @@ export interface Assignment {
|
|||
status: AssignmentStatus;
|
||||
reason?: string;
|
||||
funcaoId?: string;
|
||||
is_coordinator?: boolean;
|
||||
}
|
||||
|
||||
export interface EventData {
|
||||
|
|
|
|||
Loading…
Reference in a new issue