- Adiciona filtro de role RESEARCHER na tela de Aprovação.
- Implementa edição de Role na tela de Aprovação com suporte a funções virtuais (Cine/Recep). - Atualiza apiService com updateUserRole. - Corrige visibilidade do Dashboard para RESEARCHER (DataContext). - Backend: ListPending retorna tipo_profissional original.
This commit is contained in:
parent
b497ea8c72
commit
d471b4fc0d
16 changed files with 216 additions and 37 deletions
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
|
|
@ -35,6 +36,12 @@ func (h *Handler) Create(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Security: Block RESEARCHER
|
||||
if c.GetString("role") == "RESEARCHER" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Acesso negado: Somente leitura"})
|
||||
return
|
||||
}
|
||||
|
||||
userIDStr := c.GetString("userID")
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
|
|
@ -128,6 +135,12 @@ func (h *Handler) Update(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Security: Block RESEARCHER
|
||||
if c.GetString("role") == "RESEARCHER" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Acesso negado: Somente leitura"})
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Update Payload: %+v\n", req)
|
||||
|
||||
agenda, err := h.service.Update(c.Request.Context(), id, req)
|
||||
|
|
@ -188,6 +201,17 @@ func (h *Handler) AssignProfessional(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if idParam == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "ID inválido"})
|
||||
return
|
||||
}
|
||||
|
||||
// Security: Block RESEARCHER
|
||||
if c.GetString("role") == "RESEARCHER" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Acesso negado: Somente leitura"})
|
||||
return
|
||||
}
|
||||
|
||||
profID, err := uuid.Parse(req.ProfessionalID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "ID de profissional inválido"})
|
||||
|
|
@ -228,6 +252,12 @@ func (h *Handler) RemoveProfessional(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Security: Block RESEARCHER
|
||||
if c.GetString("role") == "RESEARCHER" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Acesso negado: Somente leitura"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.service.RemoveProfessional(c.Request.Context(), agendaID, profID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erro ao remover profissional: " + err.Error()})
|
||||
return
|
||||
|
|
@ -240,6 +270,7 @@ func (h *Handler) RemoveProfessional(c *gin.Context) {
|
|||
// @Summary Get professionals assigned to agenda
|
||||
// @Tags agenda
|
||||
// @Router /api/agenda/{id}/professionals [get]
|
||||
|
||||
func (h *Handler) GetProfessionals(c *gin.Context) {
|
||||
idParam := c.Param("id")
|
||||
agendaID, err := uuid.Parse(idParam)
|
||||
|
|
@ -254,6 +285,34 @@ func (h *Handler) GetProfessionals(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Security: Mask data for RESEARCHER
|
||||
if c.GetString("role") == "RESEARCHER" {
|
||||
for i := range profs {
|
||||
// Set sensitive fields to generic or empty values
|
||||
// Using generated struct pointers or values?
|
||||
// generated.GetAgendaProfessionalsRow has fields like Email (pgtype.Text)
|
||||
// We can override them.
|
||||
// Note: 'profs' is a slice of structs, we iterate by index to modify.
|
||||
|
||||
// Mask Email
|
||||
profs[i].Email = pgtype.Text{String: "bloqueado@acesso.restrito", Valid: true}
|
||||
|
||||
// Mask Phone/Whatsapp if available in struct
|
||||
// Checking struct definition... list includes p.*
|
||||
// p (CadastroProfissionais) has Whatsapp, CpfCnpjTitular, Banco, Agencia, ContaPix
|
||||
|
||||
profs[i].Whatsapp = pgtype.Text{String: "Bloqueado", Valid: true}
|
||||
|
||||
profs[i].CpfCnpjTitular = pgtype.Text{String: "***", Valid: true}
|
||||
|
||||
profs[i].Banco = pgtype.Text{String: "***", Valid: true}
|
||||
|
||||
profs[i].Agencia = pgtype.Text{String: "***", Valid: true}
|
||||
|
||||
profs[i].ContaPix = pgtype.Text{String: "***", Valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, profs)
|
||||
}
|
||||
|
||||
|
|
@ -444,6 +503,12 @@ func (h *Handler) NotifyLogistics(c *gin.Context) {
|
|||
}
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
|
||||
// Security: Block RESEARCHER
|
||||
if c.GetString("role") == "RESEARCHER" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Acesso negado: Somente leitura"})
|
||||
return
|
||||
}
|
||||
|
||||
go h.service.NotifyLogistics(context.Background(), agendaID, req.PassengerOrders)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Notificação de logística iniciada com sucesso."})
|
||||
|
|
|
|||
|
|
@ -419,14 +419,15 @@ func (h *Handler) ListPending(c *gin.Context) {
|
|||
}
|
||||
|
||||
resp[i] = map[string]interface{}{
|
||||
"id": uuid.UUID(u.ID.Bytes).String(),
|
||||
"email": u.Email,
|
||||
"role": u.Role,
|
||||
"ativo": u.Ativo,
|
||||
"created_at": u.CriadoEm.Time,
|
||||
"name": nome, // Mapped to name for frontend compatibility
|
||||
"phone": whatsapp,
|
||||
"company_name": empresaNome,
|
||||
"id": uuid.UUID(u.ID.Bytes).String(),
|
||||
"email": u.Email,
|
||||
"role": u.Role,
|
||||
"ativo": u.Ativo,
|
||||
"created_at": u.CriadoEm.Time,
|
||||
"name": nome, // Mapped to name for frontend compatibility
|
||||
"phone": whatsapp,
|
||||
"company_name": empresaNome,
|
||||
"professional_type": u.TipoProfissional.String, // Add this
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const (
|
|||
RolePhotographer = "PHOTOGRAPHER"
|
||||
RoleEventOwner = "EVENT_OWNER"
|
||||
RoleAgendaViewer = "AGENDA_VIEWER"
|
||||
RoleResearcher = "RESEARCHER"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
|
|
@ -269,6 +270,7 @@ func (s *Service) EnsureDemoUsers(ctx context.Context) error {
|
|||
{"empresa@photum.com", RoleBusinessOwner, "PHOTUM CEO"},
|
||||
{"foto@photum.com", RolePhotographer, "COLABORADOR PHOTUM"},
|
||||
{"cliente@photum.com", RoleEventOwner, "CLIENTE TESTE"},
|
||||
{"pesquisa@photum.com", RoleResearcher, "PESQUISADOR"},
|
||||
}
|
||||
|
||||
for _, u := range demoUsers {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ INSERT INTO funcoes_profissionais (nome) VALUES
|
|||
('Cinegrafista'),
|
||||
('Recepcionista'),
|
||||
('Fixo Photum'),
|
||||
('Controle')
|
||||
('Controle'),
|
||||
('Pesquisa')
|
||||
ON CONFLICT (nome) DO NOTHING;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cadastro_profissionais (
|
||||
|
|
|
|||
|
|
@ -31,6 +31,12 @@ func (h *Handler) Create(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Security: Block RESEARCHER
|
||||
if c.GetString("role") == "RESEARCHER" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Acesso negado: Somente leitura"})
|
||||
return
|
||||
}
|
||||
|
||||
escala, err := h.service.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
|
|
@ -76,7 +82,11 @@ func (h *Handler) ListByAgenda(c *gin.Context) {
|
|||
}
|
||||
var phone string
|
||||
if e.Whatsapp.Valid {
|
||||
phone = e.Whatsapp.String
|
||||
if c.GetString("role") == "RESEARCHER" {
|
||||
phone = "Bloqueado"
|
||||
} else {
|
||||
phone = e.Whatsapp.String
|
||||
}
|
||||
}
|
||||
var profFuncao string
|
||||
if e.FuncaoNome.Valid {
|
||||
|
|
@ -116,6 +126,12 @@ func (h *Handler) Delete(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Security: Block RESEARCHER
|
||||
if c.GetString("role") == "RESEARCHER" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Acesso negado: Somente leitura"})
|
||||
return
|
||||
}
|
||||
|
||||
err := h.service.Delete(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
|
|
@ -148,6 +164,12 @@ func (h *Handler) Update(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Security: Block RESEARCHER
|
||||
if c.GetString("role") == "RESEARCHER" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Acesso negado: Somente leitura"})
|
||||
return
|
||||
}
|
||||
|
||||
_, err := h.service.Update(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
|
|
|
|||
|
|
@ -82,6 +82,10 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
|||
return [
|
||||
{ name: "Agenda", path: "painel" },
|
||||
];
|
||||
case UserRole.RESEARCHER:
|
||||
return [
|
||||
{ name: "Painel", path: "painel" },
|
||||
];
|
||||
case UserRole.EVENT_OWNER:
|
||||
return [
|
||||
{ name: "Meus Eventos", path: "painel" },
|
||||
|
|
@ -104,6 +108,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
|||
if (user.role === UserRole.PHOTOGRAPHER) return "Fotógrafo";
|
||||
if (user.role === UserRole.SUPERADMIN) return "Super Admin";
|
||||
if (user.role === UserRole.AGENDA_VIEWER) return "Visualizador";
|
||||
if (user.role === UserRole.RESEARCHER) return "Pesquisa";
|
||||
return "";
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
import React, { useState } from "react";
|
||||
import { SimpleCrud } from "./SimpleCrud";
|
||||
import { PriceTableEditor } from "./PriceTableEditor";
|
||||
import { Building2, GraduationCap, Calendar, DollarSign, Database } from "lucide-react";
|
||||
import { Building2, GraduationCap, Calendar, DollarSign, Database, Briefcase } from "lucide-react";
|
||||
|
||||
export const SystemSettings: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<"empresas" | "cursos" | "tipos_evento" | "anos_formatura" | "precos">("empresas");
|
||||
const [activeTab, setActiveTab] = useState<"empresas" | "cursos" | "tipos_evento" | "anos_formatura" | "precos" | "funcoes">("empresas");
|
||||
|
||||
const tabs = [
|
||||
{ id: "empresas", label: "Empresas", icon: Building2 },
|
||||
{ id: "cursos", label: "Cursos", icon: GraduationCap },
|
||||
{ id: "tipos_evento", label: "Tipos de Evento", icon: Calendar },
|
||||
{ id: "anos_formatura", label: "Anos de Formatura", icon: Database },
|
||||
{ id: "funcoes", label: "Funções", icon: Briefcase },
|
||||
{ id: "precos", label: "Tabela de Preços", icon: DollarSign },
|
||||
];
|
||||
|
||||
|
|
@ -68,6 +69,13 @@ export const SystemSettings: React.FC = () => {
|
|||
columns={[{ key: "ano_semestre", label: "Ano/Semestre (Ex: 2024.1)" }]}
|
||||
/>
|
||||
)}
|
||||
{activeTab === "funcoes" && (
|
||||
<SimpleCrud
|
||||
title="Gerenciar Funções Profissionais"
|
||||
endpoint="/api/funcoes"
|
||||
columns={[{ key: "nome", label: "Nome da Função" }]}
|
||||
/>
|
||||
)}
|
||||
{activeTab === "precos" && (
|
||||
<PriceTableEditor />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -966,7 +966,7 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
|||
};
|
||||
|
||||
const getEventsByRole = (userId: string, role: string) => {
|
||||
if (role === "SUPERADMIN" || role === "BUSINESS_OWNER") {
|
||||
if (role === "SUPERADMIN" || role === "BUSINESS_OWNER" || role === "RESEARCHER") {
|
||||
return events;
|
||||
}
|
||||
if (role === "EVENT_OWNER") {
|
||||
|
|
|
|||
|
|
@ -532,7 +532,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
};
|
||||
|
||||
const renderRoleSpecificActions = () => {
|
||||
if (user.role === UserRole.PHOTOGRAPHER || user.role === UserRole.AGENDA_VIEWER) return null;
|
||||
if (user.role === UserRole.PHOTOGRAPHER || user.role === UserRole.AGENDA_VIEWER || user.role === UserRole.RESEARCHER) return null;
|
||||
|
||||
const label =
|
||||
user.role === UserRole.EVENT_OWNER
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ 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;
|
||||
const canViewLogistics = user?.role !== UserRole.AGENDA_VIEWER && user?.role !== UserRole.RESEARCHER;
|
||||
|
||||
// Use event.date which is already YYYY-MM-DD from DataContext
|
||||
const formattedDate = new Date(event.date + "T00:00:00").toLocaleDateString();
|
||||
|
|
|
|||
|
|
@ -117,6 +117,8 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
|
|||
return "Fotógrafo";
|
||||
case UserRole.EVENT_OWNER:
|
||||
return "Cliente";
|
||||
case UserRole.RESEARCHER:
|
||||
return "Pesquisador";
|
||||
default:
|
||||
return role;
|
||||
}
|
||||
|
|
@ -267,6 +269,7 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
|
|||
{ id: "2", name: "PHOTUM CEO", email: "empresa@photum.com", role: UserRole.BUSINESS_OWNER },
|
||||
{ id: "3", name: "COLABORADOR PHOTUM", email: "foto@photum.com", role: UserRole.PHOTOGRAPHER },
|
||||
{ id: "4", name: "CLIENTE TESTE", email: "cliente@photum.com", role: UserRole.EVENT_OWNER },
|
||||
{ id: "5", name: "PESQUISADOR", email: "pesquisa@photum.com", role: UserRole.RESEARCHER },
|
||||
].map((user) => (
|
||||
<button
|
||||
key={user.id}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export const ProfessionalRegister: React.FC<ProfessionalRegisterProps> = ({
|
|||
email: professionalData.email,
|
||||
senha: professionalData.senha,
|
||||
telefone: professionalData.whatsapp,
|
||||
role: "PHOTOGRAPHER", // Role fixa para profissionais
|
||||
role: professionalData.funcaoLabel?.toUpperCase().includes("PESQUISA") ? "RESEARCHER" : "PHOTOGRAPHER",
|
||||
tipo_profissional: professionalData.funcaoLabel || "", // Envia o nome da função (ex: Cinegrafista)
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -348,7 +348,7 @@ export const TeamPage: React.FC = () => {
|
|||
email: formData.email,
|
||||
senha: formData.senha,
|
||||
nome: formData.nome,
|
||||
role: "PHOTOGRAPHER", // Default role for professionals created here? Or map from selected role?
|
||||
role: roles.find(r => r.id === formData.funcao_profissional_id)?.nome.toUpperCase().includes("PESQUISA") ? "RESEARCHER" : "PHOTOGRAPHER",
|
||||
// Mapear função? Usually PHOTOGRAPHER or generic. Let's assume PHOTOGRAPHER for now as they are "Equipe".
|
||||
tipo_profissional: roles.find(r => r.id === formData.funcao_profissional_id)?.nome || "",
|
||||
ativo: true, // Auto-active as per request
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ import {
|
|||
getPendingUsers,
|
||||
approveUser as apiApproveUser,
|
||||
rejectUser as apiRejectUser,
|
||||
updateUserRole,
|
||||
} from "../services/apiService";
|
||||
import { UserApprovalStatus } from "../types";
|
||||
import { UserApprovalStatus, UserRole } from "../types";
|
||||
import {
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
|
|
@ -14,6 +15,7 @@ import {
|
|||
Filter,
|
||||
Users,
|
||||
Briefcase,
|
||||
Edit2,
|
||||
} from "lucide-react";
|
||||
import { Button } from "../components/Button";
|
||||
|
||||
|
|
@ -43,15 +45,11 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
|||
try {
|
||||
const result = await getPendingUsers(token);
|
||||
if (result.data) {
|
||||
// Mapear dados do backend para o formato esperado pelo componente, se necessário
|
||||
// Supondo que o backend retorna estrutura compatível ou fazemos o map aqui
|
||||
const mappedUsers = result.data.map((u: any) => ({
|
||||
...u,
|
||||
approvalStatus: u.ativo
|
||||
? UserApprovalStatus.APPROVED
|
||||
: UserApprovalStatus.PENDING, // Simplificação, backend deve retornar status real se houver rejected
|
||||
// Se o backend não retornar status explícito, assumimos pendente se !ativo
|
||||
// Mas idealmente o backend retornaria um status enum
|
||||
: UserApprovalStatus.PENDING,
|
||||
}));
|
||||
setUsers(mappedUsers);
|
||||
}
|
||||
|
|
@ -71,7 +69,6 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
|||
setIsProcessing(userId);
|
||||
try {
|
||||
await apiApproveUser(userId, token);
|
||||
// Atualizar lista após aprovação
|
||||
await fetchUsers();
|
||||
} catch (error) {
|
||||
console.error("Erro ao aprovar usuário:", error);
|
||||
|
|
@ -81,15 +78,31 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleRoleChange = async (userId: string, newRole: string) => {
|
||||
if (!token) return;
|
||||
try {
|
||||
// Optimistic update
|
||||
setUsers(prev => prev.map(u => u.id === userId ? {...u, role: newRole} : u));
|
||||
|
||||
await updateUserRole(userId, newRole, token);
|
||||
// Refresh to be sure
|
||||
// await fetchUsers(); // Optional if we trust optimistic
|
||||
} catch (error) {
|
||||
console.error("Erro ao atualizar role:", error);
|
||||
alert("Erro ao atualizar função do usuário");
|
||||
// Revert? simpler to just fetch
|
||||
fetchUsers();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Separar usuários Clientes (EVENT_OWNER) e Profissionais (PHOTOGRAPHER)
|
||||
// Backend roles: PHOTOGRAPHER, EVENT_OWNER, BUSINESS_OWNER, SUPERADMIN
|
||||
// Backend roles: PHOTOGRAPHER, EVENT_OWNER, BUSINESS_OWNER, SUPERADMIN, RESEARCHER
|
||||
const clientUsers = users.filter(
|
||||
(user) => user.role === "EVENT_OWNER"
|
||||
);
|
||||
const professionalUsers = users.filter(
|
||||
(user) => user.role === "PHOTOGRAPHER"
|
||||
(user) => user.role === "PHOTOGRAPHER" || user.role === "RESEARCHER" || user.role === "BUSINESS_OWNER" // Include BUSINESS_OWNER if relevant for professional list? Usually Business owner is client-side but maybe here it's treated differently. based on login.tsx business owner is "Dono do Negócio". Let's stick to PHOTOGRAPHER + RESEARCHER as per request, but user explicitly mentioned "admin ter liberdade de editar role".
|
||||
);
|
||||
|
||||
// Filtrar usuários baseado na aba ativa
|
||||
|
|
@ -99,20 +112,10 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
|||
const matchesSearch =
|
||||
(user.name || "").toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(user.email || "").toLowerCase().includes(searchTerm.toLowerCase());
|
||||
// Remover filtro por registeredInstitution se não vier do backend ainda
|
||||
|
||||
// Por enquanto, como o backend retorna apenas pendentes na rota /pending (conforme nome da rota),
|
||||
// o statusFilter pode ser redundante se a rota só traz pendentes.
|
||||
// Mas se o backend trouxer todos, o filtro funciona.
|
||||
// Se a rota for /users/pending, assumimos que todos são pendentes.
|
||||
// VAMOS ASSUMIR QUE O BACKEND SÓ RETORNA PENDENTES POR ENQUANTO.
|
||||
// Mas para manter a UI, vamos considerar todos como Pendentes se status não vier.
|
||||
|
||||
return matchesSearch;
|
||||
});
|
||||
|
||||
const getStatusBadge = (status: UserApprovalStatus) => {
|
||||
// Se status undefined, assume pendente para visualização
|
||||
const s = status || UserApprovalStatus.PENDING;
|
||||
switch (s) {
|
||||
case UserApprovalStatus.PENDING:
|
||||
|
|
@ -237,6 +240,10 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
|||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Telefone
|
||||
</th>
|
||||
{/* Role Column */}
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Função
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Data de Cadastro
|
||||
|
|
@ -253,7 +260,7 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
|||
{filteredUsers.length === 0 ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={activeTab === "cliente" ? 7 : 6}
|
||||
colSpan={activeTab === "cliente" ? 8 : 7}
|
||||
className="px-6 py-12 text-center text-gray-500"
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
|
|
@ -298,6 +305,36 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
|||
{user.phone || "-"}
|
||||
</div>
|
||||
</td>
|
||||
{/* Role Editor */}
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<select
|
||||
value={
|
||||
user.role === "PHOTOGRAPHER" && user.professional_type === "Cinegrafista" ? "Cinegrafista" :
|
||||
user.role === "PHOTOGRAPHER" && user.professional_type === "Recepcionista" ? "Recepcionista" :
|
||||
user.role === "PHOTOGRAPHER" && user.professional_type === "Fotógrafo" ? "Fotógrafo" :
|
||||
user.role === "PHOTOGRAPHER" ? "Fotógrafo" : // Default to Fotógrafo if generic Photographer role
|
||||
user.role
|
||||
}
|
||||
onChange={(e) => {
|
||||
let newRole = e.target.value;
|
||||
if (["Cinegrafista", "Recepcionista", "Fotógrafo"].includes(newRole)) {
|
||||
newRole = "PHOTOGRAPHER";
|
||||
// Note: We are currently only updating the System Role.
|
||||
// The 'professional_type' field is not updated by updateUserRole endpoint.
|
||||
// This UI allows the user to confirm the System Role is correct for these types.
|
||||
}
|
||||
handleRoleChange(user.id, newRole);
|
||||
}}
|
||||
className="text-sm border-gray-300 rounded-md shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50"
|
||||
>
|
||||
<option value="Fotógrafo">Fotógrafo</option>
|
||||
<option value="Cinegrafista">Cinegrafista</option>
|
||||
<option value="Recepcionista">Recepcionista</option>
|
||||
<option value="RESEARCHER">Pesquisador</option>
|
||||
<option value="EVENT_OWNER">Cliente</option>
|
||||
<option value="BUSINESS_OWNER">Empresa</option>
|
||||
</select>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-600">
|
||||
{user.created_at
|
||||
|
|
|
|||
|
|
@ -715,6 +715,40 @@ export async function rejectUser(userId: string, token: string): Promise<ApiResp
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualiza a role de um usuário
|
||||
*/
|
||||
export async function updateUserRole(userId: string, role: string, token: string): Promise<ApiResponse<any>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/admin/users/${userId}/role`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ role })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
data,
|
||||
error: null,
|
||||
isBackendDown: false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error updating user role:", error);
|
||||
return {
|
||||
data: null,
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
isBackendDown: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Atribui um profissional a um evento
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export enum UserRole {
|
|||
EVENT_OWNER = "EVENT_OWNER",
|
||||
PHOTOGRAPHER = "PHOTOGRAPHER",
|
||||
AGENDA_VIEWER = "AGENDA_VIEWER",
|
||||
RESEARCHER = "RESEARCHER",
|
||||
}
|
||||
|
||||
export enum UserApprovalStatus {
|
||||
|
|
|
|||
Loading…
Reference in a new issue