Merge pull request #12 from rede5/feature/ui-improvements
feat: melhorias de UI e novas funcionalidades
This commit is contained in:
commit
5711a727c3
12 changed files with 888 additions and 82 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 />} />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
Já 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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
266
frontend/pages/UserApproval.tsx
Normal file
266
frontend/pages/UserApproval.tsx
Normal 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 há 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>
|
||||
);
|
||||
};
|
||||
83
frontend/services/apiService.ts
Normal file
83
frontend/services/apiService.ts
Normal 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');
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue