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
|
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
|
# 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 { SettingsPage } from "./pages/Settings";
|
||||||
import { CourseManagement } from "./pages/CourseManagement";
|
import { CourseManagement } from "./pages/CourseManagement";
|
||||||
import { InspirationPage } from "./pages/Inspiration";
|
import { InspirationPage } from "./pages/Inspiration";
|
||||||
|
import { UserApproval } from "./pages/UserApproval";
|
||||||
import { PrivacyPolicy } from "./pages/PrivacyPolicy";
|
import { PrivacyPolicy } from "./pages/PrivacyPolicy";
|
||||||
import { TermsOfUse } from "./pages/TermsOfUse";
|
import { TermsOfUse } from "./pages/TermsOfUse";
|
||||||
import { LGPD } from "./pages/LGPD";
|
import { LGPD } from "./pages/LGPD";
|
||||||
|
|
@ -429,6 +430,16 @@ const AppContent: React.FC = () => {
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/aprovacao-cadastros"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute allowedRoles={[UserRole.SUPERADMIN, UserRole.BUSINESS_OWNER]}>
|
||||||
|
<PageWrapper>
|
||||||
|
<UserApproval />
|
||||||
|
</PageWrapper>
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Rota padrão - redireciona para home */}
|
{/* Rota padrão - redireciona para home */}
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<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 { Course, Institution } from "../types";
|
||||||
import { Input, Select } from "./Input";
|
import { Input, Select } from "./Input";
|
||||||
import { Button } from "./Button";
|
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 {
|
interface CourseFormProps {
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
|
@ -44,6 +45,38 @@ export const CourseForm: React.FC<CourseFormProps> = ({
|
||||||
|
|
||||||
const [showToast, setShowToast] = useState(false);
|
const [showToast, setShowToast] = useState(false);
|
||||||
const [error, setError] = useState("");
|
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) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -140,25 +173,63 @@ export const CourseForm: React.FC<CourseFormProps> = ({
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<div>
|
||||||
label="Nome do Curso/Turma*"
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
placeholder="Ex: Engenharia Civil 2025, Medicina - Turma A"
|
Nome do Curso/Turma*
|
||||||
value={formData.name || ""}
|
</label>
|
||||||
onChange={(e) => handleChange("name", e.target.value)}
|
<select
|
||||||
required
|
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">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
<Input
|
<div>
|
||||||
label="Ano*"
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
type="number"
|
Ano*
|
||||||
placeholder={currentYear.toString()}
|
</label>
|
||||||
value={formData.year || currentYear}
|
<select
|
||||||
onChange={(e) => handleChange("year", parseInt(e.target.value))}
|
required
|
||||||
min={currentYear - 1}
|
value={formData.year || currentYear}
|
||||||
max={currentYear + 5}
|
onChange={(e) => handleChange("year", parseInt(e.target.value))}
|
||||||
required
|
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
|
<Select
|
||||||
label="Semestre"
|
label="Semestre"
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
Building2,
|
Building2,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
|
AlertTriangle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
searchMapboxLocation,
|
searchMapboxLocation,
|
||||||
|
|
@ -25,6 +26,7 @@ import { useData } from "../contexts/DataContext";
|
||||||
import { UserRole } from "../types";
|
import { UserRole } from "../types";
|
||||||
import { InstitutionForm } from "./InstitutionForm";
|
import { InstitutionForm } from "./InstitutionForm";
|
||||||
import { MapboxMap } from "./MapboxMap";
|
import { MapboxMap } from "./MapboxMap";
|
||||||
|
import { getEventTypes } from "../services/apiService";
|
||||||
|
|
||||||
interface EventFormProps {
|
interface EventFormProps {
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
|
@ -54,6 +56,9 @@ export const EventForm: React.FC<EventFormProps> = ({
|
||||||
const [showToast, setShowToast] = useState(false);
|
const [showToast, setShowToast] = useState(false);
|
||||||
const [showInstitutionForm, setShowInstitutionForm] = useState(false);
|
const [showInstitutionForm, setShowInstitutionForm] = useState(false);
|
||||||
const [availableCourses, setAvailableCourses] = useState<any[]>([]);
|
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
|
// Get institutions based on user role
|
||||||
// Business owners and admins see all institutions, clients see only their own
|
// Business owners and admins see all institutions, clients see only their own
|
||||||
|
|
@ -98,6 +103,26 @@ export const EventForm: React.FC<EventFormProps> = ({
|
||||||
: isClientRequest
|
: isClientRequest
|
||||||
? "Solicitar Orçamento/Evento"
|
? "Solicitar Orçamento/Evento"
|
||||||
: "Cadastrar Novo 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
|
const submitLabel = initialData
|
||||||
? "Salvar Alterações"
|
? "Salvar Alterações"
|
||||||
: isClientRequest
|
: isClientRequest
|
||||||
|
|
@ -440,17 +465,36 @@ export const EventForm: React.FC<EventFormProps> = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Select
|
<div>
|
||||||
label="Tipo de Evento"
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
options={Object.values(EventType).map((t) => ({
|
Tipo de Evento*
|
||||||
value: t,
|
</label>
|
||||||
label: t,
|
<select
|
||||||
}))}
|
required
|
||||||
value={formData.type}
|
value={formData.type}
|
||||||
onChange={(e) =>
|
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
|
||||||
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
|
<Input
|
||||||
label="Número de Pessoas"
|
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: "Gestão de Eventos", path: "painel" },
|
||||||
{ name: "Equipe", path: "equipe" },
|
{ name: "Equipe", path: "equipe" },
|
||||||
{ name: "Gestão de Cursos", path: "cursos" },
|
{ name: "Gestão de Cursos", path: "cursos" },
|
||||||
|
{ name: "Aprovação de Cadastros", path: "aprovacao-cadastros" },
|
||||||
{ name: "Financeiro", path: "financeiro" },
|
{ name: "Financeiro", path: "financeiro" },
|
||||||
];
|
];
|
||||||
case UserRole.EVENT_OWNER:
|
case UserRole.EVENT_OWNER:
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ import {
|
||||||
EventType,
|
EventType,
|
||||||
Institution,
|
Institution,
|
||||||
Course,
|
Course,
|
||||||
|
User,
|
||||||
|
UserApprovalStatus,
|
||||||
|
UserRole,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
// Initial Mock Data
|
// Initial Mock Data
|
||||||
|
|
@ -582,6 +585,7 @@ interface DataContextType {
|
||||||
events: EventData[];
|
events: EventData[];
|
||||||
institutions: Institution[];
|
institutions: Institution[];
|
||||||
courses: Course[];
|
courses: Course[];
|
||||||
|
pendingUsers: User[];
|
||||||
addEvent: (event: EventData) => void;
|
addEvent: (event: EventData) => void;
|
||||||
updateEventStatus: (id: string, status: EventStatus) => void;
|
updateEventStatus: (id: string, status: EventStatus) => void;
|
||||||
assignPhotographer: (eventId: string, photographerId: string) => void;
|
assignPhotographer: (eventId: string, photographerId: string) => void;
|
||||||
|
|
@ -595,6 +599,9 @@ interface DataContextType {
|
||||||
getCoursesByInstitutionId: (institutionId: string) => Course[];
|
getCoursesByInstitutionId: (institutionId: string) => Course[];
|
||||||
getActiveCoursesByInstitutionId: (institutionId: string) => Course[];
|
getActiveCoursesByInstitutionId: (institutionId: string) => Course[];
|
||||||
getCourseById: (id: string) => Course | undefined;
|
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);
|
const DataContext = createContext<DataContextType | undefined>(undefined);
|
||||||
|
|
@ -606,6 +613,7 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
const [institutions, setInstitutions] =
|
const [institutions, setInstitutions] =
|
||||||
useState<Institution[]>(INITIAL_INSTITUTIONS);
|
useState<Institution[]>(INITIAL_INSTITUTIONS);
|
||||||
const [courses, setCourses] = useState<Course[]>(INITIAL_COURSES);
|
const [courses, setCourses] = useState<Course[]>(INITIAL_COURSES);
|
||||||
|
const [pendingUsers, setPendingUsers] = useState<User[]>([]);
|
||||||
|
|
||||||
const addEvent = (event: EventData) => {
|
const addEvent = (event: EventData) => {
|
||||||
setEvents((prev) => [event, ...prev]);
|
setEvents((prev) => [event, ...prev]);
|
||||||
|
|
@ -686,12 +694,54 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
return courses.find((course) => course.id === id);
|
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 (
|
return (
|
||||||
<DataContext.Provider
|
<DataContext.Provider
|
||||||
value={{
|
value={{
|
||||||
events,
|
events,
|
||||||
institutions,
|
institutions,
|
||||||
courses,
|
courses,
|
||||||
|
pendingUsers,
|
||||||
addEvent,
|
addEvent,
|
||||||
updateEventStatus,
|
updateEventStatus,
|
||||||
assignPhotographer,
|
assignPhotographer,
|
||||||
|
|
@ -705,6 +755,9 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
getCoursesByInstitutionId,
|
getCoursesByInstitutionId,
|
||||||
getActiveCoursesByInstitutionId,
|
getActiveCoursesByInstitutionId,
|
||||||
getCourseById,
|
getCourseById,
|
||||||
|
registerPendingUser,
|
||||||
|
approveUser,
|
||||||
|
rejectUser,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -42,18 +42,18 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-50 to-white p-4">
|
<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 space-y-8 fade-in">
|
<div className="w-full max-w-md fade-in relative z-10 space-y-6">
|
||||||
{/* Logo */}
|
<div className="bg-white rounded-2xl shadow-xl border border-gray-100 p-6 sm:p-8">
|
||||||
<div className="flex justify-center mb-8">
|
{/* Logo dentro do card */}
|
||||||
<img
|
<div className="flex justify-center mb-4">
|
||||||
src="/logo.png"
|
<img
|
||||||
alt="Photum Formaturas"
|
src="/logo.png"
|
||||||
className="h-20 w-auto object-contain"
|
alt="Photum Formaturas"
|
||||||
/>
|
className="h-18 sm:h-30 w-auto max-w-[200px] object-contain"
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl shadow-xl border border-gray-100 p-8">
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<span className="font-bold tracking-widest uppercase text-xs sm:text-sm" style={{ color: '#B9CF33' }}>Bem-vindo de volta</span>
|
<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>
|
<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 }) => {
|
export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
||||||
const { addInstitution } = useData();
|
const { addInstitution, registerPendingUser } = useData();
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
|
|
@ -22,9 +22,10 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
||||||
const [agreedToTerms, setAgreedToTerms] = useState(false);
|
const [agreedToTerms, setAgreedToTerms] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [success, setSuccess] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
const [showInstitutionForm, setShowInstitutionForm] = useState(false);
|
const [showInstitutionForm, setShowInstitutionForm] = useState(false);
|
||||||
const [tempUserId] = useState(`user-${Date.now()}`);
|
const [tempUserId] = useState(`user-${Date.now()}`);
|
||||||
|
const [registeredInstitutionName, setRegisteredInstitutionName] = useState<string | undefined>();
|
||||||
|
|
||||||
const handleChange = (field: string, value: string | boolean) => {
|
const handleChange = (field: string, value: string | boolean) => {
|
||||||
setFormData(prev => ({ ...prev, [field]: value }));
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
|
|
@ -63,13 +64,17 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simular registro (conta será criada como Cliente/EVENT_OWNER automaticamente)
|
// Simular registro - usuário ficará pendente de aprovação
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
registerPendingUser({
|
||||||
|
id: tempUserId,
|
||||||
|
name: formData.name,
|
||||||
|
email: formData.email,
|
||||||
|
phone: formData.phone,
|
||||||
|
registeredInstitution: undefined
|
||||||
|
});
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setSuccess(true);
|
setIsPending(true);
|
||||||
setTimeout(() => {
|
|
||||||
onNavigate('entrar');
|
|
||||||
}, 2000);
|
|
||||||
}, 1500);
|
}, 1500);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -80,16 +85,21 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
||||||
ownerId: tempUserId
|
ownerId: tempUserId
|
||||||
};
|
};
|
||||||
addInstitution(newInstitution);
|
addInstitution(newInstitution);
|
||||||
|
setRegisteredInstitutionName(newInstitution.name);
|
||||||
setShowInstitutionForm(false);
|
setShowInstitutionForm(false);
|
||||||
|
|
||||||
// Complete registration
|
// Complete registration - usuário ficará pendente de aprovação
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
registerPendingUser({
|
||||||
|
id: tempUserId,
|
||||||
|
name: formData.name,
|
||||||
|
email: formData.email,
|
||||||
|
phone: formData.phone,
|
||||||
|
registeredInstitution: newInstitution.name
|
||||||
|
});
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setSuccess(true);
|
setIsPending(true);
|
||||||
setTimeout(() => {
|
|
||||||
onNavigate('entrar');
|
|
||||||
}, 2000);
|
|
||||||
}, 1500);
|
}, 1500);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -111,39 +121,66 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (success) {
|
if (isPending) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-white">
|
<div className="min-h-screen flex items-center justify-center bg-white">
|
||||||
<div className="text-center fade-in">
|
<div className="text-center fade-in max-w-md px-4">
|
||||||
<div className="w-16 h-16 bg-green-500 rounded-full flex items-center justify-center mx-auto mb-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">
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Cadastro realizado com sucesso!</h2>
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">Cadastro Pendente de Aprovação</h2>
|
||||||
<p className="text-gray-600">Redirecionando para o login...</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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="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 space-y-8 fade-in">
|
<div className="w-full max-w-md fade-in relative z-10">
|
||||||
{/* Logo */}
|
<div className="bg-white rounded-2xl shadow-xl border border-gray-100 p-6 sm:p-8">
|
||||||
<div className="flex justify-center mb-8">
|
{/* Logo dentro do card */}
|
||||||
<img
|
<div className="flex justify-center mb-4">
|
||||||
src="/logo.png"
|
<img
|
||||||
alt="Photum Formaturas"
|
src="/logo.png"
|
||||||
className="h-20 w-auto object-contain"
|
alt="Photum Formaturas"
|
||||||
/>
|
className="h-18 sm:h-30 w-auto max-w-[200px] object-contain"
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl shadow-xl border border-gray-100 p-8">
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<span className="font-bold tracking-widest uppercase text-sm" style={{ color: '#B9CF33' }}>Comece agora</span>
|
<span className="font-bold tracking-widest uppercase text-xs sm: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>
|
<h2 className="mt-2 text-2xl sm:text-3xl font-serif font-bold text-gray-900">Crie sua conta</h2>
|
||||||
<p className="mt-3 text-sm text-gray-600">
|
<p className="mt-2 text-xs sm:text-sm text-gray-600">
|
||||||
Já tem uma conta?{' '}
|
Já tem uma conta?{' '}
|
||||||
<button
|
<button
|
||||||
onClick={() => onNavigate('entrar')}
|
onClick={() => onNavigate('entrar')}
|
||||||
|
|
@ -155,8 +192,8 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form className="mt-8 space-y-5" onSubmit={handleSubmit}>
|
<form className="mt-6 space-y-4" onSubmit={handleSubmit}>
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
<Input
|
<Input
|
||||||
label="Nome Completo"
|
label="Nome Completo"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
Users,
|
Users,
|
||||||
Camera,
|
Camera,
|
||||||
|
|
@ -18,8 +18,10 @@ import {
|
||||||
Building,
|
Building,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
|
AlertTriangle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "../components/Button";
|
import { Button } from "../components/Button";
|
||||||
|
import { getProfessionalRoles } from "../services/apiService";
|
||||||
|
|
||||||
type ProfessionalRole = "Fotógrafo" | "Cinegrafista" | "Recepcionista";
|
type ProfessionalRole = "Fotógrafo" | "Cinegrafista" | "Recepcionista";
|
||||||
|
|
||||||
|
|
@ -202,6 +204,11 @@ export const TeamPage: React.FC = () => {
|
||||||
const [selectedProfessional, setSelectedProfessional] =
|
const [selectedProfessional, setSelectedProfessional] =
|
||||||
useState<Professional | null>(null);
|
useState<Professional | null>(null);
|
||||||
const [showAddModal, setShowAddModal] = useState(false);
|
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>>(
|
const [newProfessional, setNewProfessional] = useState<Partial<Professional>>(
|
||||||
{
|
{
|
||||||
|
|
@ -248,6 +255,26 @@ export const TeamPage: React.FC = () => {
|
||||||
const [avatarFile, setAvatarFile] = useState<File | null>(null);
|
const [avatarFile, setAvatarFile] = useState<File | null>(null);
|
||||||
const [avatarPreview, setAvatarPreview] = useState<string>("");
|
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 handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
|
|
@ -265,6 +292,64 @@ export const TeamPage: React.FC = () => {
|
||||||
setAvatarPreview("");
|
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"]) => {
|
const getStatusColor = (status: Professional["status"]) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "active":
|
case "active":
|
||||||
|
|
@ -571,6 +656,109 @@ export const TeamPage: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Add Professional Modal - com todos os campos da planilha */}
|
||||||
{showAddModal && (
|
{showAddModal && (
|
||||||
<div
|
<div
|
||||||
|
|
@ -640,12 +828,27 @@ export const TeamPage: React.FC = () => {
|
||||||
role: e.target.value as ProfessionalRole,
|
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="">Selecione uma função</option>
|
||||||
<option value="Cinegrafista">Cinegrafista</option>
|
{professionalRoles.map((role) => (
|
||||||
<option value="Recepcionista">Recepcionista</option>
|
<option key={role} value={role}>
|
||||||
|
{role}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
</select>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1035,6 +1238,24 @@ export const TeamPage: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* Avaliações */}
|
{/* Avaliações */}
|
||||||
|
|
@ -1429,7 +1650,10 @@ export const TeamPage: React.FC = () => {
|
||||||
>
|
>
|
||||||
Fechar
|
Fechar
|
||||||
</button>
|
</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
|
Editar Dados
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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",
|
PHOTOGRAPHER = "PHOTOGRAPHER",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum UserApprovalStatus {
|
||||||
|
PENDING = "PENDING",
|
||||||
|
APPROVED = "APPROVED",
|
||||||
|
REJECTED = "REJECTED",
|
||||||
|
}
|
||||||
|
|
||||||
export enum EventStatus {
|
export enum EventStatus {
|
||||||
PENDING_APPROVAL = "Aguardando Aprovação", // Novo status para clientes
|
PENDING_APPROVAL = "Aguardando Aprovação", // Novo status para clientes
|
||||||
PLANNING = "Em Planejamento",
|
PLANNING = "Em Planejamento",
|
||||||
|
|
@ -33,6 +39,10 @@ export interface User {
|
||||||
role: UserRole;
|
role: UserRole;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
institutionId?: string; // Instituição vinculada ao usuário
|
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 {
|
export interface Institution {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue