diff --git a/frontend/.env.example b/frontend/.env.example index 0229e78..6502e07 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -8,5 +8,11 @@ VITE_MAPBOX_TOKEN=YOUR_MAPBOX_TOKEN_HERE -# Nota: A chave já está configurada diretamente no código para demonstração +# Configuração da URL do Backend +# Configure a URL base do seu backend API +# Default: http://localhost:3000/api + +VITE_API_URL=http://localhost:3000/api + +# Nota: A chave do Mapbox já está configurada diretamente no código para demonstração # Em produção, use variáveis de ambiente como acima diff --git a/frontend/App.tsx b/frontend/App.tsx index c7c7401..fdcca9d 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -10,6 +10,7 @@ import { FinancePage } from "./pages/Finance"; import { SettingsPage } from "./pages/Settings"; import { CourseManagement } from "./pages/CourseManagement"; import { InspirationPage } from "./pages/Inspiration"; +import { UserApproval } from "./pages/UserApproval"; import { PrivacyPolicy } from "./pages/PrivacyPolicy"; import { TermsOfUse } from "./pages/TermsOfUse"; import { LGPD } from "./pages/LGPD"; @@ -429,6 +430,16 @@ const AppContent: React.FC = () => { } /> + + + + + + } + /> {/* Rota padrão - redireciona para home */} } /> diff --git a/frontend/components/CourseForm.tsx b/frontend/components/CourseForm.tsx index 85b9f27..fade4e5 100644 --- a/frontend/components/CourseForm.tsx +++ b/frontend/components/CourseForm.tsx @@ -1,8 +1,9 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { Course, Institution } from "../types"; import { Input, Select } from "./Input"; import { Button } from "./Button"; -import { GraduationCap, X, Check, AlertCircle } from "lucide-react"; +import { GraduationCap, X, Check, AlertCircle, AlertTriangle } from "lucide-react"; +import { getInstitutions, getGraduationYears } from "../services/apiService"; interface CourseFormProps { onCancel: () => void; @@ -44,6 +45,38 @@ export const CourseForm: React.FC = ({ const [showToast, setShowToast] = useState(false); const [error, setError] = useState(""); + const [backendInstitutions, setBackendInstitutions] = useState>([]); + const [graduationYears, setGraduationYears] = useState([]); + const [isBackendDown, setIsBackendDown] = useState(false); + const [isLoadingData, setIsLoadingData] = useState(true); + + // Buscar dados do backend + useEffect(() => { + const fetchBackendData = async () => { + setIsLoadingData(true); + + const [institutionsResponse, yearsResponse] = await Promise.all([ + getInstitutions(), + getGraduationYears() + ]); + + if (institutionsResponse.isBackendDown || yearsResponse.isBackendDown) { + setIsBackendDown(true); + } else { + setIsBackendDown(false); + if (institutionsResponse.data) { + setBackendInstitutions(institutionsResponse.data); + } + if (yearsResponse.data) { + setGraduationYears(yearsResponse.data); + } + } + + setIsLoadingData(false); + }; + + fetchBackendData(); + }, []); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -140,25 +173,63 @@ export const CourseForm: React.FC = ({ required /> - handleChange("name", e.target.value)} - required - /> +
+ + + {isBackendDown && ( +
+ + Backend não está rodando. Não é possível carregar os cursos. +
+ )} + {isLoadingData && !isBackendDown && ( +
+ Carregando cursos... +
+ )} +
- handleChange("year", parseInt(e.target.value))} - min={currentYear - 1} - max={currentYear + 5} - required - /> +
+ + + {isBackendDown && ( +
+ + Backend offline +
+ )} +
({ - value: t, - label: t, - }))} - value={formData.type} - onChange={(e) => - setFormData({ ...formData, type: e.target.value }) - } - /> +
+ + + {isBackendDown && ( +
+ + Backend não está rodando. Não é possível carregar os tipos de eventos. +
+ )} + {isLoadingEventTypes && !isBackendDown && ( +
+ Carregando tipos de eventos... +
+ )} +
= ({ onNavigate, currentPage }) => { { name: "Gestão de Eventos", path: "painel" }, { name: "Equipe", path: "equipe" }, { name: "Gestão de Cursos", path: "cursos" }, + { name: "Aprovação de Cadastros", path: "aprovacao-cadastros" }, { name: "Financeiro", path: "financeiro" }, ]; case UserRole.EVENT_OWNER: diff --git a/frontend/contexts/DataContext.tsx b/frontend/contexts/DataContext.tsx index f1b4e00..48b418e 100644 --- a/frontend/contexts/DataContext.tsx +++ b/frontend/contexts/DataContext.tsx @@ -5,6 +5,9 @@ import { EventType, Institution, Course, + User, + UserApprovalStatus, + UserRole, } from "../types"; // Initial Mock Data @@ -582,6 +585,7 @@ interface DataContextType { events: EventData[]; institutions: Institution[]; courses: Course[]; + pendingUsers: User[]; addEvent: (event: EventData) => void; updateEventStatus: (id: string, status: EventStatus) => void; assignPhotographer: (eventId: string, photographerId: string) => void; @@ -595,6 +599,9 @@ interface DataContextType { getCoursesByInstitutionId: (institutionId: string) => Course[]; getActiveCoursesByInstitutionId: (institutionId: string) => Course[]; getCourseById: (id: string) => Course | undefined; + registerPendingUser: (userData: { id: string; name: string; email: string; phone: string; registeredInstitution?: string }) => void; + approveUser: (userId: string) => void; + rejectUser: (userId: string) => void; } const DataContext = createContext(undefined); @@ -606,6 +613,7 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({ const [institutions, setInstitutions] = useState(INITIAL_INSTITUTIONS); const [courses, setCourses] = useState(INITIAL_COURSES); + const [pendingUsers, setPendingUsers] = useState([]); const addEvent = (event: EventData) => { setEvents((prev) => [event, ...prev]); @@ -686,12 +694,54 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({ return courses.find((course) => course.id === id); }; + // Funções para gerenciar usuários pendentes + const registerPendingUser = (userData: { + id: string; + name: string; + email: string; + phone: string; + registeredInstitution?: string + }) => { + const newUser: User = { + id: userData.id, + name: userData.name, + email: userData.email, + phone: userData.phone, + role: UserRole.EVENT_OWNER, + approvalStatus: UserApprovalStatus.PENDING, + registeredInstitution: userData.registeredInstitution, + createdAt: new Date().toISOString(), + }; + setPendingUsers((prev) => [...prev, newUser]); + }; + + const approveUser = (userId: string) => { + setPendingUsers((prev) => + prev.map((user) => + user.id === userId + ? { ...user, approvalStatus: UserApprovalStatus.APPROVED } + : user + ) + ); + }; + + const rejectUser = (userId: string) => { + setPendingUsers((prev) => + prev.map((user) => + user.id === userId + ? { ...user, approvalStatus: UserApprovalStatus.REJECTED } + : user + ) + ); + }; + return ( = ({ getCoursesByInstitutionId, getActiveCoursesByInstitutionId, getCourseById, + registerPendingUser, + approveUser, + rejectUser, }} > {children} diff --git a/frontend/pages/Login.tsx b/frontend/pages/Login.tsx index c72834d..6fc9f7b 100644 --- a/frontend/pages/Login.tsx +++ b/frontend/pages/Login.tsx @@ -42,18 +42,18 @@ export const Login: React.FC = ({ onNavigate }) => { } return ( -
-
- {/* Logo */} -
- Photum Formaturas -
+
+
+
+ {/* Logo dentro do card */} +
+ Photum Formaturas +
-
Bem-vindo de volta

