From 8016a0298e7efec1e0bcffed9da41eade2319fd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Vitor?= Date: Mon, 8 Dec 2025 09:11:05 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20Melhorias=20na=20tabela=20de=20eventos?= =?UTF-8?q?=20e=20formul=C3=A1rios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adiciona coluna de Cidade com ordenação independente na tabela de eventos - Implementa ordenação clicável em todas as colunas (nome, tipo, data, horário, cidade, local, status) - Remove dependência de estado para filtro de cidade - Adiciona exibição de curso/turma no modal de detalhes do evento - Torna campo curso/turma obrigatório no formulário de solicitação de evento - Remove campo de imagem de capa do formulário - Corrige importação de getActiveCoursesByInstitutionId no Dashboard --- frontend/components/EventFiltersBar.tsx | 5 +- frontend/components/EventForm.tsx | 59 +----- frontend/components/EventTable.tsx | 163 +++++++++++++-- frontend/pages/Dashboard.tsx | 30 ++- frontend/pages/Team.tsx | 262 ++++++++++++++++-------- 5 files changed, 362 insertions(+), 157 deletions(-) diff --git a/frontend/components/EventFiltersBar.tsx b/frontend/components/EventFiltersBar.tsx index 1b85106..f9ecf4e 100644 --- a/frontend/components/EventFiltersBar.tsx +++ b/frontend/components/EventFiltersBar.tsx @@ -84,7 +84,7 @@ export const EventFiltersBar: React.FC = ({ onFilterChange({ ...filters, city: e.target.value })} - disabled={!filters.state} - className="px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:border-brand-gold transition-colors bg-white disabled:bg-gray-100 disabled:cursor-not-allowed" + className="px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:border-brand-gold transition-colors bg-white" > {availableCities.map((city) => ( diff --git a/frontend/components/EventForm.tsx b/frontend/components/EventForm.tsx index 2fd3258..c27aa09 100644 --- a/frontend/components/EventForm.tsx +++ b/frontend/components/EventForm.tsx @@ -86,8 +86,6 @@ export const EventForm: React.FC = ({ briefing: "", contacts: [{ name: "", role: "", phone: "" }], files: [] as File[], - coverImage: - "https://images.unsplash.com/photo-1511795409834-ef04bbd61622?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=80", // Default institutionId: "", attendees: "", courseId: "", @@ -248,6 +246,12 @@ export const EventForm: React.FC = ({ return; } + // Validate course selection + if (!formData.courseId) { + alert("Por favor, selecione um curso/turma antes de continuar."); + return; + } + // Show toast setShowToast(true); // Call original submit after small delay for visual effect or immediately @@ -543,7 +547,7 @@ export const EventForm: React.FC = ({ {formData.institutionId && (
{availableCourses.length === 0 ? ( @@ -575,8 +579,9 @@ export const EventForm: React.FC = ({ courseId: e.target.value, }) } + required > - + {availableCourses.map((course) => (
)} - - {/* Cover Image Upload */} -
- -
- { - if (e.target.files && e.target.files[0]) { - const file = e.target.files[0]; - const imageUrl = URL.createObjectURL(file); - setFormData({ ...formData, coverImage: imageUrl }); - } - }} - /> -
- - {formData.coverImage && - !formData.coverImage.startsWith("http") - ? "Imagem selecionada" - : formData.coverImage - ? "Imagem atual (URL)" - : "Clique para selecionar..."} - -
- -
-
-
- {formData.coverImage && ( -
- Preview -
- Visualização da Capa -
-
- )} -
diff --git a/frontend/components/EventTable.tsx b/frontend/components/EventTable.tsx index 917eeca..fd3648b 100644 --- a/frontend/components/EventTable.tsx +++ b/frontend/components/EventTable.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useState, useMemo } from 'react'; import { EventData, EventStatus, UserRole } from '../types'; -import { Calendar, MapPin, Users, CheckCircle, Clock } from 'lucide-react'; +import { Calendar, MapPin, Users, CheckCircle, Clock, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react'; import { STATUS_COLORS } from '../constants'; interface EventTableProps { @@ -10,6 +10,9 @@ interface EventTableProps { userRole: UserRole; } +type SortField = 'name' | 'type' | 'date' | 'time' | 'city' | 'location' | 'status'; +type SortOrder = 'asc' | 'desc' | null; + export const EventTable: React.FC = ({ events, onEventClick, @@ -17,6 +20,84 @@ export const EventTable: React.FC = ({ userRole }) => { const canApprove = (userRole === UserRole.BUSINESS_OWNER || userRole === UserRole.SUPERADMIN); + const [sortField, setSortField] = useState(null); + const [sortOrder, setSortOrder] = useState(null); + + const handleSort = (field: SortField) => { + if (sortField === field) { + // Se já está ordenando por este campo, alterna a ordem + if (sortOrder === 'asc') { + setSortOrder('desc'); + } else if (sortOrder === 'desc') { + setSortOrder(null); + setSortField(null); + } + } else { + // Novo campo, começa com ordem ascendente + setSortField(field); + setSortOrder('asc'); + } + }; + + const sortedEvents = useMemo(() => { + if (!sortField || !sortOrder) { + return events; + } + + const sorted = [...events].sort((a, b) => { + let aValue: any; + let bValue: any; + + switch (sortField) { + case 'name': + aValue = a.name.toLowerCase(); + bValue = b.name.toLowerCase(); + break; + case 'type': + aValue = a.type.toLowerCase(); + bValue = b.type.toLowerCase(); + break; + case 'date': + aValue = new Date(a.date + 'T00:00:00').getTime(); + bValue = new Date(b.date + 'T00:00:00').getTime(); + break; + case 'time': + aValue = a.time; + bValue = b.time; + break; + case 'city': + aValue = a.address.city.toLowerCase(); + bValue = b.address.city.toLowerCase(); + break; + case 'location': + aValue = `${a.address.city}, ${a.address.state}`.toLowerCase(); + bValue = `${b.address.city}, ${b.address.state}`.toLowerCase(); + break; + case 'status': + aValue = a.status.toLowerCase(); + bValue = b.status.toLowerCase(); + break; + default: + return 0; + } + + if (aValue < bValue) return sortOrder === 'asc' ? -1 : 1; + if (aValue > bValue) return sortOrder === 'asc' ? 1 : -1; + return 0; + }); + + return sorted; + }, [events, sortField, sortOrder]); + + const getSortIcon = (field: SortField) => { + if (sortField !== field) { + return ; + } + if (sortOrder === 'asc') { + return ; + } + return ; + }; const formatDate = (date: string) => { const eventDate = new Date(date + 'T00:00:00'); @@ -50,20 +131,59 @@ export const EventTable: React.FC = ({ Ações )} - - Nome do Evento + handleSort('name')} + > +
+ Nome do Evento + {getSortIcon('name')} +
- - Tipo + handleSort('type')} + > +
+ Tipo + {getSortIcon('type')} +
- - Data + handleSort('date')} + > +
+ Data + {getSortIcon('date')} +
- - Horário + handleSort('time')} + > +
+ Horário + {getSortIcon('time')} +
- - Local + handleSort('city')} + > +
+ Cidade + {getSortIcon('city')} +
+ + handleSort('location')} + > +
+ Local + {getSortIcon('location')} +
Contatos @@ -71,13 +191,19 @@ export const EventTable: React.FC = ({ Equipe - - Status + handleSort('status')} + > +
+ Status + {getSortIcon('status')} +
- {events.map((event) => ( + {sortedEvents.map((event) => ( onEventClick(event)} @@ -115,6 +241,11 @@ export const EventTable: React.FC = ({ {event.time}
+ +
+ {event.address.city} +
+
@@ -164,7 +295,7 @@ export const EventTable: React.FC = ({
- {events.length === 0 && ( + {sortedEvents.length === 0 && (

Nenhum evento encontrado.

diff --git a/frontend/pages/Dashboard.tsx b/frontend/pages/Dashboard.tsx index 79ff528..0a22ec6 100644 --- a/frontend/pages/Dashboard.tsx +++ b/frontend/pages/Dashboard.tsx @@ -35,6 +35,7 @@ export const Dashboard: React.FC = ({ updateEventStatus, assignPhotographer, getInstitutionById, + getActiveCoursesByInstitutionId, } = useData(); const [view, setView] = useState<"list" | "create" | "edit" | "details">( initialView @@ -67,14 +68,10 @@ export const Dashboard: React.FC = ({ // Extract unique values for filters const { availableStates, availableCities, availableTypes } = useMemo(() => { const states = [...new Set(myEvents.map(e => e.address.state))].sort(); - const cities = advancedFilters.state - ? [...new Set(myEvents - .filter(e => e.address.state === advancedFilters.state) - .map(e => e.address.city))].sort() - : []; + const cities = [...new Set(myEvents.map(e => e.address.city))].sort(); const types = [...new Set(myEvents.map(e => e.type))].sort(); return { availableStates: states, availableCities: cities, availableTypes: types }; - }, [myEvents, advancedFilters.state]); + }, [myEvents]); // Helper function to check time range const isInTimeRange = (time: string, range: string): boolean => { @@ -451,6 +448,27 @@ export const Dashboard: React.FC = ({ {institution.type}

+ {/* Course Information */} + {selectedEvent.courseId && + (() => { + const course = getActiveCoursesByInstitutionId( + selectedEvent.institutionId + ).find((c) => c.id === selectedEvent.courseId); + if (course) { + return ( +
+

+ Curso/Turma +

+

+ {course.name} - {course.graduationType} ({course.year}/{course.semester}º sem) +

+
+ ); + } + return null; + })()} +

diff --git a/frontend/pages/Team.tsx b/frontend/pages/Team.tsx index 22a7bc7..e58f9f3 100644 --- a/frontend/pages/Team.tsx +++ b/frontend/pages/Team.tsx @@ -202,47 +202,49 @@ export const TeamPage: React.FC = () => { const [selectedProfessional, setSelectedProfessional] = useState(null); const [showAddModal, setShowAddModal] = useState(false); - - const [newProfessional, setNewProfessional] = useState>({ - name: "", - role: "Fotógrafo", - address: { - street: "", - number: "", - complement: "", - neighborhood: "", - city: "", - state: "", - }, - whatsapp: "", - cpfCnpj: "", - bankInfo: { - bank: "", - agency: "", - accountPix: "", - }, - hasCar: false, - hasStudio: false, - studioQuantity: 0, - cardType: "", - accountHolder: "", - observations: "", - ratings: { - technicalQuality: 0, - appearance: 0, - education: 0, - sympathy: 0, - eventPerformance: 0, - scheduleAvailability: 0, - average: 0, - }, - freeTable: "", - extraFee: "", - email: "", - specialties: [], - avatar: "", - }); - + + const [newProfessional, setNewProfessional] = useState>( + { + name: "", + role: "Fotógrafo", + address: { + street: "", + number: "", + complement: "", + neighborhood: "", + city: "", + state: "", + }, + whatsapp: "", + cpfCnpj: "", + bankInfo: { + bank: "", + agency: "", + accountPix: "", + }, + hasCar: false, + hasStudio: false, + studioQuantity: 0, + cardType: "", + accountHolder: "", + observations: "", + ratings: { + technicalQuality: 0, + appearance: 0, + education: 0, + sympathy: 0, + eventPerformance: 0, + scheduleAvailability: 0, + average: 0, + }, + freeTable: "", + extraFee: "", + email: "", + specialties: [], + avatar: "", + } + ); + const [avatarFile, setAvatarFile] = useState(null); const [avatarPreview, setAvatarPreview] = useState(""); @@ -300,16 +302,21 @@ export const TeamPage: React.FC = () => { const matchesSearch = professional.name.toLowerCase().includes(searchTerm.toLowerCase()) || professional.email.toLowerCase().includes(searchTerm.toLowerCase()); - const matchesRole = roleFilter === "all" || professional.role === roleFilter; + const matchesRole = + roleFilter === "all" || professional.role === roleFilter; const matchesStatus = statusFilter === "all" || professional.status === statusFilter; return matchesSearch && matchesRole && matchesStatus; }); const stats = { - photographers: MOCK_PROFESSIONALS.filter((p) => p.role === "Fotógrafo").length, - cinematographers: MOCK_PROFESSIONALS.filter((p) => p.role === "Cinegrafista").length, - receptionists: MOCK_PROFESSIONALS.filter((p) => p.role === "Recepcionista").length, + photographers: MOCK_PROFESSIONALS.filter((p) => p.role === "Fotógrafo") + .length, + cinematographers: MOCK_PROFESSIONALS.filter( + (p) => p.role === "Cinegrafista" + ).length, + receptionists: MOCK_PROFESSIONALS.filter((p) => p.role === "Recepcionista") + .length, total: MOCK_PROFESSIONALS.length, }; @@ -344,7 +351,9 @@ export const TeamPage: React.FC = () => {

-

Total de Cinegrafistas

+

+ Total de Cinegrafistas +

{stats.cinematographers}

@@ -355,7 +364,9 @@ export const TeamPage: React.FC = () => {
-

Total de Recepcionistas

+

+ Total de Recepcionistas +

{stats.receptionists}

@@ -606,7 +617,10 @@ export const TeamPage: React.FC = () => { required value={newProfessional.name} onChange={(e) => - setNewProfessional({ ...newProfessional, name: e.target.value }) + setNewProfessional({ + ...newProfessional, + name: e.target.value, + }) } placeholder="Nome completo" className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" @@ -645,7 +659,10 @@ export const TeamPage: React.FC = () => { required value={newProfessional.email} onChange={(e) => - setNewProfessional({ ...newProfessional, email: e.target.value }) + setNewProfessional({ + ...newProfessional, + email: e.target.value, + }) } placeholder="email@exemplo.com" className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" @@ -661,7 +678,10 @@ export const TeamPage: React.FC = () => { required value={newProfessional.whatsapp} onChange={(e) => - setNewProfessional({ ...newProfessional, whatsapp: e.target.value }) + setNewProfessional({ + ...newProfessional, + whatsapp: e.target.value, + }) } placeholder="(00) 00000-0000" className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" @@ -678,7 +698,10 @@ export const TeamPage: React.FC = () => { required value={newProfessional.cpfCnpj} onChange={(e) => - setNewProfessional({ ...newProfessional, cpfCnpj: e.target.value }) + setNewProfessional({ + ...newProfessional, + cpfCnpj: e.target.value, + }) } placeholder="000.000.000-00 ou 00.000.000/0000-00" className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" @@ -704,7 +727,10 @@ export const TeamPage: React.FC = () => { onChange={(e) => setNewProfessional({ ...newProfessional, - address: { ...newProfessional.address!, street: e.target.value }, + address: { + ...newProfessional.address!, + street: e.target.value, + }, }) } placeholder="Nome da rua" @@ -723,7 +749,10 @@ export const TeamPage: React.FC = () => { onChange={(e) => setNewProfessional({ ...newProfessional, - address: { ...newProfessional.address!, number: e.target.value }, + address: { + ...newProfessional.address!, + number: e.target.value, + }, }) } placeholder="123" @@ -743,7 +772,10 @@ export const TeamPage: React.FC = () => { onChange={(e) => setNewProfessional({ ...newProfessional, - address: { ...newProfessional.address!, complement: e.target.value }, + address: { + ...newProfessional.address!, + complement: e.target.value, + }, }) } placeholder="Apto, Sala, etc" @@ -762,7 +794,10 @@ export const TeamPage: React.FC = () => { onChange={(e) => setNewProfessional({ ...newProfessional, - address: { ...newProfessional.address!, neighborhood: e.target.value }, + address: { + ...newProfessional.address!, + neighborhood: e.target.value, + }, }) } placeholder="Nome do bairro" @@ -783,7 +818,10 @@ export const TeamPage: React.FC = () => { onChange={(e) => setNewProfessional({ ...newProfessional, - address: { ...newProfessional.address!, city: e.target.value }, + address: { + ...newProfessional.address!, + city: e.target.value, + }, }) } placeholder="Nome da cidade" @@ -803,7 +841,10 @@ export const TeamPage: React.FC = () => { onChange={(e) => setNewProfessional({ ...newProfessional, - address: { ...newProfessional.address!, state: e.target.value.toUpperCase() }, + address: { + ...newProfessional.address!, + state: e.target.value.toUpperCase(), + }, }) } placeholder="SP" @@ -831,7 +872,10 @@ export const TeamPage: React.FC = () => { onChange={(e) => setNewProfessional({ ...newProfessional, - bankInfo: { ...newProfessional.bankInfo!, bank: e.target.value }, + bankInfo: { + ...newProfessional.bankInfo!, + bank: e.target.value, + }, }) } placeholder="Nome do banco" @@ -850,7 +894,10 @@ export const TeamPage: React.FC = () => { onChange={(e) => setNewProfessional({ ...newProfessional, - bankInfo: { ...newProfessional.bankInfo!, agency: e.target.value }, + bankInfo: { + ...newProfessional.bankInfo!, + agency: e.target.value, + }, }) } placeholder="0000-0" @@ -869,7 +916,10 @@ export const TeamPage: React.FC = () => { onChange={(e) => setNewProfessional({ ...newProfessional, - bankInfo: { ...newProfessional.bankInfo!, accountPix: e.target.value }, + bankInfo: { + ...newProfessional.bankInfo!, + accountPix: e.target.value, + }, }) } placeholder="Conta ou chave Pix" @@ -888,7 +938,10 @@ export const TeamPage: React.FC = () => { required value={newProfessional.cardType} onChange={(e) => - setNewProfessional({ ...newProfessional, cardType: e.target.value }) + setNewProfessional({ + ...newProfessional, + cardType: e.target.value, + }) } placeholder="Débito, Crédito, Pix, etc" className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" @@ -904,7 +957,10 @@ export const TeamPage: React.FC = () => { required value={newProfessional.accountHolder} onChange={(e) => - setNewProfessional({ ...newProfessional, accountHolder: e.target.value }) + setNewProfessional({ + ...newProfessional, + accountHolder: e.target.value, + }) } placeholder="Nome do titular" className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" @@ -926,7 +982,10 @@ export const TeamPage: React.FC = () => { type="checkbox" checked={newProfessional.hasCar} onChange={(e) => - setNewProfessional({ ...newProfessional, hasCar: e.target.checked }) + setNewProfessional({ + ...newProfessional, + hasCar: e.target.checked, + }) } className="w-4 h-4 text-brand-gold focus:ring-brand-gold border-gray-300 rounded" /> @@ -942,7 +1001,10 @@ export const TeamPage: React.FC = () => { type="checkbox" checked={newProfessional.hasStudio} onChange={(e) => - setNewProfessional({ ...newProfessional, hasStudio: e.target.checked }) + setNewProfessional({ + ...newProfessional, + hasStudio: e.target.checked, + }) } className="w-4 h-4 text-brand-gold focus:ring-brand-gold border-gray-300 rounded" /> @@ -1096,7 +1158,10 @@ export const TeamPage: React.FC = () => { required value={newProfessional.freeTable} onChange={(e) => - setNewProfessional({ ...newProfessional, freeTable: e.target.value }) + setNewProfessional({ + ...newProfessional, + freeTable: e.target.value, + }) } placeholder="R$ 800,00" className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" @@ -1111,7 +1176,10 @@ export const TeamPage: React.FC = () => { type="text" value={newProfessional.extraFee} onChange={(e) => - setNewProfessional({ ...newProfessional, extraFee: e.target.value }) + setNewProfessional({ + ...newProfessional, + extraFee: e.target.value, + }) } placeholder="R$ 150,00" className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" @@ -1128,7 +1196,10 @@ export const TeamPage: React.FC = () => {