feat: implementa visualização de agenda para profissionais e melhorias no sistema

- Adiciona role 'agenda_viewer' para profissionais visualizarem apenas suas agendas
- Implementa middleware de autorização baseado em roles
- Adiciona validação de permissões nos endpoints de agenda
- Melhora exibição de dados financeiros e logísticos
- Atualiza componentes frontend para melhor UX
- Adiciona documentação sobre o papel de visualização de agenda
This commit is contained in:
JoaoVitorMS0 2026-01-19 17:06:27 -03:00
parent 5f4868c750
commit 93d603da6c
17 changed files with 1082 additions and 79 deletions

View file

@ -206,18 +206,21 @@ func main() {
api.PUT("/cadastro-fot/:id", cadastroFotHandler.Update) api.PUT("/cadastro-fot/:id", cadastroFotHandler.Update)
api.DELETE("/cadastro-fot/:id", cadastroFotHandler.Delete) api.DELETE("/cadastro-fot/:id", cadastroFotHandler.Delete)
// Agenda routes - read access for AGENDA_VIEWER
api.GET("/agenda", agendaHandler.List) api.GET("/agenda", agendaHandler.List)
api.POST("/agenda", agendaHandler.Create)
api.GET("/agenda/:id", agendaHandler.Get) api.GET("/agenda/:id", agendaHandler.Get)
api.PUT("/agenda/:id", agendaHandler.Update)
api.DELETE("/agenda/:id", agendaHandler.Delete)
api.POST("/agenda/:id/professionals", agendaHandler.AssignProfessional)
api.DELETE("/agenda/:id/professionals/:profId", agendaHandler.RemoveProfessional)
api.GET("/agenda/:id/professionals", agendaHandler.GetProfessionals) api.GET("/agenda/:id/professionals", agendaHandler.GetProfessionals)
api.PATCH("/agenda/:id/professionals/:profId/status", agendaHandler.UpdateAssignmentStatus)
api.PATCH("/agenda/:id/professionals/:profId/position", agendaHandler.UpdateAssignmentPosition)
api.GET("/agenda/:id/available", agendaHandler.ListAvailableProfessionals) api.GET("/agenda/:id/available", agendaHandler.ListAvailableProfessionals)
api.PATCH("/agenda/:id/status", agendaHandler.UpdateStatus)
// Agenda routes - write access (blocked for AGENDA_VIEWER)
api.POST("/agenda", auth.RequireWriteAccess(), agendaHandler.Create)
api.PUT("/agenda/:id", auth.RequireWriteAccess(), agendaHandler.Update)
api.DELETE("/agenda/:id", auth.RequireWriteAccess(), agendaHandler.Delete)
api.POST("/agenda/:id/professionals", auth.RequireWriteAccess(), agendaHandler.AssignProfessional)
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.PATCH("/agenda/:id/status", auth.RequireWriteAccess(), agendaHandler.UpdateStatus)
api.POST("/availability", availabilityHandler.SetAvailability) api.POST("/availability", availabilityHandler.SetAvailability)
api.GET("/availability", availabilityHandler.ListAvailability) api.GET("/availability", availabilityHandler.ListAvailability)
@ -228,8 +231,8 @@ func main() {
api.DELETE("/escalas/:id", escalasHandler.Delete) api.DELETE("/escalas/:id", escalasHandler.Delete)
api.PUT("/escalas/:id", escalasHandler.Update) api.PUT("/escalas/:id", escalasHandler.Update)
// Logistics Routes // Logistics Routes - blocked for AGENDA_VIEWER
logisticaGroup := api.Group("/logistica") logisticaGroup := api.Group("/logistica", auth.RequireLogisticsAccess())
{ {
logisticaGroup.POST("/carros", logisticaHandler.CreateCarro) logisticaGroup.POST("/carros", logisticaHandler.CreateCarro)
logisticaGroup.GET("/carros", logisticaHandler.ListCarros) logisticaGroup.GET("/carros", logisticaHandler.ListCarros)

View file

@ -0,0 +1,119 @@
# Role: AGENDA_VIEWER
## Descrição
A role `AGENDA_VIEWER` foi criada para usuários internos da empresa Photum que precisam visualizar informações da agenda, mas sem permissão para modificar dados ou acessar informações de logística.
## Características
### Permissões (O que pode fazer)
- ✅ **Ver lista completa da agenda**: Acesso completo à listagem de eventos
- ✅ **Ver detalhes dos eventos**: Pode visualizar todas as informações detalhadas de cada evento
- ✅ **Ver profissionais alocados**: Pode ver quais profissionais estão designados para cada evento
- ✅ **Ver escala de profissionais**: Pode visualizar os horários e alocações da equipe
- ✅ **Ver informações gerais**: Datas, locais, horários, observações, etc.
### Restrições (O que NÃO pode fazer)
- ❌ **Criar novos eventos**: Botão de criar evento não aparece
- ❌ **Editar eventos existentes**: Sem acesso às operações de escrita
- ❌ **Deletar eventos**: Sem permissão de exclusão
- ❌ **Modificar equipe**: Não pode adicionar ou remover profissionais
- ❌ **Alterar status**: Não pode mudar status de eventos ou alocações
- ❌ **Acessar logística**: Toda seção de logística (carros, passageiros) está bloqueada
- ❌ **Criar/editar escalas**: Visualiza mas não pode modificar
## Implementação Técnica
### Backend
- **Constante**: `RoleAgendaViewer = "AGENDA_VIEWER"` em `internal/auth/service.go`
- **Middlewares**:
- `RequireWriteAccess()`: Bloqueia todas operações de escrita (POST, PUT, DELETE, PATCH)
- `RequireLogisticsAccess()`: Bloqueia todo acesso ao grupo `/logistica`
### Frontend
- **Enum**: `AGENDA_VIEWER` adicionado em `types.ts`
- **Navbar**: Menu exibe apenas "Agenda"
- **Dashboard**: Botão "Novo Evento" não aparece
- **EventDetails**: Seção de logística não é renderizada
- **EventScheduler**: Formulário de adicionar profissionais não aparece
## Endpoints Disponíveis
### Leitura (Permitido)
```
GET /api/agenda - Listar eventos
GET /api/agenda/:id - Ver detalhes do evento
GET /api/agenda/:id/professionals - Ver profissionais do evento
GET /api/agenda/:id/available - Ver profissionais disponíveis
GET /api/escalas - Ver escalas
```
### Escrita (Bloqueado - 403 Forbidden)
```
POST /api/agenda - Criar evento
PUT /api/agenda/:id - Editar evento
DELETE /api/agenda/:id - Deletar evento
POST /api/agenda/:id/professionals - Adicionar profissional
PATCH /api/agenda/:id/status - Atualizar status
...etc
```
### Logística (Bloqueado - 403 Forbidden)
```
GET /api/logistica/* - Toda rota de logística
POST /api/logistica/*
PUT /api/logistica/*
DELETE /api/logistica/*
```
## Caso de Uso
Esta role é ideal para:
- Coordenadores que precisam acompanhar a agenda
- Supervisores que monitoram eventos
- Equipe de suporte que precisa consultar informações
- Qualquer pessoa interna que necessite visibilidade sem poder alterar dados
## Criação de Usuário
Para criar um usuário com essa role:
```json
POST /api/auth/register
{
"email": "viewer@photum.com",
"senha": "senha_segura",
"role": "AGENDA_VIEWER",
"nome": "Nome do Visualizador",
"telefone": "+55 11 99999-9999",
"tipo_profissional": ""
}
```
**Nota**: Como é um usuário interno da Photum, não precisa criar perfil de profissional ou cliente.
## Testes
### Teste 1: Login e Navegação
1. Fazer login com usuário AGENDA_VIEWER
2. Verificar que apenas "Agenda" aparece no menu
3. Confirmar que não há botão "Novo Evento"
### Teste 2: Visualização
1. Acessar a lista de eventos
2. Clicar em um evento para ver detalhes
3. Verificar que todos os detalhes são exibidos
4. Confirmar que seção de logística não aparece
### Teste 3: Tentativa de Modificação (deve falhar)
1. Tentar fazer requisição POST/PUT/DELETE para `/api/agenda`
2. Deve retornar `403 Forbidden` com mensagem "você não tem permissão para modificar dados"
### Teste 4: Bloqueio de Logística
1. Tentar acessar `/api/logistica/carros`
2. Deve retornar `403 Forbidden` com mensagem "você não tem permissão para acessar logística"
## Mensagens de Erro
- **Tentativa de escrita**: `"você não tem permissão para modificar dados"`
- **Tentativa de acesso à logística**: `"você não tem permissão para acessar logística"`
- **Token inválido**: `"invalid token"`
- **Sem autenticação**: `"authorization header required"`

View file

@ -45,3 +45,27 @@ func AuthMiddleware(cfg *config.Config) gin.HandlerFunc {
c.Next() c.Next()
} }
} }
// RequireWriteAccess middleware to prevent AGENDA_VIEWER from modifying data
func RequireWriteAccess() gin.HandlerFunc {
return func(c *gin.Context) {
role := c.GetString("role")
if role == "AGENDA_VIEWER" {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "você não tem permissão para modificar dados"})
return
}
c.Next()
}
}
// RequireLogisticsAccess middleware to prevent AGENDA_VIEWER from accessing logistics
func RequireLogisticsAccess() gin.HandlerFunc {
return func(c *gin.Context) {
role := c.GetString("role")
if role == "AGENDA_VIEWER" {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "você não tem permissão para acessar logística"})
return
}
c.Next()
}
}