Acesse sua conta

diff --git a/frontend/pages/Register.tsx b/frontend/pages/Register.tsx index 98a5597..4549098 100644 --- a/frontend/pages/Register.tsx +++ b/frontend/pages/Register.tsx @@ -10,7 +10,7 @@ interface RegisterProps { } export const Register: React.FC = ({ onNavigate }) => { - const { addInstitution } = useData(); + const { addInstitution, registerPendingUser } = useData(); const [formData, setFormData] = useState({ name: '', email: '', @@ -22,9 +22,10 @@ export const Register: React.FC = ({ onNavigate }) => { const [agreedToTerms, setAgreedToTerms] = useState(false); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); - const [success, setSuccess] = useState(false); + const [isPending, setIsPending] = useState(false); const [showInstitutionForm, setShowInstitutionForm] = useState(false); const [tempUserId] = useState(`user-${Date.now()}`); + const [registeredInstitutionName, setRegisteredInstitutionName] = useState(); const handleChange = (field: string, value: string | boolean) => { setFormData(prev => ({ ...prev, [field]: value })); @@ -63,13 +64,17 @@ export const Register: React.FC = ({ onNavigate }) => { return; } - // Simular registro (conta será criada como Cliente/EVENT_OWNER automaticamente) + // Simular registro - usuário ficará pendente de aprovação setTimeout(() => { + registerPendingUser({ + id: tempUserId, + name: formData.name, + email: formData.email, + phone: formData.phone, + registeredInstitution: undefined + }); setIsLoading(false); - setSuccess(true); - setTimeout(() => { - onNavigate('entrar'); - }, 2000); + setIsPending(true); }, 1500); }; @@ -80,16 +85,21 @@ export const Register: React.FC = ({ onNavigate }) => { ownerId: tempUserId }; addInstitution(newInstitution); + setRegisteredInstitutionName(newInstitution.name); setShowInstitutionForm(false); - // Complete registration + // Complete registration - usuário ficará pendente de aprovação setIsLoading(true); setTimeout(() => { + registerPendingUser({ + id: tempUserId, + name: formData.name, + email: formData.email, + phone: formData.phone, + registeredInstitution: newInstitution.name + }); setIsLoading(false); - setSuccess(true); - setTimeout(() => { - onNavigate('entrar'); - }, 2000); + setIsPending(true); }, 1500); }; @@ -111,39 +121,66 @@ export const Register: React.FC = ({ onNavigate }) => { ); } - if (success) { + if (isPending) { return (
-
-
+
+
- +
-

Cadastro realizado com sucesso!

-

Redirecionando para o login...

+

Cadastro Pendente de Aprovação

+

+ Seu cadastro foi realizado com sucesso e está aguardando aprovação da empresa. +

+

+ Você receberá um e-mail assim que seu cadastro for aprovado e poderá acessar o sistema. +

+
+

+ Atenção: Enquanto seu cadastro não for aprovado, você não terá acesso ao sistema. +

+
+
); } return ( -
-
- {/* Logo */} -
- Photum Formaturas -
+
+
+
+ {/* Logo dentro do card */} +
+ Photum Formaturas +
-
- Comece agora -

Crie sua conta

-

+ Comece agora +

Crie sua conta

+

Já tem uma conta?{' '}

-
-
+ +
{ const [selectedProfessional, setSelectedProfessional] = useState(null); const [showAddModal, setShowAddModal] = useState(false); + const [showEditModal, setShowEditModal] = useState(false); + const [editingProfessional, setEditingProfessional] = useState(null); + const [professionalRoles, setProfessionalRoles] = useState([]); + const [isBackendDown, setIsBackendDown] = useState(false); + const [isLoadingRoles, setIsLoadingRoles] = useState(true); const [newProfessional, setNewProfessional] = useState>( { @@ -248,6 +255,26 @@ export const TeamPage: React.FC = () => { const [avatarFile, setAvatarFile] = useState(null); const [avatarPreview, setAvatarPreview] = useState(""); + // Buscar funções profissionais do backend + useEffect(() => { + const fetchRoles = async () => { + setIsLoadingRoles(true); + const response = await getProfessionalRoles(); + + if (response.isBackendDown) { + setIsBackendDown(true); + setProfessionalRoles([]); + } else if (response.data) { + setIsBackendDown(false); + setProfessionalRoles(response.data); + } + + setIsLoadingRoles(false); + }; + + fetchRoles(); + }, []); + const handleAvatarChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { @@ -265,6 +292,64 @@ export const TeamPage: React.FC = () => { setAvatarPreview(""); }; + const handleEditProfessional = (professional: Professional) => { + setEditingProfessional(professional); + setNewProfessional(professional); + setAvatarPreview(professional.avatar); + setSelectedProfessional(null); + setShowEditModal(true); + }; + + const handleUpdateProfessional = () => { + // Aqui você implementaria a lógica de atualização + // Por exemplo, chamada à API para atualizar os dados + console.log('Atualizando profissional:', newProfessional); + + // Simulando atualização + setShowEditModal(false); + setEditingProfessional(null); + setNewProfessional({ + 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: "", + }); + removeAvatar(); + }; + const getStatusColor = (status: Professional["status"]) => { switch (status) { case "active": @@ -571,6 +656,109 @@ export const TeamPage: React.FC = () => {
+ {/* Edit Professional Modal */} + {showEditModal && ( +
setShowEditModal(false)} + > +
e.stopPropagation()} + > +
+
+

+ Editar Profissional +

+

+ Atualize as informações do profissional +

+
+ +
+ + { + e.preventDefault(); + handleUpdateProfessional(); + alert("Profissional atualizado com sucesso!"); + }} + > + {/* Reutilizar o mesmo conteúdo do formulário de adicionar */} + {/* Avatar Upload */} +
+ +
+ {avatarPreview ? ( +
+ Preview + +
+ ) : ( +
+ +
+ )} + +
+
+ + {/* Botões de Ação */} +
+ + +
+ +
+
+ )} + {/* Add Professional Modal - com todos os campos da planilha */} {showAddModal && (
{ role: e.target.value as ProfessionalRole, }) } - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" + disabled={isLoadingRoles || isBackendDown} + className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold disabled:bg-gray-100 disabled:cursor-not-allowed" > - - - + + {professionalRoles.map((role) => ( + + ))} + {isBackendDown && ( +
+ + Backend não está rodando. Não é possível carregar as funções profissionais. +
+ )} + {isLoadingRoles && !isBackendDown && ( +
+ Carregando funções profissionais... +
+ )}
@@ -1035,6 +1238,24 @@ export const TeamPage: React.FC = () => {
)}
+ +
+ + + setNewProfessional({ + ...newProfessional, + cardType: e.target.value, + }) + } + placeholder="Ex: SD, CF, XQD, etc." + className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" + /> +
{/* Avaliações */} @@ -1429,7 +1650,10 @@ export const TeamPage: React.FC = () => { > Fechar -
diff --git a/frontend/pages/UserApproval.tsx b/frontend/pages/UserApproval.tsx new file mode 100644 index 0000000..2cedaed --- /dev/null +++ b/frontend/pages/UserApproval.tsx @@ -0,0 +1,266 @@ +import React, { useState } from 'react'; +import { useData } from '../contexts/DataContext'; +import { UserApprovalStatus } from '../types'; +import { CheckCircle, XCircle, Clock, Search, Filter } from 'lucide-react'; +import { Button } from '../components/Button'; + +interface UserApprovalProps { + onNavigate?: (page: string) => void; +} + +export const UserApproval: React.FC = ({ onNavigate }) => { + const { pendingUsers, approveUser, rejectUser } = useData(); + const [searchTerm, setSearchTerm] = useState(''); + const [statusFilter, setStatusFilter] = useState<'ALL' | UserApprovalStatus>('ALL'); + const [isProcessing, setIsProcessing] = useState(null); + + const handleApprove = async (userId: string) => { + setIsProcessing(userId); + // Simular processamento + setTimeout(() => { + approveUser(userId); + setIsProcessing(null); + }, 800); + }; + + const handleReject = async (userId: string) => { + setIsProcessing(userId); + // Simular processamento + setTimeout(() => { + rejectUser(userId); + setIsProcessing(null); + }, 800); + }; + + // Filtrar usuários + const filteredUsers = pendingUsers.filter(user => { + const matchesSearch = + user.name.toLowerCase().includes(searchTerm.toLowerCase()) || + user.email.toLowerCase().includes(searchTerm.toLowerCase()) || + (user.registeredInstitution?.toLowerCase().includes(searchTerm.toLowerCase()) ?? false); + + const matchesStatus = statusFilter === 'ALL' || user.approvalStatus === statusFilter; + + return matchesSearch && matchesStatus; + }); + + const getStatusBadge = (status: UserApprovalStatus) => { + switch (status) { + case UserApprovalStatus.PENDING: + return ( + + + Pendente + + ); + case UserApprovalStatus.APPROVED: + return ( + + + Aprovado + + ); + case UserApprovalStatus.REJECTED: + return ( + + + Rejeitado + + ); + } + }; + + const pendingCount = pendingUsers.filter(u => u.approvalStatus === UserApprovalStatus.PENDING).length; + const approvedCount = pendingUsers.filter(u => u.approvalStatus === UserApprovalStatus.APPROVED).length; + const rejectedCount = pendingUsers.filter(u => u.approvalStatus === UserApprovalStatus.REJECTED).length; + + return ( +
+
+ {/* Header */} +
+

Aprovação de Cadastros

+

Gerencie os cadastros pendentes de aprovação

+
+ + {/* Stats Cards */} +
+
+
+
+

Pendentes

+

{pendingCount}

+
+
+ +
+
+
+ +
+
+
+

Aprovados

+

{approvedCount}

+
+
+ +
+
+
+ +
+
+
+

Rejeitados

+

{rejectedCount}

+
+
+ +
+
+
+
+ + {/* Filters */} +
+
+ {/* Search */} +
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-gold focus:border-transparent" + /> +
+ + {/* Status Filter */} +
+ + +
+
+
+ + {/* Table */} +
+
+ + + + + + + + + + + + + + {filteredUsers.length === 0 ? ( + + + + ) : ( + filteredUsers.map((user) => ( + + + + + + + + + + )) + )} + +
+ Nome + + Email + + Telefone + + Universidade + + Data de Cadastro + + Status + + Ações +
+
+ +

Nenhum cadastro encontrado

+

Não há cadastros que correspondam aos filtros selecionados.

+
+
+
{user.name}
+
+
{user.email}
+
+
{user.phone || '-'}
+
+
+ {user.registeredInstitution || ( + Não cadastrado + )} +
+
+
+ {user.createdAt ? new Date(user.createdAt).toLocaleDateString('pt-BR') : '-'} +
+
+ {getStatusBadge(user.approvalStatus!)} + + {user.approvalStatus === UserApprovalStatus.PENDING && ( +
+ + +
+ )} + {user.approvalStatus === UserApprovalStatus.APPROVED && ( + Aprovado + )} + {user.approvalStatus === UserApprovalStatus.REJECTED && ( + Rejeitado + )} +
+
+
+
+
+ ); +}; diff --git a/frontend/services/apiService.ts b/frontend/services/apiService.ts new file mode 100644 index 0000000..d68a787 --- /dev/null +++ b/frontend/services/apiService.ts @@ -0,0 +1,83 @@ +// Serviço para comunicação com o backend +const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api'; + +interface ApiResponse { + data: T | null; + error: string | null; + isBackendDown: boolean; +} + +// Função auxiliar para fazer requisições +async function fetchFromBackend(endpoint: string): Promise> { + try { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return { + data, + error: null, + isBackendDown: false, + }; + } catch (error) { + console.error(`Error fetching ${endpoint}:`, error); + return { + data: null, + error: error instanceof Error ? error.message : 'Erro desconhecido', + isBackendDown: true, + }; + } +} + +// Funções específicas para cada endpoint + +/** + * Busca as funções profissionais disponíveis + */ +export async function getProfessionalRoles(): Promise> { + return fetchFromBackend('/professional-roles'); +} + +/** + * Busca os tipos de eventos disponíveis + */ +export async function getEventTypes(): Promise> { + return fetchFromBackend('/event-types'); +} + +/** + * Busca os cursos/turmas disponíveis + */ +export async function getCourses(): Promise>> { + return fetchFromBackend('/courses'); +} + +/** + * Busca as instituições/empresas disponíveis + */ +export async function getInstitutions(): Promise>> { + return fetchFromBackend('/institutions'); +} + +/** + * Busca os anos de formatura disponíveis + */ +export async function getGraduationYears(): Promise> { + return fetchFromBackend('/graduation-years'); +} diff --git a/frontend/types.ts b/frontend/types.ts index f86ede2..6729ca2 100644 --- a/frontend/types.ts +++ b/frontend/types.ts @@ -5,6 +5,12 @@ export enum UserRole { PHOTOGRAPHER = "PHOTOGRAPHER", } +export enum UserApprovalStatus { + PENDING = "PENDING", + APPROVED = "APPROVED", + REJECTED = "REJECTED", +} + export enum EventStatus { PENDING_APPROVAL = "Aguardando Aprovação", // Novo status para clientes PLANNING = "Em Planejamento", @@ -33,6 +39,10 @@ export interface User { role: UserRole; avatar?: string; institutionId?: string; // Instituição vinculada ao usuário + approvalStatus?: UserApprovalStatus; // Status de aprovação do cadastro + registeredInstitution?: string; // Nome da instituição cadastrada durante o registro (se houver) + phone?: string; // Telefone do usuário + createdAt?: string; // Data de criação do cadastro } export interface Institution {