From 93d603da6cb60c3a1c6ca2f525b454f647952c76 Mon Sep 17 00:00:00 2001 From: JoaoVitorMS0 Date: Mon, 19 Jan 2026 17:06:27 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20implementa=20visualiza=C3=A7=C3=A3o=20d?= =?UTF-8?q?e=20agenda=20para=20profissionais=20e=20melhorias=20no=20sistem?= =?UTF-8?q?a?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- backend/cmd/api/main.go | 23 +- backend/docs/AGENDA_VIEWER_ROLE.md | 119 ++++++++++ backend/internal/auth/middleware.go | 24 ++ backend/internal/auth/service.go | 1 + frontend/components/EventLogistics.tsx | 105 ++++++++- frontend/components/EventScheduler.tsx | 1 + frontend/components/Navbar.tsx | 5 + frontend/components/ProfessionalForm.tsx | 8 +- frontend/contexts/AuthContext.tsx | 7 + frontend/docs/CHANGELOG_19JAN2026.md | 198 ++++++++++++++++ frontend/docs/CHANGELOG_GESTAO_EQUIPE.md | 276 +++++++++++++++++++++++ frontend/pages/Dashboard.tsx | 31 ++- frontend/pages/EventDetails.tsx | 67 +++--- frontend/pages/Finance.tsx | 105 ++++++++- frontend/pages/Home.tsx | 14 +- frontend/pages/ProfessionalRegister.tsx | 176 +++++++++++++-- frontend/types.ts | 1 + 17 files changed, 1082 insertions(+), 79 deletions(-) create mode 100644 backend/docs/AGENDA_VIEWER_ROLE.md create mode 100644 frontend/docs/CHANGELOG_19JAN2026.md create mode 100644 frontend/docs/CHANGELOG_GESTAO_EQUIPE.md diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index 35eb47a..0c7d8b6 100644 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -206,18 +206,21 @@ func main() { api.PUT("/cadastro-fot/:id", cadastroFotHandler.Update) api.DELETE("/cadastro-fot/:id", cadastroFotHandler.Delete) + // Agenda routes - read access for AGENDA_VIEWER api.GET("/agenda", agendaHandler.List) - api.POST("/agenda", agendaHandler.Create) 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.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.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.GET("/availability", availabilityHandler.ListAvailability) @@ -228,8 +231,8 @@ func main() { api.DELETE("/escalas/:id", escalasHandler.Delete) api.PUT("/escalas/:id", escalasHandler.Update) - // Logistics Routes - logisticaGroup := api.Group("/logistica") + // Logistics Routes - blocked for AGENDA_VIEWER + logisticaGroup := api.Group("/logistica", auth.RequireLogisticsAccess()) { logisticaGroup.POST("/carros", logisticaHandler.CreateCarro) logisticaGroup.GET("/carros", logisticaHandler.ListCarros) diff --git a/backend/docs/AGENDA_VIEWER_ROLE.md b/backend/docs/AGENDA_VIEWER_ROLE.md new file mode 100644 index 0000000..4920ee5 --- /dev/null +++ b/backend/docs/AGENDA_VIEWER_ROLE.md @@ -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"` diff --git a/backend/internal/auth/middleware.go b/backend/internal/auth/middleware.go index 433650e..30531e9 100644 --- a/backend/internal/auth/middleware.go +++ b/backend/internal/auth/middleware.go @@ -45,3 +45,27 @@ func AuthMiddleware(cfg *config.Config) gin.HandlerFunc { 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() + } +} diff --git a/backend/internal/auth/service.go b/backend/internal/auth/service.go index 9b248e5..6c3b0e8 100644 --- a/backend/internal/auth/service.go +++ b/backend/internal/auth/service.go @@ -21,6 +21,7 @@ const ( RoleBusinessOwner = "BUSINESS_OWNER" RolePhotographer = "PHOTOGRAPHER" RoleEventOwner = "EVENT_OWNER" + RoleAgendaViewer = "AGENDA_VIEWER" ) type Service struct { diff --git a/frontend/components/EventLogistics.tsx b/frontend/components/EventLogistics.tsx index ddc2b0a..7dc3a87 100644 --- a/frontend/components/EventLogistics.tsx +++ b/frontend/components/EventLogistics.tsx @@ -20,11 +20,19 @@ interface Carro { passengers: any[]; // We will fetch and attach } +interface PassengerWithOrder { + id: string; + profissional_id: string; + name: string; + order: number; +} + const EventLogistics: React.FC = ({ agendaId, assignedProfessionals }) => { const { token, user } = useAuth(); const { professionals } = useData(); const [carros, setCarros] = useState([]); const [loading, setLoading] = useState(false); + const [passengerOrders, setPassengerOrders] = useState>>({}); // New Car State const [driverId, setDriverId] = useState(""); @@ -33,6 +41,25 @@ const EventLogistics: React.FC = ({ agendaId, assignedProfe 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(() => { if (agendaId && token) { loadCarros(); @@ -49,6 +76,20 @@ const EventLogistics: React.FC = ({ agendaId, assignedProfe return { ...car, passengers: passRes.data || [] }; })); 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); }; @@ -82,6 +123,19 @@ const EventLogistics: React.FC = ({ agendaId, assignedProfe if (!profId) return; const res = await addPassenger(carId, profId, token!); 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(); } else { alert("Erro ao adicionar passageiro: " + res.error); @@ -91,10 +145,36 @@ const EventLogistics: React.FC = ({ agendaId, assignedProfe const handleRemovePassenger = async (carId: string, profId: string) => { const res = await removePassenger(carId, profId, token!); if (!res.error) { + // Remover ordem do passageiro + setPassengerOrders(prev => { + const newOrders = { ...prev }; + if (newOrders[carId]) { + delete newOrders[carId][profId]; + } + return newOrders; + }); 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 (

@@ -169,11 +249,28 @@ const EventLogistics: React.FC = ({ agendaId, assignedProfe

Passageiros

{car.passengers.length === 0 &&

Vazio

} - {car.passengers.map((p: any) => ( -
- {p.name || "Desconhecido"} + {getSortedPassengers(car.id, car.passengers).map((p: any, index: number) => ( +
+ {isEditable ? ( +
+ + {p.name || "Desconhecido"} +
+ ) : ( + + {(passengerOrders[car.id]?.[p.profissional_id] || index + 1)}º - {p.name || "Desconhecido"} + + )} {isEditable && ( - )} diff --git a/frontend/components/EventScheduler.tsx b/frontend/components/EventScheduler.tsx index 179cec0..10a3fb9 100644 --- a/frontend/components/EventScheduler.tsx +++ b/frontend/components/EventScheduler.tsx @@ -33,6 +33,7 @@ const EventScheduler: React.FC = ({ agendaId, dataEvento, a const [role, setRole] = useState(""); 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 const checkAvailability = (profId: string) => { diff --git a/frontend/components/Navbar.tsx b/frontend/components/Navbar.tsx index b27c43f..d71b694 100644 --- a/frontend/components/Navbar.tsx +++ b/frontend/components/Navbar.tsx @@ -69,6 +69,10 @@ export const Navbar: React.FC = ({ onNavigate, currentPage }) => { { name: "Códigos de Acesso", path: "codigos-acesso" }, { name: "Financeiro", path: "financeiro" }, ]; + case UserRole.AGENDA_VIEWER: + return [ + { name: "Agenda", path: "painel" }, + ]; case UserRole.EVENT_OWNER: return [ { name: "Meus Eventos", path: "painel" }, @@ -90,6 +94,7 @@ export const Navbar: React.FC = ({ onNavigate, currentPage }) => { if (user.role === UserRole.EVENT_OWNER) return "Cliente"; if (user.role === UserRole.PHOTOGRAPHER) return "Fotógrafo"; if (user.role === UserRole.SUPERADMIN) return "Super Admin"; + if (user.role === UserRole.AGENDA_VIEWER) return "Visualizador"; return ""; }; diff --git a/frontend/components/ProfessionalForm.tsx b/frontend/components/ProfessionalForm.tsx index 99802a3..0c89358 100644 --- a/frontend/components/ProfessionalForm.tsx +++ b/frontend/components/ProfessionalForm.tsx @@ -144,6 +144,12 @@ export const ProfessionalForm: React.FC = ({ const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); + // Validação de foto de perfil + if (!formData.avatar) { + alert("A foto de perfil é obrigatória!"); + return; + } + // Validação de senha if (formData.senha !== formData.confirmarSenha) { alert("As senhas não coincidem!"); @@ -243,7 +249,7 @@ export const ProfessionalForm: React.FC = ({
{avatarPreview ? ( diff --git a/frontend/contexts/AuthContext.tsx b/frontend/contexts/AuthContext.tsx index 6312989..2d95d52 100644 --- a/frontend/contexts/AuthContext.tsx +++ b/frontend/contexts/AuthContext.tsx @@ -31,6 +31,13 @@ const MOCK_USERS: User[] = [ email: 'cliente@photum.com', role: UserRole.EVENT_OWNER, 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' } ]; diff --git a/frontend/docs/CHANGELOG_19JAN2026.md b/frontend/docs/CHANGELOG_19JAN2026.md new file mode 100644 index 0000000..be97a5f --- /dev/null +++ b/frontend/docs/CHANGELOG_19JAN2026.md @@ -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 +
+
+ + +
+ +
+ + +
+ +
+ +
+
+``` + +### 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 diff --git a/frontend/docs/CHANGELOG_GESTAO_EQUIPE.md b/frontend/docs/CHANGELOG_GESTAO_EQUIPE.md new file mode 100644 index 0000000..58408e9 --- /dev/null +++ b/frontend/docs/CHANGELOG_GESTAO_EQUIPE.md @@ -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 \ No newline at end of file diff --git a/frontend/pages/Dashboard.tsx b/frontend/pages/Dashboard.tsx index d08b90b..81b5388 100644 --- a/frontend/pages/Dashboard.tsx +++ b/frontend/pages/Dashboard.tsx @@ -231,8 +231,29 @@ export const Dashboard: React.FC = ({ // Filtro por função/role if (teamRoleFilter !== "all") { + const professionalFunctions = professional.functions || []; 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 @@ -525,7 +546,7 @@ export const Dashboard: React.FC = ({ }; const renderRoleSpecificActions = () => { - if (user.role === UserRole.PHOTOGRAPHER) return null; + if (user.role === UserRole.PHOTOGRAPHER || user.role === UserRole.AGENDA_VIEWER) return null; const label = user.role === UserRole.EVENT_OWNER @@ -1397,9 +1418,9 @@ export const Dashboard: React.FC = ({ > - - - + + + {/* Filtro por status */} diff --git a/frontend/pages/EventDetails.tsx b/frontend/pages/EventDetails.tsx index 0c84c14..6219399 100644 --- a/frontend/pages/EventDetails.tsx +++ b/frontend/pages/EventDetails.tsx @@ -4,12 +4,14 @@ import { ArrowLeft, MapPin, Calendar, Clock, DollarSign } from 'lucide-react'; import { useAuth } from '../contexts/AuthContext'; import { useData } from '../contexts/DataContext'; import { getAgendas } from '../services/apiService'; +import { UserRole } from '../types'; import EventScheduler from '../components/EventScheduler'; import EventLogistics from '../components/EventLogistics'; const EventDetails: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); + const { user } = useAuth(); const { events, loading } = useData(); const [calculatedStats, setCalculatedStats] = useState({ studios: 0 }); @@ -20,6 +22,9 @@ const EventDetails: React.FC = () => { if (!event) return
Evento não encontrado.
; + // Check if user can view logistics + const canViewLogistics = user?.role !== UserRole.AGENDA_VIEWER; + // Use event.date which is already YYYY-MM-DD from DataContext const formattedDate = new Date(event.date + "T00:00:00").toLocaleDateString(); @@ -96,7 +101,7 @@ const EventDetails: React.FC = () => {
{/* Main Content: Scheduling & Logistics */} -
+
{ defaultTime={event.time} /> - {/* Right: Logistics (Carros) */} -
- a.professionalId)} - /> + {/* Right: Logistics (Carros) - Only visible if user has permission */} + {canViewLogistics && ( +
+ a.professionalId)} + /> - {/* Equipment / Studios Section (Placeholder for now based on spreadsheet) */} -
-

- {/* Using DollarSign as generic icon for assets/inventory for now, map to Camera later */} - Equipamentos & Estúdios -

-
-
- Qtd. Estúdios (Automático): - {calculatedStats.studios} -
-
- Ponto de Foto: - {event.qtd_ponto_foto || 0} -
-
-

Notas de Equipamento:

-