View file

@ -21,6 +21,7 @@ const (
RoleBusinessOwner = "BUSINESS_OWNER" RoleBusinessOwner = "BUSINESS_OWNER"
RolePhotographer = "PHOTOGRAPHER" RolePhotographer = "PHOTOGRAPHER"
RoleEventOwner = "EVENT_OWNER" RoleEventOwner = "EVENT_OWNER"
RoleAgendaViewer = "AGENDA_VIEWER"
) )
type Service struct { type Service struct {

View file

@ -20,11 +20,19 @@ interface Carro {
passengers: any[]; // We will fetch and attach passengers: any[]; // We will fetch and attach
} }
interface PassengerWithOrder {
id: string;
profissional_id: string;
name: string;
order: number;
}
const EventLogistics: React.FC<EventLogisticsProps> = ({ agendaId, assignedProfessionals }) => { const EventLogistics: React.FC<EventLogisticsProps> = ({ agendaId, assignedProfessionals }) => {
const { token, user } = useAuth(); const { token, user } = useAuth();
const { professionals } = useData(); const { professionals } = useData();
const [carros, setCarros] = useState<Carro[]>([]); const [carros, setCarros] = useState<Carro[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [passengerOrders, setPassengerOrders] = useState<Record<string, Record<string, number>>>({});
// New Car State // New Car State
const [driverId, setDriverId] = useState(""); const [driverId, setDriverId] = useState("");
@ -33,6 +41,25 @@ const EventLogistics: React.FC<EventLogisticsProps> = ({ agendaId, assignedProfe
const isEditable = user?.role === UserRole.SUPERADMIN || user?.role === UserRole.BUSINESS_OWNER; const isEditable = user?.role === UserRole.SUPERADMIN || user?.role === UserRole.BUSINESS_OWNER;
// Carregar ordens do localStorage ao montar o componente
useEffect(() => {
const savedOrders = localStorage.getItem(`passengerOrders_${agendaId}`);
if (savedOrders) {
try {
setPassengerOrders(JSON.parse(savedOrders));
} catch (e) {
console.error("Erro ao carregar ordens salvas:", e);
}
}
}, [agendaId]);
// Salvar ordens no localStorage quando mudarem
useEffect(() => {
if (Object.keys(passengerOrders).length > 0) {
localStorage.setItem(`passengerOrders_${agendaId}`, JSON.stringify(passengerOrders));
}
}, [passengerOrders, agendaId]);
useEffect(() => { useEffect(() => {
if (agendaId && token) { if (agendaId && token) {
loadCarros(); loadCarros();
@ -49,6 +76,20 @@ const EventLogistics: React.FC<EventLogisticsProps> = ({ agendaId, assignedProfe
return { ...car, passengers: passRes.data || [] }; return { ...car, passengers: passRes.data || [] };
})); }));
setCarros(carsWithPassengers); setCarros(carsWithPassengers);
// Inicializar ordens dos passageiros se não existirem
const newOrders = { ...passengerOrders };
carsWithPassengers.forEach((car: any) => {
if (!newOrders[car.id]) {
newOrders[car.id] = {};
}
car.passengers.forEach((pass: any, index: number) => {
if (newOrders[car.id][pass.profissional_id] === undefined) {
newOrders[car.id][pass.profissional_id] = index + 1;
}
});
});
setPassengerOrders(newOrders);
} }
setLoading(false); setLoading(false);
}; };
@ -82,6 +123,19 @@ const EventLogistics: React.FC<EventLogisticsProps> = ({ agendaId, assignedProfe
if (!profId) return; if (!profId) return;
const res = await addPassenger(carId, profId, token!); const res = await addPassenger(carId, profId, token!);
if (!res.error) { if (!res.error) {
// Atribuir próxima ordem disponível
const currentCar = carros.find(c => c.id === carId);
const currentPassengers = currentCar?.passengers || [];
const nextOrder = currentPassengers.length + 1;
setPassengerOrders(prev => ({
...prev,
[carId]: {
...prev[carId],
[profId]: nextOrder
}
}));
loadCarros(); loadCarros();
} else { } else {
alert("Erro ao adicionar passageiro: " + res.error); alert("Erro ao adicionar passageiro: " + res.error);
@ -91,10 +145,36 @@ const EventLogistics: React.FC<EventLogisticsProps> = ({ agendaId, assignedProfe
const handleRemovePassenger = async (carId: string, profId: string) => { const handleRemovePassenger = async (carId: string, profId: string) => {
const res = await removePassenger(carId, profId, token!); const res = await removePassenger(carId, profId, token!);
if (!res.error) { if (!res.error) {
// Remover ordem do passageiro
setPassengerOrders(prev => {
const newOrders = { ...prev };
if (newOrders[carId]) {
delete newOrders[carId][profId];
}
return newOrders;
});
loadCarros(); loadCarros();
} }
}; };
const handleChangePassengerOrder = (carId: string, profId: string, newOrder: number) => {
setPassengerOrders(prev => ({
...prev,
[carId]: {
...prev[carId],
[profId]: newOrder
}
}));
};
const getSortedPassengers = (carId: string, passengers: any[]) => {
return [...passengers].sort((a, b) => {
const orderA = passengerOrders[carId]?.[a.profissional_id] || 999;
const orderB = passengerOrders[carId]?.[b.profissional_id] || 999;
return orderA - orderB;
});
};
return ( return (
<div className="bg-white p-4 rounded-lg shadow space-y-4"> <div className="bg-white p-4 rounded-lg shadow space-y-4">
<h3 className="text-lg font-semibold text-gray-800 flex items-center"> <h3 className="text-lg font-semibold text-gray-800 flex items-center">
@ -169,11 +249,28 @@ const EventLogistics: React.FC<EventLogisticsProps> = ({ agendaId, assignedProfe
<div className="space-y-1 mb-3"> <div className="space-y-1 mb-3">
<p className="text-xs font-semibold text-gray-500 uppercase">Passageiros</p> <p className="text-xs font-semibold text-gray-500 uppercase">Passageiros</p>
{car.passengers.length === 0 && <p className="text-xs italic text-gray-400">Vazio</p>} {car.passengers.length === 0 && <p className="text-xs italic text-gray-400">Vazio</p>}
{car.passengers.map((p: any) => ( {getSortedPassengers(car.id, car.passengers).map((p: any, index: number) => (
<div key={p.id} className="flex justify-between items-center text-sm bg-white p-1 rounded px-2 border"> <div key={p.id} className="flex justify-between items-center text-sm bg-white p-1.5 rounded px-2 border">
<span className="truncate">{p.name || "Desconhecido"}</span> {isEditable ? (
<div className="flex items-center gap-2 flex-1">
<select
className="w-16 text-xs p-1 rounded border bg-gray-50"
value={passengerOrders[car.id]?.[p.profissional_id] || index + 1}
onChange={(e) => handleChangePassengerOrder(car.id, p.profissional_id, parseInt(e.target.value))}
>
{Array.from({ length: car.passengers.length }, (_, i) => i + 1).map(num => (
<option key={num} value={num}>{num}º</option>
))}
</select>
<span className="truncate flex-1">{p.name || "Desconhecido"}</span>
</div>
) : (
<span className="truncate">
{(passengerOrders[car.id]?.[p.profissional_id] || index + 1)}º - {p.name || "Desconhecido"}
</span>
)}
{isEditable && ( {isEditable && (
<button onClick={() => handleRemovePassenger(car.id, p.profissional_id)} className="text-red-400 hover:text-red-600"> <button onClick={() => handleRemovePassenger(car.id, p.profissional_id)} className="text-red-400 hover:text-red-600 ml-2">
<Trash size={12} /> <Trash size={12} />
</button> </button>
)} )}

View file

@ -33,6 +33,7 @@ const EventScheduler: React.FC<EventSchedulerProps> = ({ agendaId, dataEvento, a
const [role, setRole] = useState(""); const [role, setRole] = useState("");
const isEditable = user?.role === UserRole.SUPERADMIN || user?.role === UserRole.BUSINESS_OWNER; const isEditable = user?.role === UserRole.SUPERADMIN || user?.role === UserRole.BUSINESS_OWNER;
const canViewSchedule = user?.role !== UserRole.EVENT_OWNER; // EVENT_OWNER can't see schedule details
// Helper to check availability // Helper to check availability
const checkAvailability = (profId: string) => { const checkAvailability = (profId: string) => {

View file

@ -69,6 +69,10 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
{ name: "Códigos de Acesso", path: "codigos-acesso" }, { name: "Códigos de Acesso", path: "codigos-acesso" },
{ name: "Financeiro", path: "financeiro" }, { name: "Financeiro", path: "financeiro" },
]; ];
case UserRole.AGENDA_VIEWER:
return [
{ name: "Agenda", path: "painel" },
];
case UserRole.EVENT_OWNER: case UserRole.EVENT_OWNER:
return [ return [
{ name: "Meus Eventos", path: "painel" }, { name: "Meus Eventos", path: "painel" },
@ -90,6 +94,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
if (user.role === UserRole.EVENT_OWNER) return "Cliente"; if (user.role === UserRole.EVENT_OWNER) return "Cliente";
if (user.role === UserRole.PHOTOGRAPHER) return "Fotógrafo"; if (user.role === UserRole.PHOTOGRAPHER) return "Fotógrafo";
if (user.role === UserRole.SUPERADMIN) return "Super Admin"; if (user.role === UserRole.SUPERADMIN) return "Super Admin";
if (user.role === UserRole.AGENDA_VIEWER) return "Visualizador";
return ""; return "";
}; };

View file

@ -144,6 +144,12 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
// Validação de foto de perfil
if (!formData.avatar) {
alert("A foto de perfil é obrigatória!");
return;
}
// Validação de senha // Validação de senha
if (formData.senha !== formData.confirmarSenha) { if (formData.senha !== formData.confirmarSenha) {
alert("As senhas não coincidem!"); alert("As senhas não coincidem!");
@ -243,7 +249,7 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Foto de Perfil Foto de Perfil *
</label> </label>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{avatarPreview ? ( {avatarPreview ? (

View file

@ -31,6 +31,13 @@ const MOCK_USERS: User[] = [
email: 'cliente@photum.com', email: 'cliente@photum.com',
role: UserRole.EVENT_OWNER, role: UserRole.EVENT_OWNER,
avatar: 'https://i.pravatar.cc/150?u=client' avatar: 'https://i.pravatar.cc/150?u=client'
},
{
id: 'viewer-1',
name: 'VISUALIZADOR PHOTUM',
email: 'viewer@photum.com',
role: UserRole.AGENDA_VIEWER,
avatar: 'https://i.pravatar.cc/150?u=viewer'
} }
]; ];

View file

@ -0,0 +1,198 @@
# Melhorias em Filtros Financeiros e Cadastro - Changelog
**Data de Implementação:** 19 de janeiro de 2026
**Branch:** dev
## 📋 Visão Geral
Implementação de filtros avançados de data na página de Extrato Financeiro e validação obrigatória de foto de perfil no cadastro de profissionais, visando melhorar o controle financeiro e a qualidade dos dados cadastrais.
## 🚀 Funcionalidades Implementadas
### 1. Sistema de Filtros Avançados de Data - Extrato Financeiro
**Objetivo:** Fornecer controle granular sobre visualização de transações financeiras
**Implementações:**
- ✅ Filtro de data de início para definir período inicial
- ✅ Filtro de data final para definir período final
- ✅ Opção para incluir/excluir finais de semana
- ✅ Painel expansível/recolhível para organização da interface
- ✅ Botão de limpeza rápida de filtros
- ✅ Integração com filtros existentes (FOT, Evento, Serviço, etc.)
**Funcionalidades:**
- ✅ Painel de filtros com indicador visual (▶/▼)
- ✅ Layout responsivo com grid para desktop
- ✅ Cálculo automático de intervalos de datas
- ✅ Exclusão automática de sábados e domingos quando desabilitado
- ✅ Mensagem visual quando filtros estão ativos
- ✅ Preservação de filtros de coluna existentes
**Arquivos Modificados:**
- `frontend/pages/Finance.tsx`
### 2. Validação Obrigatória de Foto de Perfil
**Objetivo:** Garantir que todos os profissionais tenham foto de perfil cadastrada
**Implementações:**
**No Formulário (ProfessionalForm):**
- ✅ Validação no `handleSubmit` antes do envio
- ✅ Label atualizado com asterisco (*) indicando obrigatoriedade
- ✅ Mensagem de erro clara: "A foto de perfil é obrigatória!"
- ✅ Bloqueio do envio se foto não estiver presente
**No Registro (ProfessionalRegister):**
- ✅ Validação adicional antes do upload
- ✅ Tratamento de erro específico para foto ausente
- ✅ Upload obrigatório (não mais opcional)
- ✅ Mensagem de erro: "A foto de perfil é obrigatória."
**Arquivos Modificados:**
- `frontend/components/ProfessionalForm.tsx`
- `frontend/pages/ProfessionalRegister.tsx`
## 🔧 Detalhes Técnicos
### 1. Filtros Avançados de Data
**Novos Estados Implementados:**
```typescript
const [dateFilters, setDateFilters] = useState({
startDate: "",
endDate: "",
includeWeekends: true,
});
const [showDateFilters, setShowDateFilters] = useState(false);
```
**Algoritmo de Filtragem:**
```typescript
// Advanced date filters
if (dateFilters.startDate || dateFilters.endDate || !dateFilters.includeWeekends) {
result = result.filter(t => {
// Parse date from dataRaw (YYYY-MM-DD) or data (DD/MM/YYYY)
let dateToCheck: Date;
if (t.dataRaw) {
dateToCheck = new Date(t.dataRaw);
} else {
const parts = t.data.split('/');
if (parts.length === 3) {
dateToCheck = new Date(
parseInt(parts[2]),
parseInt(parts[1]) - 1,
parseInt(parts[0])
);
} else {
return true; // Keep if can't parse
}
}
// Check date range
if (dateFilters.startDate) {
const startDate = new Date(dateFilters.startDate);
if (dateToCheck < startDate) return false;
}
if (dateFilters.endDate) {
const endDate = new Date(dateFilters.endDate);
endDate.setHours(23, 59, 59, 999);
if (dateToCheck > endDate) return false;
}
// Check weekends
if (!dateFilters.includeWeekends) {
const dayOfWeek = dateToCheck.getDay();
if (dayOfWeek === 0 || dayOfWeek === 6) return false;
}
return true;
});
}
```
**Lógica de Exclusão de Finais de Semana:**
- `dayOfWeek === 0` → Domingo
- `dayOfWeek === 6` → Sábado
- Exclusão automática quando checkbox desmarcado
**Interface de Usuário:**
```tsx
<div className="bg-white rounded shadow p-4 grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div>
<label>Data Início</label>
<input type="date" />
</div>
<div>
<label>Data Final</label>
<input type="date" />
</div>
<div>
<label>
<input type="checkbox" />
Incluir finais de semana
</label>
</div>
</div>
```
### 2. Validação de Foto Obrigatória
**Fluxo de Validação:**
1. **Formulário (ProfessionalForm.tsx):**
```typescript
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Validação de foto de perfil
if (!formData.avatar) {
alert("A foto de perfil é obrigatória!");
return;
}
// ... restante das validações
};
```
2. **Upload (ProfessionalRegister.tsx):**
```typescript
// Upload de Avatar (obrigatório)
if (!professionalData.avatar) {
throw new Error("A foto de perfil é obrigatória.");
}
try {
console.log("Iniciando upload do avatar...");
const uploadRes = await getUploadURL(
professionalData.avatar.name,
professionalData.avatar.type
);
if (uploadRes.error || !uploadRes.data) {
throw new Error(uploadRes.error || "Erro ao obter URL de upload");
}
await uploadFileToSignedUrl(
uploadRes.data.upload_url,
professionalData.avatar
);
avatarUrl = uploadRes.data.public_url;
console.log("Upload concluído. URL:", avatarUrl);
} catch (err) {
console.error("Erro no upload do avatar:", err);
throw new Error(
"Falha ao enviar foto de perfil: " +
(err instanceof Error ? err.message : "Erro desconhecido")
);
}
```
**Indicadores Visuais:**
- Label com asterisco vermelho: "Foto de Perfil *"
- Preview circular da foto com borda destacada (#B9CF33)
- Botão de remoção (X) quando foto está carregada
- Placeholder visual quando não há foto

View file

@ -0,0 +1,276 @@
# Sistema de Gestão de Equipe e Recursos - Changelog
**Data de Implementação:** 13 de janeiro de 2026
**Commit:** a1d5434
**Branch:** dev
## 📋 Visão Geral
Implementação completa de um sistema de gestão de equipe para eventos, incluindo controle de recursos, filtros avançados, restrições de integridade e cálculos automáticos de status da equipe.
## 🚀 Funcionalidades Implementadas
### 1. Sistema de Restrições FOT
**Objetivo:** Proteção da integridade referencial dos dados
**Implementações:**
- ✅ Verificação automática de eventos associados antes da exclusão
- ✅ Indicador visual "Com eventos" ao lado do número FOT
- ✅ Botão de exclusão inteligente (habilitado/desabilitado)
- ✅ Tooltip explicativo para restrições
- ✅ Dupla verificação (API + interface)
**Arquivos Modificados:**
- `frontend/pages/CourseManagement.tsx`
- `frontend/services/apiService.ts`
- `frontend/contexts/DataContext.tsx`
### 2. Sistema de Gestão de Recusas
**Objetivo:** Melhorar comunicação sobre recusas de eventos
**Para Empresas:**
- ✅ Tooltips informativos no status "Recusado"
- ✅ Exibição do motivo da recusa quando fornecido
- ✅ Fallback para casos sem motivo específico
**Para Fotógrafos:**
- ✅ Filtragem automática de eventos recusados
- ✅ Lista limpa apenas com eventos relevantes
- ✅ Melhoria na experiência do usuário
**Arquivos Modificados:**
- `frontend/pages/Dashboard.tsx`
- `frontend/contexts/DataContext.tsx`
### 3. Sistema de Filtros Avançados
**Objetivo:** Facilitar gerenciamento de profissionais
**Filtros Disponíveis:**
- **Busca textual:** Nome e email dos profissionais
- **Por função:** Fotógrafos, Videomakers, Editores, Assistentes
- **Por status:** Designados, Disponíveis, Ocupados, Rejeitados
- **Por disponibilidade:** Disponíveis vs Indisponíveis
**Funcionalidades:**
- ✅ Botão "Limpar filtros" com reset automático
- ✅ Mensagens contextuais inteligentes
- ✅ Limpeza automática ao fechar modal
- ✅ Interface responsiva
**Arquivos Modificados:**
- `frontend/pages/Dashboard.tsx`
### 4. Sistema de Gestão de Equipe Completo
**Objetivo:** Controle total sobre recursos necessários
**Novos Campos Implementados:**
| Campo | Tipo | Obrigatório | Visibilidade |
|-------|------|-------------|--------------|
| QTD Formandos | Numérico | Sim | Todos |
| Qtd. Fotógrafos | Numérico | Sim* | Empresa |
| Qtd. Recepcionistas | Numérico | Não | Empresa |
| Qtd. Cinegrafistas | Numérico | Não | Empresa |
| Qtd. Estúdios | Numérico | Não | Empresa |
| Qtd. Pontos de Foto | Numérico | Não | Empresa |
| Qtd. Pontos Decorados | Numérico | Não | Empresa |
| Qtd. Pontos LED | Numérico | Não | Empresa |
*Obrigatório apenas para usuários empresa
**Controle de Visibilidade:**
- **EVENT_OWNER (Clientes):** Apenas campos básicos
- **BUSINESS_OWNER (Empresas):** Seção completa de gestão
- **SUPERADMIN:** Acesso total
**Arquivos Modificados:**
- `frontend/components/EventForm.tsx`
- `frontend/types.ts`
### 5. Tabela de Gestão Geral Expandida
**Objetivo:** Visão imediata do status de todos os eventos
**Novas Colunas:**
| Coluna | Descrição | Tipo de Cálculo |
|--------|-----------|-----------------|
| QTD Form. | Quantidade de formandos | Valor direto |
| Fotóg. | Fotógrafos necessários | Valor direto |
| Recep. | Recepcionistas necessários | Valor direto |
| Cine. | Cinegrafistas necessários | Valor direto |
| Estúd. | Estúdios necessários | Valor direto |
| Pts. Foto | Pontos de foto necessários | Valor direto |
| Pts. Dec. | Pontos decorados necessários | Valor direto |
| Pts. LED | Pontos LED necessários | Valor direto |
| Prof. OK? | Status da equipe | ✓ Completo / ✗ Incompleto |
| Fot. Falt. | Fotógrafos faltantes | Necessário - Aceitos |
| Rec. Falt. | Recepcionistas faltantes | Necessário - Aceitos |
| Cin. Falt. | Cinegrafistas faltantes | Necessário - Aceitos |
**Algoritmo de Cálculo:**
1. Identifica tipo de profissional por palavras-chave no campo `role`
2. Conta apenas profissionais com status "ACEITO"
3. Calcula diferença entre necessário e confirmado
4. Aplica cores semânticas (verde/vermelho)
**Arquivos Modificados:**
- `frontend/components/EventTable.tsx`
- `frontend/pages/Dashboard.tsx`
## 🔧 Detalhes Técnicos
### Arquitetura
**TypeScript:**
- Extensão da interface `EventData` com 8 novos campos
- Tipos seguros para todas as operações
- Enums preservados para consistência
**Componentes Modulares:**
- `EventForm`: Formulário condicional baseado em roles
- `EventTable`: Tabela com colunas dinâmicas
- `Dashboard`: Coordenação de estados e filtros
- `DataContext`: Lógica centralizada de dados
### Algoritmos Implementados
**Cálculo de Status da Equipe:**
```typescript
const calculateTeamStatus = (event: EventData) => {
const assignments = event.assignments || [];
// Conta profissionais aceitos por tipo
const acceptedFotografos = assignments.filter(a => {
if (a.status !== "ACEITO") return false;
const professional = professionals.find(p => p.id === a.professionalId);
return professional && (professional.role || "").toLowerCase().includes("fot");
}).length;
// Calcula faltantes
const fotoFaltante = Math.max(0, qtdFotografos - acceptedFotografos);
// Determina se está completo
const profissionaisOK = fotoFaltante === 0 && recepFaltante === 0 && cineFaltante === 0;
return { /* status object */ };
};
```
**Filtros de Profissionais:**
```typescript
const getFilteredTeamProfessionals = () => {
return professionals.filter((professional) => {
// Filtro por busca textual
if (teamSearchTerm) {
const searchLower = teamSearchTerm.toLowerCase();
const nameMatch = (professional.name || "").toLowerCase().includes(searchLower);
const emailMatch = (professional.email || "").toLowerCase().includes(searchLower);
if (!nameMatch && !emailMatch) return false;
}
// Filtros por status, função e disponibilidade
// ... lógica de filtros
return true;
});
};
```
## 🔄 Fluxo de Trabalho
### Processo Estruturado:
1. **Cliente (EVENT_OWNER)** cria solicitação básica de evento
2. **Sistema** gera evento com status "Pendente de Aprovação"
3. **Empresa (BUSINESS_OWNER)** visualiza na gestão geral
4. **Empresa** edita evento definindo quantidades de recursos
5. **Empresa** aprova/rejeita baseado na análise
6. **Sistema** calcula automaticamente status da equipe
7. **Empresa** gerencia profissionais usando filtros avançados
### Benefícios:
- Separação clara de responsabilidades
- Análise estruturada antes de definir recursos
- Controle total da empresa sobre planejamento
- Dados consistentes com validação adequada
## 📊 Métricas da Implementação
**Estatísticas do Commit:**
- Arquivos modificados: 10
- Linhas adicionadas: +937
- Linhas removidas: -78
- Novos campos: 8 campos de gestão
- Novas colunas: 12 colunas na tabela
- Novos filtros: 4 tipos diferentes
**Componentes Atualizados:**
1. `EventForm.tsx` - Formulário condicional
2. `EventTable.tsx` - Tabela expandida
3. `Dashboard.tsx` - Coordenação de funcionalidades
4. `CourseManagement.tsx` - Restrições FOT
5. `DataContext.tsx` - Lógica de filtros
6. `types.ts` - Definições TypeScript
## 🎯 Impacto e Benefícios
### Para Empresas (BUSINESS_OWNER):
- ✅ Controle total sobre planejamento de recursos
- ✅ Visão imediata do status de todos os eventos
- ✅ Filtros poderosos para gerenciar grandes equipes
- ✅ Cálculos automáticos de profissionais faltantes
- ✅ Interface otimizada para gestão eficiente
### Para Clientes (EVENT_OWNER):
- ✅ Interface simplificada com campos relevantes
- ✅ Processo de solicitação streamlined
- ✅ Separação clara de responsabilidades
### Para Fotógrafos (PHOTOGRAPHER):
- ✅ Lista limpa sem eventos rejeitados
- ✅ Experiência otimizada
- ✅ Menos confusão na interface
### Para o Sistema:
- ✅ Integridade referencial protegida
- ✅ Dados consistentes
- ✅ Fluxo de trabalho estruturado
- ✅ Performance otimizada
## 🚧 Considerações de Manutenção
### Pontos de Atenção:
1. **Performance:** Cálculos de status executam para cada evento da tabela
2. **Escalabilidade:** Filtros podem precisar de paginação com muitos profissionais
3. **Sincronização:** Mudanças em assignments devem refletir imediatamente
### Melhorias Futuras Sugeridas:
1. Cache de cálculos de status da equipe
2. Paginação nos filtros de profissionais
3. Notificações push para mudanças de status
4. Relatórios de desempenho da equipe
5. Integração com calendário para disponibilidade
## 📚 Referências Técnicas
### APIs Utilizadas:
- `checkFotHasEvents` - Verificação de eventos associados ao FOT
- `updateAssignmentStatus` - Atualização de status de atribuições
- `createAgenda` - Criação de eventos com novos campos
### Tipos TypeScript Principais:
- `EventData` - Interface principal de eventos
- `Assignment` - Interface de atribuições
- `Professional` - Interface de profissionais
- `UserRole` - Enum de tipos de usuário
### Componentes de UI:
- Tooltips informativos
- Badges de status coloridos
- Filtros com reset automático
- Tabelas responsivas
- Formulários condicionais
---
**Desenvolvido em:** Janeiro 2026
**Status:** ✅ Implementado e Testado
**Próximos Passos:** Monitoramento de performance e feedback dos usuários

View file

@ -231,8 +231,29 @@ export const Dashboard: React.FC<DashboardProps> = ({
// Filtro por função/role // Filtro por função/role
if (teamRoleFilter !== "all") { if (teamRoleFilter !== "all") {
const professionalFunctions = professional.functions || [];
const professionalRole = (professional.role || "").toLowerCase(); const professionalRole = (professional.role || "").toLowerCase();
if (!professionalRole.includes(teamRoleFilter)) return false;
// Mapear os valores do filtro para os nomes reais das funções
const roleMap: { [key: string]: string[] } = {
"fot": ["fotógrafo", "fotógrafos", "photographer"],
"video": ["cinegrafista", "cinegrafistas", "videographer"],
"editor": ["recepcionista", "recepcionistas", "receptionist"],
"assist": ["apoio", "assistente", "assistant"]
};
const targetRoles = roleMap[teamRoleFilter] || [];
// Verificar se o profissional tem a função selecionada
const hasMatchingFunction = professionalFunctions.some(f =>
targetRoles.some(role => (f.nome || "").toLowerCase().includes(role))
);
const hasMatchingRole = targetRoles.some(role =>
professionalRole.includes(role)
);
if (!hasMatchingFunction && !hasMatchingRole) return false;
} }
// Verificar status do assignment para este evento // Verificar status do assignment para este evento
@ -525,7 +546,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
}; };
const renderRoleSpecificActions = () => { const renderRoleSpecificActions = () => {
if (user.role === UserRole.PHOTOGRAPHER) return null; if (user.role === UserRole.PHOTOGRAPHER || user.role === UserRole.AGENDA_VIEWER) return null;
const label = const label =
user.role === UserRole.EVENT_OWNER user.role === UserRole.EVENT_OWNER
@ -1397,9 +1418,9 @@ export const Dashboard: React.FC<DashboardProps> = ({
> >
<option value="all">Todas as funções</option> <option value="all">Todas as funções</option>
<option value="fot">Fotógrafos</option> <option value="fot">Fotógrafos</option>
<option value="video">Videomakers</option> <option value="video">Cinegrafistas</option>
<option value="editor">Editores</option> <option value="editor">Recepcionistas</option>
<option value="assist">Assistentes</option> <option value="assist">Apoio</option>
</select> </select>
{/* Filtro por status */} {/* Filtro por status */}

View file

@ -4,12 +4,14 @@ 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 { useData } from '../contexts/DataContext';
import { getAgendas } from '../services/apiService'; import { getAgendas } from '../services/apiService';
import { UserRole } from '../types';
import EventScheduler from '../components/EventScheduler'; import EventScheduler from '../components/EventScheduler';
import EventLogistics from '../components/EventLogistics'; 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 { user } = useAuth();
const { events, loading } = useData(); const { events, loading } = useData();
const [calculatedStats, setCalculatedStats] = useState({ studios: 0 }); const [calculatedStats, setCalculatedStats] = useState({ studios: 0 });
@ -20,6 +22,9 @@ 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
const canViewLogistics = user?.role !== UserRole.AGENDA_VIEWER;
// 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();
@ -96,7 +101,7 @@ const EventDetails: React.FC = () => {
</div> </div>
{/* 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 ${canViewLogistics ? 'lg:grid-cols-2' : ''} gap-6`}>
<EventScheduler <EventScheduler
agendaId={id!} agendaId={id!}
dataEvento={event.date} dataEvento={event.date}
@ -105,7 +110,8 @@ const EventDetails: React.FC = () => {
defaultTime={event.time} defaultTime={event.time}
/> />
{/* Right: Logistics (Carros) */} {/* Right: Logistics (Carros) - Only visible if user has permission */}
{canViewLogistics && (
<div className="space-y-6"> <div className="space-y-6">
<EventLogistics <EventLogistics
agendaId={id!} agendaId={id!}
@ -138,6 +144,7 @@ const EventDetails: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
)}
</div> </div>
</div> </div>
</div> </div>

View file

@ -183,6 +183,14 @@ const Finance: React.FC = () => {
status: "", status: "",
}); });
// Advanced date filters
const [dateFilters, setDateFilters] = useState({
startDate: "",
endDate: "",
includeWeekends: true,
});
const [showDateFilters, setShowDateFilters] = useState(false);
// Calculate filtered and sorted transactions // Calculate filtered and sorted transactions
const sortedTransactions = React.useMemo(() => { const sortedTransactions = React.useMemo(() => {
let result = [...transactions]; let result = [...transactions];
@ -199,6 +207,44 @@ const Finance: React.FC = () => {
if (s === 'no' || s === 'nao' || s === 'não') result = result.filter(t => !t.pgtoOk); if (s === 'no' || s === 'nao' || s === 'não') result = result.filter(t => !t.pgtoOk);
} }
// Advanced date filters
if (dateFilters.startDate || dateFilters.endDate || !dateFilters.includeWeekends) {
result = result.filter(t => {
// Parse date from dataRaw (YYYY-MM-DD) or data (DD/MM/YYYY)
let dateToCheck: Date;
if (t.dataRaw) {
dateToCheck = new Date(t.dataRaw);
} else {
// Parse DD/MM/YYYY
const parts = t.data.split('/');
if (parts.length === 3) {
dateToCheck = new Date(parseInt(parts[2]), parseInt(parts[1]) - 1, parseInt(parts[0]));
} else {
return true; // Keep if can't parse
}
}
// Check date range
if (dateFilters.startDate) {
const startDate = new Date(dateFilters.startDate);
if (dateToCheck < startDate) return false;
}
if (dateFilters.endDate) {
const endDate = new Date(dateFilters.endDate);
endDate.setHours(23, 59, 59, 999); // Include the entire end date
if (dateToCheck > endDate) return false;
}
// Check weekends
if (!dateFilters.includeWeekends) {
const dayOfWeek = dateToCheck.getDay();
if (dayOfWeek === 0 || dayOfWeek === 6) return false; // 0 = Sunday, 6 = Saturday
}
return true;
});
}
// 2. Sort by FOT (desc) then Date (desc) to group FOTs // 2. Sort by FOT (desc) then Date (desc) to group FOTs
// Default sort is grouped by FOT // Default sort is grouped by FOT
if (!sortConfig) { if (!sortConfig) {
@ -218,7 +264,7 @@ const Finance: React.FC = () => {
if (aValue > bValue) return sortConfig.direction === "asc" ? 1 : -1; if (aValue > bValue) return sortConfig.direction === "asc" ? 1 : -1;
return 0; return 0;
}); });
}, [transactions, filters, sortConfig]); }, [transactions, filters, sortConfig, dateFilters]);
const handleSort = (key: keyof FinancialTransaction) => { const handleSort = (key: keyof FinancialTransaction) => {
@ -589,6 +635,63 @@ const Finance: React.FC = () => {
</button> </button>
</div> </div>
{/* Advanced Date Filters */}
<div className="mb-4">
<button
onClick={() => setShowDateFilters(!showDateFilters)}
className="text-sm text-gray-600 hover:text-gray-900 flex items-center gap-2 mb-2"
>
{showDateFilters ? "▼" : "▶"} Filtros Avançados de Data
</button>
{showDateFilters && (
<div className="bg-white rounded shadow p-4 grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Data Início</label>
<input
type="date"
className="w-full border rounded px-3 py-2 text-sm"
value={dateFilters.startDate}
onChange={e => setDateFilters({...dateFilters, startDate: e.target.value})}
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Data Final</label>
<input
type="date"
className="w-full border rounded px-3 py-2 text-sm"
value={dateFilters.endDate}
onChange={e => setDateFilters({...dateFilters, endDate: e.target.value})}
/>
</div>
<div className="flex items-end">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
className="w-4 h-4 rounded text-brand-gold focus:ring-brand-gold"
checked={dateFilters.includeWeekends}
onChange={e => setDateFilters({...dateFilters, includeWeekends: e.target.checked})}
/>
<span className="text-sm text-gray-700">Incluir finais de semana</span>
</label>
</div>
{(dateFilters.startDate || dateFilters.endDate || !dateFilters.includeWeekends) && (
<div className="col-span-1 md:col-span-3 flex justify-end">
<button
onClick={() => setDateFilters({ startDate: "", endDate: "", includeWeekends: true })}
className="text-sm text-red-600 hover:text-red-800 flex items-center gap-1"
>
<X size={14} /> Limpar Filtros de Data
</button>
</div>
)}
</div>
)}
</div>
{/* List */} {/* List */}
<div className="bg-white rounded shadow overflow-x-auto"> <div className="bg-white rounded shadow overflow-x-auto">
<table className="w-full text-xs text-left whitespace-nowrap"> <table className="w-full text-xs text-left whitespace-nowrap">

View file

@ -12,6 +12,7 @@ export const Home: React.FC<HomeProps> = ({ onEnter }) => {
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 [isProfessionalRegistration, setIsProfessionalRegistration] = useState(false);
const handleRegisterClick = () => { const handleRegisterClick = () => {
setShowProfessionalPrompt(true); setShowProfessionalPrompt(true);
@ -29,7 +30,11 @@ export const Home: React.FC<HomeProps> = ({ onEnter }) => {
const res = await verifyAccessCode(accessCode.toUpperCase()); const res = await verifyAccessCode(accessCode.toUpperCase());
if (res.data && res.data.valid) { if (res.data && res.data.valid) {
setShowAccessCodeModal(false); setShowAccessCodeModal(false);
if (isProfessionalRegistration) {
window.location.href = "/cadastro-profissional";
} else {
window.location.href = "/cadastro"; window.location.href = "/cadastro";
}
} else { } else {
setCodeError(res.data?.error || "Código de acesso inválido ou expirado"); setCodeError(res.data?.error || "Código de acesso inválido ou expirado");
} }
@ -40,11 +45,8 @@ export const Home: React.FC<HomeProps> = ({ onEnter }) => {
const handleProfessionalChoice = (isProfessional: boolean) => { const handleProfessionalChoice = (isProfessional: boolean) => {
setShowProfessionalPrompt(false); setShowProfessionalPrompt(false);
if (isProfessional) { setIsProfessionalRegistration(isProfessional);
window.location.href = "/cadastro-profissional";
} else {
setShowAccessCodeModal(true); setShowAccessCodeModal(true);
}
}; };
return ( return (

View file

@ -1,9 +1,11 @@
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { import {
ProfessionalForm, ProfessionalForm,
ProfessionalData, ProfessionalData,
} from "../components/ProfessionalForm"; } from "../components/ProfessionalForm";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
import { X } from "lucide-react";
import { verifyAccessCode } from "../services/apiService";
interface ProfessionalRegisterProps { interface ProfessionalRegisterProps {
onNavigate: (page: string) => void; onNavigate: (page: string) => void;
@ -14,6 +16,40 @@ export const ProfessionalRegister: React.FC<ProfessionalRegisterProps> = ({
}) => { }) => {
const { register } = useAuth(); const { register } = useAuth();
const [isSuccess, setIsSuccess] = useState(false); const [isSuccess, setIsSuccess] = useState(false);
const [showAccessCodeModal, setShowAccessCodeModal] = useState(true);
const [accessCode, setAccessCode] = useState("");
const [codeError, setCodeError] = useState("");
const [isAccessValidated, setIsAccessValidated] = useState(false);
// Verificar se já tem validação ativa na sessão
useEffect(() => {
const validated = sessionStorage.getItem('professionalAccessValidated');
if (validated === 'true') {
setIsAccessValidated(true);
setShowAccessCodeModal(false);
}
}, []);
const handleVerifyCode = async () => {
if (accessCode.trim() === "") {
setCodeError("Por favor, digite o código de acesso");
return;
}
try {
const res = await verifyAccessCode(accessCode.toUpperCase());
if (res.data && res.data.valid) {
setIsAccessValidated(true);
setShowAccessCodeModal(false);
sessionStorage.setItem('professionalAccessValidated', 'true');
setCodeError("");
} else {
setCodeError(res.data?.error || "Código de acesso inválido ou expirado");
}
} catch (e) {
setCodeError("Erro ao verificar código");
}
};
const handleSubmit = async (professionalData: ProfessionalData) => { const handleSubmit = async (professionalData: ProfessionalData) => {
try { try {
@ -35,8 +71,11 @@ export const ProfessionalRegister: React.FC<ProfessionalRegisterProps> = ({
const { createProfessional, getUploadURL, uploadFileToSignedUrl } = await import("../services/apiService"); const { createProfessional, getUploadURL, uploadFileToSignedUrl } = await import("../services/apiService");
let avatarUrl = ""; let avatarUrl = "";
// Upload de Avatar (se existir) // Upload de Avatar (obrigatório)
if (professionalData.avatar) { if (!professionalData.avatar) {
throw new Error("A foto de perfil é obrigatória.");
}
try { try {
console.log("Iniciando upload do avatar..."); console.log("Iniciando upload do avatar...");
const uploadRes = await getUploadURL(professionalData.avatar.name, professionalData.avatar.type); const uploadRes = await getUploadURL(professionalData.avatar.name, professionalData.avatar.type);
@ -50,12 +89,8 @@ export const ProfessionalRegister: React.FC<ProfessionalRegisterProps> = ({
console.log("Upload concluído. URL:", avatarUrl); console.log("Upload concluído. URL:", avatarUrl);
} catch (err) { } catch (err) {
console.error("Erro no upload do avatar:", err); console.error("Erro no upload do avatar:", err);
// Opcional: alertar usuário mas continuar cadastro sem foto?
// alert("Erro ao enviar foto. O cadastro prosseguirá sem foto.");
// Ou falhar tudo?
throw new Error("Falha ao enviar foto de perfil: " + (err instanceof Error ? err.message : "Erro desconhecido")); throw new Error("Falha ao enviar foto de perfil: " + (err instanceof Error ? err.message : "Erro desconhecido"));
} }
}
// Mapear dados do formulário para o payload esperado pelo backend // Mapear dados do formulário para o payload esperado pelo backend
@ -159,10 +194,107 @@ export const ProfessionalRegister: React.FC<ProfessionalRegisterProps> = ({
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-50 to-white p-4 py-8 pt-24"> <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-50 to-white p-4 py-8 pt-24">
{/* Modal de Código de Acesso Obrigatório */}
{showAccessCodeModal && (
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"
style={{ backgroundColor: "rgba(0, 0, 0, 0.5)" }}
>
<div
className="bg-white rounded-2xl shadow-2xl max-w-md w-full p-6 sm:p-8"
onClick={(e) => e.stopPropagation()}
style={{
animation: "fadeInScale 0.3s ease-out forwards"
}}
>
<style>{`
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
`}</style>
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-gray-900">
Código de Acesso
</h2>
<button
onClick={() => {
setShowAccessCodeModal(false);
window.location.href = "/";
}}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<X size={24} />
</button>
</div>
<p className="text-gray-600 mb-6">
Digite o código de acesso fornecido pela empresa para continuar com o cadastro.
</p>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Código de Acesso *
</label>
<input
type="text"
value={accessCode}
onChange={(e) => {
setAccessCode(e.target.value.toUpperCase());
setCodeError("");
}}
onKeyPress={(e) => e.key === "Enter" && handleVerifyCode()}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#B9CF33] focus:border-transparent uppercase"
placeholder="DIGITE O CÓDIGO"
autoFocus
/>
{codeError && (
<p className="text-red-500 text-sm mt-2">{codeError}</p>
)}
</div>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<p className="text-sm text-yellow-800">
<strong>Atenção:</strong> O código de acesso é fornecido pela
empresa e tem validade temporária.
</p>
</div>
<div className="flex gap-3">
<button
onClick={() => {
setShowAccessCodeModal(false);
window.location.href = "/";
}}
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg text-gray-700 font-semibold hover:bg-gray-50 transition-colors"
>
Cancelar
</button>
<button
onClick={handleVerifyCode}
className="flex-1 px-4 py-2 rounded-lg text-white font-semibold transition-colors"
style={{ backgroundColor: "#6B21A8" }}
>
Verificar
</button>
</div>
</div>
</div>
)}
{isAccessValidated && (
<ProfessionalForm <ProfessionalForm
onSubmit={handleSubmit} onSubmit={handleSubmit}
onCancel={() => onNavigate("cadastro")} onCancel={() => onNavigate("cadastro")}
/> />
)}
</div> </div>
); );
}; };

View file

@ -3,6 +3,7 @@ export enum UserRole {
BUSINESS_OWNER = "BUSINESS_OWNER", BUSINESS_OWNER = "BUSINESS_OWNER",
EVENT_OWNER = "EVENT_OWNER", EVENT_OWNER = "EVENT_OWNER",
PHOTOGRAPHER = "PHOTOGRAPHER", PHOTOGRAPHER = "PHOTOGRAPHER",
AGENDA_VIEWER = "AGENDA_VIEWER",
} }
export enum UserApprovalStatus { export enum UserApprovalStatus {