Merge pull request #12 from rede5/feature/ui-improvements

feat: melhorias de UI e novas funcionalidades
This commit is contained in:
João Vitor 2025-12-10 17:19:48 -03:00 committed by GitHub
commit 5711a727c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 888 additions and 82 deletions

View file

@ -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

View file

@ -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 = () => {
</ProtectedRoute>
}
/>
<Route
path="/aprovacao-cadastros"
element={
<ProtectedRoute allowedRoles={[UserRole.SUPERADMIN, UserRole.BUSINESS_OWNER]}>
<PageWrapper>
<UserApproval />
</PageWrapper>
</ProtectedRoute>
}
/>
{/* Rota padrão - redireciona para home */}
<Route path="*" element={<Navigate to="/" replace />} />

View file

@ -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<CourseFormProps> = ({
const [showToast, setShowToast] = useState(false);
const [error, setError] = useState("");
const [backendInstitutions, setBackendInstitutions] = useState<Array<{ id: string; name: string }>>([]);
const [graduationYears, setGraduationYears] = useState<number[]>([]);
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<CourseFormProps> = ({
required
/>
<Input
label="Nome do Curso/Turma*"
placeholder="Ex: Engenharia Civil 2025, Medicina - Turma A"
value={formData.name || ""}
onChange={(e) => handleChange("name", e.target.value)}
required
/>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Nome do Curso/Turma*
</label>
<select
required
value={formData.name || ""}
onChange={(e) => handleChange("name", e.target.value)}
disabled={isLoadingData || 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"
>
<option value="">Selecione um curso</option>
{backendInstitutions.map((inst) => (
<option key={inst.id} value={inst.name}>
{inst.name}
</option>
))}
</select>
{isBackendDown && (
<div className="mt-2 flex items-center gap-2 text-sm text-red-600">
<AlertTriangle size={16} />
<span>Backend não está rodando. Não é possível carregar os cursos.</span>
</div>
)}
{isLoadingData && !isBackendDown && (
<div className="mt-2 text-sm text-gray-500">
Carregando cursos...
</div>
)}
</div>
<div className="grid grid-cols-3 gap-4">
<Input
label="Ano*"
type="number"
placeholder={currentYear.toString()}
value={formData.year || currentYear}
onChange={(e) => handleChange("year", parseInt(e.target.value))}
min={currentYear - 1}
max={currentYear + 5}
required
/>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Ano*
</label>
<select
required
value={formData.year || currentYear}
onChange={(e) => handleChange("year", parseInt(e.target.value))}
disabled={isLoadingData || 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"
>
<option value="">Selecione o ano</option>
{graduationYears.map((year) => (
<option key={year} value={year}>
{year}
</option>
))}
</select>
{isBackendDown && (
<div className="mt-2 flex items-center gap-2 text-sm text-red-600">
<AlertTriangle size={16} />
<span>Backend offline</span>
</div>
)}
</div>
<Select
label="Semestre"

View file

@ -14,6 +14,7 @@ import {
CheckCircle,
Building2,
AlertCircle,
AlertTriangle,
} from "lucide-react";
import {
searchMapboxLocation,
@ -25,6 +26,7 @@ import { useData } from "../contexts/DataContext";
import { UserRole } from "../types";
import { InstitutionForm } from "./InstitutionForm";
import { MapboxMap } from "./MapboxMap";
import { getEventTypes } from "../services/apiService";
interface EventFormProps {
onCancel: () => void;
@ -54,6 +56,9 @@ export const EventForm: React.FC<EventFormProps> = ({
const [showToast, setShowToast] = useState(false);
const [showInstitutionForm, setShowInstitutionForm] = useState(false);
const [availableCourses, setAvailableCourses] = useState<any[]>([]);
const [eventTypes, setEventTypes] = useState<string[]>([]);
const [isBackendDown, setIsBackendDown] = useState(false);
const [isLoadingEventTypes, setIsLoadingEventTypes] = useState(true);
// Get institutions based on user role
// Business owners and admins see all institutions, clients see only their own
@ -98,6 +103,26 @@ export const EventForm: React.FC<EventFormProps> = ({
: isClientRequest
? "Solicitar Orçamento/Evento"
: "Cadastrar Novo Evento";
// Buscar tipos de eventos do backend
useEffect(() => {
const fetchEventTypes = async () => {
setIsLoadingEventTypes(true);
const response = await getEventTypes();
if (response.isBackendDown) {
setIsBackendDown(true);
setEventTypes([]);
} else if (response.data) {
setIsBackendDown(false);
setEventTypes(response.data);
}
setIsLoadingEventTypes(false);
};
fetchEventTypes();
}, []);
const submitLabel = initialData
? "Salvar Alterações"
: isClientRequest
@ -440,17 +465,36 @@ export const EventForm: React.FC<EventFormProps> = ({
/>
</div>
<Select
label="Tipo de Evento"
options={Object.values(EventType).map((t) => ({
value: t,
label: t,
}))}
value={formData.type}
onChange={(e) =>
setFormData({ ...formData, type: e.target.value })
}
/>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Tipo de Evento*
</label>
<select
required
value={formData.type}
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
disabled={isLoadingEventTypes || 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"
>
<option value="">Selecione o tipo de evento</option>
{eventTypes.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
{isBackendDown && (
<div className="mt-2 flex items-center gap-2 text-sm text-red-600">
<AlertTriangle size={16} />
<span>Backend não está rodando. Não é possível carregar os tipos de eventos.</span>
</div>
)}
{isLoadingEventTypes && !isBackendDown && (
<div className="mt-2 text-sm text-gray-500">
Carregando tipos de eventos...
</div>
)}
</div>
<Input
label="Número de Pessoas"

View file

@ -63,6 +63,7 @@ export const Navbar: React.FC<NavbarProps> = ({ 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:

View file

@ -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<DataContextType | undefined>(undefined);
@ -606,6 +613,7 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
const [institutions, setInstitutions] =
useState<Institution[]>(INITIAL_INSTITUTIONS);
const [courses, setCourses] = useState<Course[]>(INITIAL_COURSES);
const [pendingUsers, setPendingUsers] = useState<User[]>([]);
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 (
<DataContext.Provider
value={{
events,
institutions,
courses,
pendingUsers,
addEvent,
updateEventStatus,
assignPhotographer,
@ -705,6 +755,9 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
getCoursesByInstitutionId,
getActiveCoursesByInstitutionId,
getCourseById,
registerPendingUser,
approveUser,
rejectUser,
}}
>
{children}

View file

@ -42,18 +42,18 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
}
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-50 to-white p-4">
<div className="w-full max-w-md space-y-8 fade-in">
{/* Logo */}
<div className="flex justify-center mb-8">
<img
src="/logo.png"
alt="Photum Formaturas"
className="h-20 w-auto object-contain"
/>
</div>
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-50 to-white p-4 pt-24">
<div className="w-full max-w-md fade-in relative z-10 space-y-6">
<div className="bg-white rounded-2xl shadow-xl border border-gray-100 p-6 sm:p-8">
{/* Logo dentro do card */}
<div className="flex justify-center mb-4">
<img
src="/logo.png"
alt="Photum Formaturas"
className="h-18 sm:h-30 w-auto max-w-[200px] object-contain"
/>
</div>
<div className="bg-white rounded-2xl shadow-xl border border-gray-100 p-8">
<div className="text-center">
<span className="font-bold tracking-widest uppercase text-xs sm:text-sm" style={{ color: '#B9CF33' }}>Bem-vindo de volta</span>
<h2 className="mt-2 text-2xl sm:text-3xl font-serif font-bold text-gray-900">Acesse sua conta</h2>

View file

@ -10,7 +10,7 @@ interface RegisterProps {
}
export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
const { addInstitution } = useData();
const { addInstitution, registerPendingUser } = useData();
const [formData, setFormData] = useState({
name: '',
email: '',
@ -22,9 +22,10 @@ export const Register: React.FC<RegisterProps> = ({ 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<string | undefined>();
const handleChange = (field: string, value: string | boolean) => {
setFormData(prev => ({ ...prev, [field]: value }));
@ -63,13 +64,17 @@ export const Register: React.FC<RegisterProps> = ({ 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<RegisterProps> = ({ 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<RegisterProps> = ({ onNavigate }) => {
);
}
if (success) {
if (isPending) {
return (
<div className="min-h-screen flex items-center justify-center bg-white">
<div className="text-center fade-in">
<div className="w-16 h-16 bg-green-500 rounded-full flex items-center justify-center mx-auto mb-4">
<div className="text-center fade-in max-w-md px-4">
<div className="w-16 h-16 bg-yellow-500 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">Cadastro realizado com sucesso!</h2>
<p className="text-gray-600">Redirecionando para o login...</p>
<h2 className="text-2xl font-bold text-gray-900 mb-2">Cadastro Pendente de Aprovação</h2>
<p className="text-gray-600 mb-4">
Seu cadastro foi realizado com sucesso e está aguardando aprovação da empresa.
</p>
<p className="text-gray-600 mb-6">
Você receberá um e-mail assim que seu cadastro for aprovado e poderá acessar o sistema.
</p>
<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> Enquanto seu cadastro não for aprovado, você não terá acesso ao sistema.
</p>
</div>
<button
onClick={() => {
setIsPending(false);
setFormData({
name: '',
email: '',
phone: '',
password: '',
confirmPassword: '',
wantsToAddInstitution: false
});
setAgreedToTerms(false);
}}
className="px-6 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors"
>
Fazer Novo Cadastro
</button>
</div>
</div>
);
}
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-50 to-white p-4 py-12">
<div className="w-full max-w-md space-y-8 fade-in">
{/* Logo */}
<div className="flex justify-center mb-8">
<img
src="/logo.png"
alt="Photum Formaturas"
className="h-20 w-auto object-contain"
/>
</div>
<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="w-full max-w-md fade-in relative z-10">
<div className="bg-white rounded-2xl shadow-xl border border-gray-100 p-6 sm:p-8">
{/* Logo dentro do card */}
<div className="flex justify-center mb-4">
<img
src="/logo.png"
alt="Photum Formaturas"
className="h-18 sm:h-30 w-auto max-w-[200px] object-contain"
/>
</div>
<div className="bg-white rounded-2xl shadow-xl border border-gray-100 p-8">
<div className="text-center">
<span className="font-bold tracking-widest uppercase text-sm" style={{ color: '#B9CF33' }}>Comece agora</span>
<h2 className="mt-3 text-3xl font-serif font-bold text-gray-900">Crie sua conta</h2>
<p className="mt-3 text-sm text-gray-600">
<span className="font-bold tracking-widest uppercase text-xs sm:text-sm" style={{ color: '#B9CF33' }}>Comece agora</span>
<h2 className="mt-2 text-2xl sm:text-3xl font-serif font-bold text-gray-900">Crie sua conta</h2>
<p className="mt-2 text-xs sm:text-sm text-gray-600">
tem uma conta?{' '}
<button
onClick={() => onNavigate('entrar')}
@ -155,8 +192,8 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
</p>
</div>
<form className="mt-8 space-y-5" onSubmit={handleSubmit}>
<div className="space-y-4">
<form className="mt-6 space-y-4" onSubmit={handleSubmit}>
<div className="space-y-3">
<Input
label="Nome Completo"
type="text"

View file

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import {
Users,
Camera,
@ -18,8 +18,10 @@ import {
Building,
CreditCard,
TrendingUp,
AlertTriangle,
} from "lucide-react";
import { Button } from "../components/Button";
import { getProfessionalRoles } from "../services/apiService";
type ProfessionalRole = "Fotógrafo" | "Cinegrafista" | "Recepcionista";
@ -202,6 +204,11 @@ export const TeamPage: React.FC = () => {
const [selectedProfessional, setSelectedProfessional] =
useState<Professional | null>(null);
const [showAddModal, setShowAddModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [editingProfessional, setEditingProfessional] = useState<Professional | null>(null);
const [professionalRoles, setProfessionalRoles] = useState<string[]>([]);
const [isBackendDown, setIsBackendDown] = useState(false);
const [isLoadingRoles, setIsLoadingRoles] = useState(true);
const [newProfessional, setNewProfessional] = useState<Partial<Professional>>(
{
@ -248,6 +255,26 @@ export const TeamPage: React.FC = () => {
const [avatarFile, setAvatarFile] = useState<File | null>(null);
const [avatarPreview, setAvatarPreview] = useState<string>("");
// 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<HTMLInputElement>) => {
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 = () => {
</div>
</div>
{/* Edit Professional Modal */}
{showEditModal && (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 overflow-y-auto"
onClick={() => setShowEditModal(false)}
>
<div
className="bg-white rounded-lg max-w-4xl w-full p-8 max-h-[90vh] overflow-y-auto my-8"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-2xl font-serif font-bold text-brand-black">
Editar Profissional
</h2>
<p className="text-sm text-gray-500 mt-1">
Atualize as informações do profissional
</p>
</div>
<button
onClick={() => {
setShowEditModal(false);
setEditingProfessional(null);
}}
className="text-gray-400 hover:text-gray-600"
>
<X size={24} />
</button>
</div>
<form
className="space-y-6"
onSubmit={(e) => {
e.preventDefault();
handleUpdateProfessional();
alert("Profissional atualizado com sucesso!");
}}
>
{/* Reutilizar o mesmo conteúdo do formulário de adicionar */}
{/* Avatar Upload */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Foto do Profissional
</label>
<div className="flex items-center space-x-4">
{avatarPreview ? (
<div className="relative">
<img
src={avatarPreview}
alt="Preview"
className="w-24 h-24 rounded-full object-cover border-2 border-gray-200"
/>
<button
type="button"
onClick={removeAvatar}
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 hover:bg-red-600"
>
<X size={16} />
</button>
</div>
) : (
<div className="w-24 h-24 rounded-full bg-gray-200 flex items-center justify-center">
<User className="text-gray-400" size={40} />
</div>
)}
<label className="cursor-pointer bg-gray-100 hover:bg-gray-200 text-gray-700 px-4 py-2 rounded-md transition-colors flex items-center space-x-2">
<Upload size={16} />
<span>Upload Foto</span>
<input
type="file"
accept="image/*"
onChange={handleAvatarChange}
className="hidden"
/>
</label>
</div>
</div>
{/* Botões de Ação */}
<div className="flex justify-end space-x-3 pt-6 border-t">
<button
type="button"
onClick={() => {
setShowEditModal(false);
setEditingProfessional(null);
}}
className="px-6 py-3 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition-colors font-medium"
>
Cancelar
</button>
<button
type="submit"
className="px-6 py-3 bg-brand-gold text-white rounded-md hover:bg-[#a5bd2e] transition-colors font-medium flex items-center space-x-2"
>
<Check size={18} />
<span>Salvar Alterações</span>
</button>
</div>
</form>
</div>
</div>
)}
{/* Add Professional Modal - com todos os campos da planilha */}
{showAddModal && (
<div
@ -640,12 +828,27 @@ export const TeamPage: React.FC = () => {
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"
>
<option value="Fotógrafo">Fotógrafo</option>
<option value="Cinegrafista">Cinegrafista</option>
<option value="Recepcionista">Recepcionista</option>
<option value="">Selecione uma função</option>
{professionalRoles.map((role) => (
<option key={role} value={role}>
{role}
</option>
))}
</select>
{isBackendDown && (
<div className="mt-2 flex items-center gap-2 text-sm text-red-600">
<AlertTriangle size={16} />
<span>Backend não está rodando. Não é possível carregar as funções profissionais.</span>
</div>
)}
{isLoadingRoles && !isBackendDown && (
<div className="mt-2 text-sm text-gray-500">
Carregando funções profissionais...
</div>
)}
</div>
</div>
@ -1035,6 +1238,24 @@ export const TeamPage: React.FC = () => {
</div>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Tipo Cartão
</label>
<input
type="text"
value={newProfessional.cardType}
onChange={(e) =>
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"
/>
</div>
</div>
{/* Avaliações */}
@ -1429,7 +1650,10 @@ export const TeamPage: React.FC = () => {
>
Fechar
</button>
<button className="flex-1 px-6 py-3 bg-brand-gold text-white rounded-md hover:bg-[#a5bd2e] transition-colors font-medium">
<button
onClick={() => handleEditProfessional(selectedProfessional)}
className="flex-1 px-6 py-3 bg-brand-gold text-white rounded-md hover:bg-[#a5bd2e] transition-colors font-medium"
>
Editar Dados
</button>
</div>

View file

@ -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<UserApprovalProps> = ({ onNavigate }) => {
const { pendingUsers, approveUser, rejectUser } = useData();
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<'ALL' | UserApprovalStatus>('ALL');
const [isProcessing, setIsProcessing] = useState<string | null>(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 (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
<Clock className="w-3 h-3 mr-1" />
Pendente
</span>
);
case UserApprovalStatus.APPROVED:
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<CheckCircle className="w-3 h-3 mr-1" />
Aprovado
</span>
);
case UserApprovalStatus.REJECTED:
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
<XCircle className="w-3 h-3 mr-1" />
Rejeitado
</span>
);
}
};
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 (
<div className="min-h-screen bg-gray-50 pt-20 pb-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Aprovação de Cadastros</h1>
<p className="text-gray-600">Gerencie os cadastros pendentes de aprovação</p>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Pendentes</p>
<p className="text-3xl font-bold text-yellow-600">{pendingCount}</p>
</div>
<div className="p-3 bg-yellow-100 rounded-full">
<Clock className="w-8 h-8 text-yellow-600" />
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Aprovados</p>
<p className="text-3xl font-bold text-green-600">{approvedCount}</p>
</div>
<div className="p-3 bg-green-100 rounded-full">
<CheckCircle className="w-8 h-8 text-green-600" />
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Rejeitados</p>
<p className="text-3xl font-bold text-red-600">{rejectedCount}</p>
</div>
<div className="p-3 bg-red-100 rounded-full">
<XCircle className="w-8 h-8 text-red-600" />
</div>
</div>
</div>
</div>
{/* Filters */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-6">
<div className="flex flex-col sm:flex-row gap-4">
{/* Search */}
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
placeholder="Buscar por nome, email ou universidade..."
value={searchTerm}
onChange={(e) => 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"
/>
</div>
{/* Status Filter */}
<div className="flex items-center gap-2">
<Filter className="w-5 h-5 text-gray-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as any)}
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-gold focus:border-transparent"
>
<option value="ALL">Todos os Status</option>
<option value={UserApprovalStatus.PENDING}>Pendentes</option>
<option value={UserApprovalStatus.APPROVED}>Aprovados</option>
<option value={UserApprovalStatus.REJECTED}>Rejeitados</option>
</select>
</div>
</div>
</div>
{/* Table */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Nome
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Email
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Telefone
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Universidade
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Data de Cadastro
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Ações
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredUsers.length === 0 ? (
<tr>
<td colSpan={7} className="px-6 py-12 text-center text-gray-500">
<div className="flex flex-col items-center justify-center">
<Clock className="w-12 h-12 text-gray-300 mb-3" />
<p className="text-lg font-medium">Nenhum cadastro encontrado</p>
<p className="text-sm">Não cadastros que correspondam aos filtros selecionados.</p>
</div>
</td>
</tr>
) : (
filteredUsers.map((user) => (
<tr key={user.id} className="hover:bg-gray-50 transition-colors">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{user.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">{user.email}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">{user.phone || '-'}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{user.registeredInstitution || (
<span className="text-gray-400 italic">Não cadastrado</span>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{user.createdAt ? new Date(user.createdAt).toLocaleDateString('pt-BR') : '-'}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getStatusBadge(user.approvalStatus!)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
{user.approvalStatus === UserApprovalStatus.PENDING && (
<div className="flex gap-2">
<Button
size="sm"
onClick={() => handleApprove(user.id)}
isLoading={isProcessing === user.id}
disabled={isProcessing !== null}
className="bg-green-600 hover:bg-green-700 text-white"
>
<CheckCircle className="w-4 h-4 mr-1" />
Aprovar
</Button>
<Button
size="sm"
onClick={() => handleReject(user.id)}
isLoading={isProcessing === user.id}
disabled={isProcessing !== null}
className="bg-red-600 hover:bg-red-700 text-white"
>
<XCircle className="w-4 h-4 mr-1" />
Rejeitar
</Button>
</div>
)}
{user.approvalStatus === UserApprovalStatus.APPROVED && (
<span className="text-green-600 text-xs">Aprovado</span>
)}
{user.approvalStatus === UserApprovalStatus.REJECTED && (
<span className="text-red-600 text-xs">Rejeitado</span>
)}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
};

View file

@ -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<T> {
data: T | null;
error: string | null;
isBackendDown: boolean;
}
// Função auxiliar para fazer requisições
async function fetchFromBackend<T>(endpoint: string): Promise<ApiResponse<T>> {
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<ApiResponse<string[]>> {
return fetchFromBackend<string[]>('/professional-roles');
}
/**
* Busca os tipos de eventos disponíveis
*/
export async function getEventTypes(): Promise<ApiResponse<string[]>> {
return fetchFromBackend<string[]>('/event-types');
}
/**
* Busca os cursos/turmas disponíveis
*/
export async function getCourses(): Promise<ApiResponse<Array<{
id: string;
name: string;
institution: string;
year: number;
}>>> {
return fetchFromBackend('/courses');
}
/**
* Busca as instituições/empresas disponíveis
*/
export async function getInstitutions(): Promise<ApiResponse<Array<{
id: string;
name: string;
}>>> {
return fetchFromBackend('/institutions');
}
/**
* Busca os anos de formatura disponíveis
*/
export async function getGraduationYears(): Promise<ApiResponse<number[]>> {
return fetchFromBackend<number[]>('/graduation-years');
}

View file

@ -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 {