Merge pull request #21 from rede5/feature/ui-improvements-photographer-finance
feat: add photographer finance page and UI improvements
|
|
@ -14,7 +14,8 @@ import { Login } from "./pages/Login";
|
|||
import { Register } from "./pages/Register";
|
||||
import { ProfessionalRegister } from "./pages/ProfessionalRegister";
|
||||
import { TeamPage } from "./pages/Team";
|
||||
import { FinancePage } from "./pages/Finance";
|
||||
import Finance from "./pages/Finance";
|
||||
import PhotographerFinance from "./pages/PhotographerFinance";
|
||||
import { SettingsPage } from "./pages/Settings";
|
||||
import { CourseManagement } from "./pages/CourseManagement";
|
||||
import { InspirationPage } from "./pages/Inspiration";
|
||||
|
|
@ -189,30 +190,28 @@ const Footer: React.FC = () => {
|
|||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<footer className="bg-gradient-to-br from-brand-purple to-brand-purple/90 text-brand-black py-12 sm:py-16 md:py-20">
|
||||
<div className="w-full max-w-[1600px] mx-auto px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 sm:gap-8 md:gap-10 lg:gap-12 xl:gap-16 mb-8 sm:mb-12 md:mb-16">
|
||||
{/* Logo e Descrição */}
|
||||
<div className="lg:col-span-1 text-center sm:text-left">
|
||||
<div className="flex flex-col items-center sm:items-start">
|
||||
<img
|
||||
src="/logo.png"
|
||||
alt="Photum Formaturas"
|
||||
className="h-24 sm:h-28 md:h-32 lg:h-36 mb-4 md:mb-6"
|
||||
/>
|
||||
<p className="text-brand-black/80 text-xs sm:text-sm md:text-base leading-relaxed">
|
||||
Eternizando momentos únicos com excelência e profissionalismo
|
||||
desde 2020.
|
||||
</p>
|
||||
</div>
|
||||
<footer className="bg-gradient-to-br from-brand-purple to-brand-purple/90 text-brand-black py-8 sm:py-12 md:py-16">
|
||||
<div className="w-full max-w-[1600px] mx-auto px-3 sm:px-6 md:px-8 lg:px-12 xl:px-16">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 sm:gap-8 md:gap-12 lg:gap-16 mb-6 sm:mb-10 md:mb-16">
|
||||
{/* Logo e Texto descritivo */}
|
||||
<div className="text-center md:text-left">
|
||||
<img
|
||||
src="/logo_preta.png"
|
||||
alt="Photum Formaturas"
|
||||
className="h-16 sm:h-20 md:h-24 mb-3 md:mb-6 mx-auto md:mx-20 object-contain"
|
||||
/>
|
||||
<p className="text-brand-black/80 text-xs sm:text-sm md:text-base leading-relaxed px-2 sm:px-0">
|
||||
Eternizando momentos únicos com excelência e profissionalismo
|
||||
desde 2020.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Links Úteis */}
|
||||
<div>
|
||||
<h4 className="font-bold text-brand-black mb-4 md:mb-6 uppercase tracking-wider text-sm sm:text-base md:text-lg">
|
||||
<div className="text-center">
|
||||
<h4 className="font-bold text-brand-black mb-3 md:mb-4 uppercase tracking-wider text-xs sm:text-sm md:text-base">
|
||||
Links Úteis
|
||||
</h4>
|
||||
<ul className="space-y-2 md:space-y-3 text-brand-black/70 text-sm sm:text-base md:text-lg">
|
||||
<ul className="space-y-1.5 sm:space-y-2 md:space-y-3 text-brand-black/70 text-xs sm:text-sm md:text-base">
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
|
|
@ -235,14 +234,14 @@ const Footer: React.FC = () => {
|
|||
</div>
|
||||
|
||||
{/* Contato */}
|
||||
<div>
|
||||
<h4 className="font-bold text-brand-black mb-4 md:mb-6 uppercase tracking-wider text-sm sm:text-base md:text-lg">
|
||||
<div className="text-center">
|
||||
<h4 className="font-bold text-brand-black mb-3 md:mb-4 uppercase tracking-wider text-xs sm:text-sm md:text-base">
|
||||
Contato
|
||||
</h4>
|
||||
<ul className="space-y-3 md:space-y-4 text-brand-black/70 text-sm sm:text-base md:text-lg">
|
||||
<li className="flex items-center gap-2 md:gap-3">
|
||||
<ul className="space-y-2 sm:space-y-3 md:space-y-4 text-brand-black/70 text-xs sm:text-sm md:text-base">
|
||||
<li className="flex items-center justify-center gap-1.5 sm:gap-2 md:gap-3">
|
||||
<svg
|
||||
className="w-5 h-5 md:w-6 md:h-6 flex-shrink-0"
|
||||
className="w-4 h-4 sm:w-5 sm:h-5 md:w-6 md:h-6 flex-shrink-0"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
|
|
@ -255,9 +254,9 @@ const Footer: React.FC = () => {
|
|||
contato@photum.com.br
|
||||
</a>
|
||||
</li>
|
||||
<li className="flex items-start gap-2 md:gap-3">
|
||||
<li className="flex items-start justify-center gap-1.5 sm:gap-2 md:gap-3">
|
||||
<svg
|
||||
className="w-5 h-5 md:w-6 md:h-6 flex-shrink-0 mt-0.5"
|
||||
className="w-4 h-4 sm:w-5 sm:h-5 md:w-6 md:h-6 flex-shrink-0 mt-0.5"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
|
|
@ -268,9 +267,9 @@ const Footer: React.FC = () => {
|
|||
<p>(19) 3621 4621</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-center gap-2 md:gap-3">
|
||||
<li className="flex items-center justify-center gap-1.5 sm:gap-2 md:gap-3">
|
||||
<svg
|
||||
className="w-5 h-5 md:w-6 md:h-6 flex-shrink-0"
|
||||
className="w-4 h-4 sm:w-5 sm:h-5 md:w-6 md:h-6 flex-shrink-0"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
|
|
@ -278,13 +277,13 @@ const Footer: React.FC = () => {
|
|||
</svg>
|
||||
<span>Americana, SP</span>
|
||||
</li>
|
||||
<li className="flex gap-4 md:gap-5 mt-4 md:mt-6">
|
||||
<li className="flex justify-center gap-3 sm:gap-4 md:gap-5 mt-3 sm:mt-4 md:mt-6">
|
||||
<a
|
||||
href="#"
|
||||
className="hover:text-brand-black transition-colors"
|
||||
>
|
||||
<svg
|
||||
className="w-7 h-7 md:w-8 md:h-8"
|
||||
className="w-6 h-6 sm:w-7 sm:h-7 md:w-8 md:h-8"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
|
|
@ -308,7 +307,7 @@ const Footer: React.FC = () => {
|
|||
className="hover:text-brand-black transition-colors"
|
||||
>
|
||||
<svg
|
||||
className="w-7 h-7 md:w-8 md:h-8"
|
||||
className="w-6 h-6 sm:w-7 sm:h-7 md:w-8 md:h-8"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
|
|
@ -321,11 +320,11 @@ const Footer: React.FC = () => {
|
|||
</div>
|
||||
|
||||
{/* Bottom Bar */}
|
||||
<div className="border-t border-black/20 pt-6 md:pt-8 flex flex-col md:flex-row justify-between items-center text-xs sm:text-sm md:text-base text-brand-black/60 gap-4">
|
||||
<p>
|
||||
<div className="border-t border-black/20 pt-4 sm:pt-6 md:pt-8 flex flex-col md:flex-row justify-between items-center text-[10px] sm:text-xs md:text-sm text-brand-black/60 gap-3 sm:gap-4">
|
||||
<p className="text-center px-2">
|
||||
© Todos os direitos reservados PHOTUM - CNPJ 27.708.950/0001-21
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-4 sm:gap-6 md:gap-8">
|
||||
<div className="flex flex-wrap justify-center gap-2 sm:gap-4 md:gap-6">
|
||||
<a
|
||||
href="#"
|
||||
onClick={() => navigate("/privacidade")}
|
||||
|
|
@ -470,7 +469,17 @@ const AppContent: React.FC = () => {
|
|||
allowedRoles={[UserRole.SUPERADMIN, UserRole.BUSINESS_OWNER]}
|
||||
>
|
||||
<PageWrapper>
|
||||
<FinancePage />
|
||||
<Finance />
|
||||
</PageWrapper>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/meus-pagamentos"
|
||||
element={
|
||||
<ProtectedRoute allowedRoles={[UserRole.PHOTOGRAPHER]}>
|
||||
<PageWrapper>
|
||||
<PhotographerFinance />
|
||||
</PageWrapper>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,36 @@
|
|||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||||
variant?: "primary" | "secondary" | "outline" | "ghost";
|
||||
size?: "sm" | "md" | "lg" | "xl";
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export const Button: React.FC<ButtonProps> = ({
|
||||
children,
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
variant = "primary",
|
||||
size = "md",
|
||||
isLoading,
|
||||
className = '',
|
||||
className = "",
|
||||
...props
|
||||
}) => {
|
||||
const baseStyles = "inline-flex items-center justify-center font-medium transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed";
|
||||
const baseStyles =
|
||||
"inline-flex items-center justify-center font-medium transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed";
|
||||
|
||||
const variants = {
|
||||
primary: "bg-[#492E61] text-white hover:bg-[#3a2450] focus:ring-[#492E61]",
|
||||
secondary: "bg-[#B9CF32] text-white hover:bg-[#a5bd2e] focus:ring-[#B9CF32]",
|
||||
outline: "border border-[#492E61] text-[#492E61] hover:bg-[#492E61]/5 focus:ring-[#492E61]",
|
||||
ghost: "text-[#492E61] hover:bg-[#492E61]/10 hover:text-[#3a2450]"
|
||||
secondary:
|
||||
"bg-[#B9CF32] text-white hover:bg-[#a5bd2e] focus:ring-[#B9CF32]",
|
||||
outline:
|
||||
"border border-[#492E61] text-[#492E61] hover:bg-[#492E61]/5 focus:ring-[#492E61]",
|
||||
ghost: "text-[#492E61] hover:bg-[#492E61]/10 hover:text-[#3a2450]",
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
sm: "text-xs px-3 py-1.5 rounded-md",
|
||||
md: "text-sm px-5 py-2.5 rounded-lg",
|
||||
lg: "text-base px-8 py-3 rounded-lg",
|
||||
xl: "text-lg px-10 py-4 rounded-xl font-semibold"
|
||||
sm: "text-[10px] sm:text-xs px-2 sm:px-3 py-1 sm:py-1.5 rounded-md",
|
||||
md: "text-xs sm:text-sm px-3 sm:px-4 md:px-5 py-2 sm:py-2.5 rounded-lg",
|
||||
lg: "text-sm sm:text-base px-5 sm:px-6 md:px-8 py-2.5 sm:py-3 rounded-lg",
|
||||
xl: "text-base sm:text-lg px-6 sm:px-8 md:px-10 py-3 sm:py-4 rounded-xl font-semibold",
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -37,12 +40,28 @@ export const Button: React.FC<ButtonProps> = ({
|
|||
{...props}
|
||||
>
|
||||
{isLoading ? (
|
||||
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-current" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
<svg
|
||||
className="animate-spin -ml-1 mr-2 h-4 w-4 text-current"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
) : null}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -361,7 +361,7 @@ export const CourseForm: React.FC<CourseFormProps> = ({
|
|||
{/* EF I / EF II */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
EF I / EF II*
|
||||
Cursos*
|
||||
</label>
|
||||
<select
|
||||
required
|
||||
|
|
@ -414,30 +414,21 @@ export const CourseForm: React.FC<CourseFormProps> = ({
|
|||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Instituição (Universidade)*
|
||||
</label>
|
||||
<select
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={instituicao}
|
||||
onChange={(e) => {
|
||||
setInstituicao(e.target.value);
|
||||
setError("");
|
||||
}}
|
||||
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 uma universidade</option>
|
||||
{universities.map((university) => (
|
||||
<option key={university.id} value={university.id}>
|
||||
{university.nome}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
placeholder="Digite o nome da universidade"
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
|
||||
/>
|
||||
{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
|
||||
universidades.
|
||||
</span>
|
||||
<span>Backend não está rodando.</span>
|
||||
</div>
|
||||
)}
|
||||
{isLoadingData && !isBackendDown && (
|
||||
|
|
|
|||
|
|
@ -1,41 +1,52 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Eye, EyeOff } from 'lucide-react';
|
||||
import React, { useState } from "react";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label: string;
|
||||
error?: string;
|
||||
mask?: 'phone' | 'cnpj' | 'cep';
|
||||
mask?: "phone" | "cnpj" | "cep";
|
||||
}
|
||||
|
||||
export const Input: React.FC<InputProps> = ({ label, error, className = '', type, mask, onChange, ...props }) => {
|
||||
export const Input: React.FC<InputProps> = ({
|
||||
label,
|
||||
error,
|
||||
className = "",
|
||||
type,
|
||||
mask,
|
||||
onChange,
|
||||
...props
|
||||
}) => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const isPassword = type === 'password';
|
||||
const inputType = isPassword && showPassword ? 'text' : type;
|
||||
const isPassword = type === "password";
|
||||
const inputType = isPassword && showPassword ? "text" : type;
|
||||
|
||||
const applyMask = (value: string, maskType?: 'phone' | 'cnpj' | 'cep') => {
|
||||
const applyMask = (value: string, maskType?: "phone" | "cnpj" | "cep") => {
|
||||
if (!maskType) return value;
|
||||
|
||||
const numbers = value.replace(/\D/g, '');
|
||||
|
||||
|
||||
const numbers = value.replace(/\D/g, "");
|
||||
|
||||
switch (maskType) {
|
||||
case 'phone':
|
||||
case "phone":
|
||||
// Limita a 11 dígitos (celular)
|
||||
const phoneNumbers = numbers.slice(0, 11);
|
||||
if (phoneNumbers.length <= 10) {
|
||||
return phoneNumbers.replace(/(\d{2})(\d{4})(\d{0,4})/, '($1) $2-$3');
|
||||
return phoneNumbers.replace(/(\d{2})(\d{4})(\d{0,4})/, "($1) $2-$3");
|
||||
}
|
||||
return phoneNumbers.replace(/(\d{2})(\d{5})(\d{0,4})/, '($1) $2-$3');
|
||||
|
||||
case 'cnpj':
|
||||
return phoneNumbers.replace(/(\d{2})(\d{5})(\d{0,4})/, "($1) $2-$3");
|
||||
|
||||
case "cnpj":
|
||||
// Limita a 14 dígitos
|
||||
const cnpjNumbers = numbers.slice(0, 14);
|
||||
return cnpjNumbers.replace(/(\d{2})(\d{3})(\d{3})(\d{4})(\d{0,2})/, '$1.$2.$3/$4-$5');
|
||||
|
||||
case 'cep':
|
||||
return cnpjNumbers.replace(
|
||||
/(\d{2})(\d{3})(\d{3})(\d{4})(\d{0,2})/,
|
||||
"$1.$2.$3/$4-$5"
|
||||
);
|
||||
|
||||
case "cep":
|
||||
// Limita a 8 dígitos
|
||||
const cepNumbers = numbers.slice(0, 8);
|
||||
return cepNumbers.replace(/(\d{5})(\d{0,3})/, '$1-$2');
|
||||
|
||||
return cepNumbers.replace(/(\d{5})(\d{0,3})/, "$1-$2");
|
||||
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
|
|
@ -53,23 +64,27 @@ export const Input: React.FC<InputProps> = ({ label, error, className = '', type
|
|||
const getMaxLength = () => {
|
||||
if (!mask) return undefined;
|
||||
switch (mask) {
|
||||
case 'phone': return 15; // (00) 00000-0000
|
||||
case 'cnpj': return 18; // 00.000.000/0000-00
|
||||
case 'cep': return 9; // 00000-000
|
||||
default: return undefined;
|
||||
case "phone":
|
||||
return 15; // (00) 00000-0000
|
||||
case "cnpj":
|
||||
return 18; // 00.000.000/0000-00
|
||||
case "cep":
|
||||
return 9; // 00000-000
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
|
||||
<label className="block text-[10px] sm:text-xs font-medium text-gray-700 mb-1 tracking-wide uppercase">
|
||||
{label}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
className={`w-full px-4 py-2 border rounded-sm focus:outline-none focus:ring-1 focus:ring-brand-gold focus:border-brand-gold transition-colors
|
||||
${error ? 'border-red-500' : 'border-gray-300'}
|
||||
${isPassword ? 'pr-10' : ''}
|
||||
className={`w-full px-2.5 sm:px-3 md:px-4 py-1.5 sm:py-2 text-xs sm:text-sm border rounded-sm focus:outline-none focus:ring-1 focus:ring-brand-gold focus:border-brand-gold transition-colors
|
||||
${error ? "border-red-500" : "border-gray-300"}
|
||||
${isPassword ? "pr-9 sm:pr-10" : ""}
|
||||
${className}`}
|
||||
type={inputType}
|
||||
onChange={handleChange}
|
||||
|
|
@ -80,13 +95,21 @@ export const Input: React.FC<InputProps> = ({ label, error, className = '', type
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
className="absolute right-2 sm:right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
{showPassword ? (
|
||||
<EyeOff size={16} className="sm:w-[18px] sm:h-[18px]" />
|
||||
) : (
|
||||
<Eye size={16} className="sm:w-[18px] sm:h-[18px]" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{error && <span className="text-xs text-red-500 mt-1">{error}</span>}
|
||||
{error && (
|
||||
<span className="text-[10px] sm:text-xs text-red-500 mt-1">
|
||||
{error}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -97,7 +120,13 @@ interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
|||
error?: string;
|
||||
}
|
||||
|
||||
export const Select: React.FC<SelectProps> = ({ label, options, error, className = '', ...props }) => {
|
||||
export const Select: React.FC<SelectProps> = ({
|
||||
label,
|
||||
options,
|
||||
error,
|
||||
className = "",
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
|
||||
|
|
@ -105,16 +134,20 @@ export const Select: React.FC<SelectProps> = ({ label, options, error, className
|
|||
</label>
|
||||
<select
|
||||
className={`w-full px-4 py-2 border rounded-sm focus:outline-none focus:ring-1 focus:ring-brand-gold focus:border-brand-gold transition-colors appearance-none bg-white
|
||||
${error ? 'border-red-500' : 'border-gray-300'}
|
||||
${error ? "border-red-500" : "border-gray-300"}
|
||||
${className}`}
|
||||
{...props}
|
||||
>
|
||||
<option value="" disabled>Selecione uma opção</option>
|
||||
{options.map(opt => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
<option value="" disabled>
|
||||
Selecione uma opção
|
||||
</option>
|
||||
{options.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{error && <span className="text-xs text-red-500 mt-1">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -72,7 +72,10 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
|||
{ name: "Solicitar Evento", path: "solicitar-evento" },
|
||||
];
|
||||
case UserRole.PHOTOGRAPHER:
|
||||
return [{ name: "Eventos Designados", path: "painel" }];
|
||||
return [
|
||||
{ name: "Eventos Designados", path: "painel" },
|
||||
{ name: "Meus Pagamentos", path: "meus-pagamentos" },
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
|
|
@ -89,9 +92,9 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<nav className="fixed w-full z-50 bg-white shadow-sm py-3">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
<nav className="fixed w-full z-50 bg-white shadow-sm py-2 sm:py-3">
|
||||
<div className="max-w-7xl mx-auto px-3 sm:px-4 lg:px-6">
|
||||
<div className="flex justify-between items-center h-12 sm:h-14 md:h-16">
|
||||
{/* Logo */}
|
||||
<div
|
||||
className="flex-shrink-0 flex items-center cursor-pointer"
|
||||
|
|
@ -100,18 +103,18 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
|||
<img
|
||||
src="/logo.png"
|
||||
alt="Photum Formaturas"
|
||||
className="h-18 sm:h-30 w-auto max-w-[200px] object-contain mb-4"
|
||||
className="h-18 sm:h-30 w-auto max-w-[200px] object-contain mb-4 ml-3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
{user && (
|
||||
<div className="hidden md:flex items-center space-x-6">
|
||||
<div className="hidden lg:flex items-center space-x-3 xl:space-x-6">
|
||||
{getLinks().map((link) => (
|
||||
<button
|
||||
key={link.path}
|
||||
onClick={() => onNavigate(link.path)}
|
||||
className={`text-sm font-medium tracking-wide uppercase hover:text-brand-gold transition-colors pb-1 ${
|
||||
className={`text-xs xl:text-sm font-medium tracking-wide uppercase hover:text-brand-gold transition-colors pb-1 ${
|
||||
currentPage === link.path
|
||||
? "text-brand-gold border-b-2 border-brand-gold"
|
||||
: "text-gray-600 border-b-2 border-transparent"
|
||||
|
|
@ -124,14 +127,14 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
|||
)}
|
||||
|
||||
{/* Right Side Actions */}
|
||||
<div className="hidden md:flex items-center space-x-4">
|
||||
<div className="hidden lg:flex items-center space-x-2 xl:space-x-4">
|
||||
{user ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-end mr-2">
|
||||
<span className="text-sm font-bold text-brand-black leading-tight">
|
||||
<div className="flex items-center gap-2 xl:gap-3">
|
||||
<div className="flex flex-col items-end mr-1 xl:mr-2">
|
||||
<span className="text-xs xl:text-sm font-bold text-brand-black leading-tight">
|
||||
{user.name}
|
||||
</span>
|
||||
<span className="text-[10px] uppercase tracking-wider text-brand-gold leading-tight">
|
||||
<span className="text-[9px] xl:text-[10px] uppercase tracking-wider text-brand-gold leading-tight">
|
||||
{getRoleLabel()}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -142,7 +145,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
|||
onClick={() =>
|
||||
setIsAccountDropdownOpen(!isAccountDropdownOpen)
|
||||
}
|
||||
className="h-9 w-9 rounded-full bg-gray-100 overflow-hidden border border-gray-200 ring-2 ring-transparent hover:ring-brand-gold transition-all cursor-pointer"
|
||||
className="h-8 w-8 xl:h-9 xl:w-9 rounded-full bg-gray-100 overflow-hidden border border-gray-200 ring-2 ring-transparent hover:ring-brand-gold transition-all cursor-pointer"
|
||||
>
|
||||
<img
|
||||
src={user.avatar}
|
||||
|
|
@ -315,7 +318,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
|||
</div>
|
||||
|
||||
{/* Mobile Buttons */}
|
||||
<div className="md:hidden flex items-center gap-2">
|
||||
<div className="lg:hidden flex items-center gap-2">
|
||||
{user ? (
|
||||
<>
|
||||
<div className="relative">
|
||||
|
|
@ -323,7 +326,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
|||
onClick={() =>
|
||||
setIsAccountDropdownOpen(!isAccountDropdownOpen)
|
||||
}
|
||||
className="h-10 w-10 rounded-full bg-gray-100 overflow-hidden border border-gray-200 ring-2 ring-transparent active:ring-brand-gold transition-all shadow-md"
|
||||
className="h-9 w-9 sm:h-10 sm:w-10 rounded-full bg-gray-100 overflow-hidden border border-gray-200 ring-2 ring-transparent active:ring-brand-gold transition-all shadow-md"
|
||||
>
|
||||
<img
|
||||
src={user.avatar}
|
||||
|
|
@ -433,9 +436,13 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
|||
{/* Menu Hamburguer - Apenas para usuários logados */}
|
||||
<button
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
className="text-brand-black hover:text-brand-gold p-2"
|
||||
className="text-brand-black hover:text-brand-gold p-1.5 sm:p-2"
|
||||
>
|
||||
{isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
{isMobileMenuOpen ? (
|
||||
<X size={22} className="sm:w-6 sm:h-6" />
|
||||
) : (
|
||||
<Menu size={22} className="sm:w-6 sm:h-6" />
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
|
|
@ -499,8 +506,8 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
|||
|
||||
{/* Mobile Menu */}
|
||||
{isMobileMenuOpen && (
|
||||
<div className="md:hidden absolute top-full left-0 w-full bg-white border-b border-gray-100 shadow-lg fade-in">
|
||||
<div className="px-4 py-4 space-y-3">
|
||||
<div className="lg:hidden absolute top-full left-0 w-full bg-white border-b border-gray-100 shadow-lg fade-in">
|
||||
<div className="px-3 sm:px-4 py-3 sm:py-4 space-y-2 sm:space-y-3">
|
||||
{user &&
|
||||
getLinks().map((link) => (
|
||||
<button
|
||||
|
|
@ -509,7 +516,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
|||
onNavigate(link.path);
|
||||
setIsMobileMenuOpen(false);
|
||||
}}
|
||||
className="block w-full text-left text-base font-medium text-gray-700 hover:text-brand-gold py-2 border-b border-gray-50"
|
||||
className="block w-full text-left text-sm sm:text-base font-medium text-gray-700 hover:text-brand-gold py-2 border-b border-gray-50"
|
||||
>
|
||||
{link.name}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PhotumFormaturas</title>
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/x-icon" href="/logofav.ico" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/logofav.ico" />
|
||||
|
||||
<!-- Mapbox GL CSS (versão CDN confiável) -->
|
||||
<link href='https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.css' rel='stylesheet' />
|
||||
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ export const CourseManagement: React.FC = () => {
|
|||
Empresa
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
EF I / EF II
|
||||
Cursos
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Observações
|
||||
|
|
|
|||
|
|
@ -211,16 +211,13 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
const matchesDate =
|
||||
!advancedFilters.date || e.date === advancedFilters.date;
|
||||
const matchesFot =
|
||||
!advancedFilters.fotId || String((e as any).fotId || '').includes(advancedFilters.fotId);
|
||||
!advancedFilters.fotId ||
|
||||
String((e as any).fotId || "").includes(advancedFilters.fotId);
|
||||
const matchesType =
|
||||
!advancedFilters.type || e.type === advancedFilters.type;
|
||||
|
||||
return (
|
||||
matchesSearch &&
|
||||
matchesStatus &&
|
||||
matchesDate &&
|
||||
matchesFot &&
|
||||
matchesType
|
||||
matchesSearch && matchesStatus && matchesDate && matchesFot && matchesType
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -292,10 +289,10 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
if (user.role === UserRole.EVENT_OWNER) {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-serif font-bold text-brand-black">
|
||||
<h1 className="text-xl sm:text-2xl md:text-3xl font-serif font-bold text-brand-black">
|
||||
Meus Eventos
|
||||
</h1>
|
||||
<p className="text-gray-500 mt-1">
|
||||
<p className="text-xs sm:text-sm text-gray-500 mt-0.5 sm:mt-1">
|
||||
Acompanhe seus eventos ou solicite novos orçamentos.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -304,10 +301,10 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
if (user.role === UserRole.PHOTOGRAPHER) {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-serif font-bold text-brand-black">
|
||||
<h1 className="text-xl sm:text-2xl md:text-3xl font-serif font-bold text-brand-black">
|
||||
Eventos Designados
|
||||
</h1>
|
||||
<p className="text-gray-500 mt-1">
|
||||
<p className="text-xs sm:text-sm text-gray-500 mt-0.5 sm:mt-1">
|
||||
Gerencie seus trabalhos e visualize detalhes.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -315,10 +312,10 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
}
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-serif font-bold text-brand-black">
|
||||
<h1 className="text-xl sm:text-2xl md:text-3xl font-serif font-bold text-brand-black">
|
||||
Gestão Geral
|
||||
</h1>
|
||||
<p className="text-gray-500 mt-1">
|
||||
<p className="text-xs sm:text-sm text-gray-500 mt-0.5 sm:mt-1">
|
||||
Controle total de eventos, aprovações e equipes.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -343,11 +340,11 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
// --- MAIN RENDER ---
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white pt-24 pb-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="min-h-screen bg-gray-50 pt-20 sm:pt-24 md:pt-28 lg:pt-32 pb-8 sm:pb-12 px-3 sm:px-4 lg:px-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
{view === "list" && (
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-4 fade-in">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between mb-6 sm:mb-8 gap-3 sm:gap-4 fade-in">
|
||||
{renderRoleSpecificHeader()}
|
||||
{renderRoleSpecificActions()}
|
||||
</div>
|
||||
|
|
@ -581,7 +578,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
FOT
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900 font-medium">
|
||||
{(selectedEvent as any).fotId || '-'}
|
||||
{(selectedEvent as any).fotId || "-"}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="hover:bg-gray-50">
|
||||
|
|
@ -589,7 +586,9 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
Data
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900">
|
||||
{new Date(selectedEvent.date + "T00:00:00").toLocaleDateString("pt-BR")}
|
||||
{new Date(
|
||||
selectedEvent.date + "T00:00:00"
|
||||
).toLocaleDateString("pt-BR")}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="hover:bg-gray-50">
|
||||
|
|
@ -597,7 +596,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
Curso
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900">
|
||||
{(selectedEvent as any).curso || '-'}
|
||||
{(selectedEvent as any).curso || "-"}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="hover:bg-gray-50">
|
||||
|
|
@ -605,7 +604,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
Instituição
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900">
|
||||
{(selectedEvent as any).instituicao || '-'}
|
||||
{(selectedEvent as any).instituicao || "-"}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="hover:bg-gray-50">
|
||||
|
|
@ -613,7 +612,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
Ano Formatura
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900">
|
||||
{(selectedEvent as any).anoFormatura || '-'}
|
||||
{(selectedEvent as any).anoFormatura || "-"}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="hover:bg-gray-50">
|
||||
|
|
@ -621,7 +620,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
Empresa
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900">
|
||||
{(selectedEvent as any).empresa || '-'}
|
||||
{(selectedEvent as any).empresa || "-"}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="hover:bg-gray-50">
|
||||
|
|
@ -637,7 +636,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
Observações
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900">
|
||||
{(selectedEvent as any).observacoes || '-'}
|
||||
{(selectedEvent as any).observacoes || "-"}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="hover:bg-gray-50">
|
||||
|
|
@ -645,7 +644,8 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
Local
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900">
|
||||
{selectedEvent.address.street}, {selectedEvent.address.number}
|
||||
{selectedEvent.address.street},{" "}
|
||||
{selectedEvent.address.number}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="hover:bg-gray-50">
|
||||
|
|
@ -653,8 +653,10 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
Endereço
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900">
|
||||
{selectedEvent.address.city} - {selectedEvent.address.state}
|
||||
{selectedEvent.address.zip && ` | CEP: ${selectedEvent.address.zip}`}
|
||||
{selectedEvent.address.city} -{" "}
|
||||
{selectedEvent.address.state}
|
||||
{selectedEvent.address.zip &&
|
||||
` | CEP: ${selectedEvent.address.zip}`}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="hover:bg-gray-50">
|
||||
|
|
@ -670,7 +672,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
Qtd Formandos
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900">
|
||||
{(selectedEvent as any).qtdFormandos || '-'}
|
||||
{(selectedEvent as any).qtdFormandos || "-"}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -40,30 +40,30 @@ export const Home: React.FC<HomeProps> = ({ onEnter }) => {
|
|||
|
||||
{/* Círculos decorativos animados */}
|
||||
<div
|
||||
className="absolute top-10 left-10 w-64 h-64 bg-white/20 rounded-full blur-3xl animate-float"
|
||||
className="absolute top-5 sm:top-10 left-5 sm:left-10 w-32 sm:w-48 md:w-64 h-32 sm:h-48 md:h-64 bg-white/20 rounded-full blur-2xl sm:blur-3xl animate-float"
|
||||
style={{ animationDelay: "0s" }}
|
||||
></div>
|
||||
<div
|
||||
className="absolute bottom-20 right-20 w-80 h-80 bg-white/20 rounded-full blur-3xl animate-float"
|
||||
className="absolute bottom-10 sm:bottom-20 right-10 sm:right-20 w-48 sm:w-64 md:w-80 h-48 sm:h-64 md:h-80 bg-white/20 rounded-full blur-2xl sm:blur-3xl animate-float"
|
||||
style={{ animationDelay: "2s" }}
|
||||
></div>
|
||||
<div
|
||||
className="absolute top-1/3 right-10 w-48 h-48 bg-white/20 rounded-full blur-2xl animate-float"
|
||||
className="absolute top-1/3 right-5 sm:right-10 w-32 sm:w-40 md:w-48 h-32 sm:h-40 md:h-48 bg-white/20 rounded-full blur-xl sm:blur-2xl animate-float"
|
||||
style={{ animationDelay: "4s" }}
|
||||
></div>
|
||||
|
||||
<div className="bg-white rounded-2xl shadow-2xl p-8 sm:p-12 max-w-md w-full mx-4 animate-fade-in-scale relative z-10">
|
||||
<div className="bg-white rounded-xl sm:rounded-2xl shadow-2xl p-6 sm:p-8 md:p-12 max-w-[90%] sm:max-w-md w-full mx-3 sm:mx-4 animate-fade-in-scale relative z-10">
|
||||
{/* Logo */}
|
||||
<div className="flex justify-center mb-6">
|
||||
<div className="flex justify-center mb-4 sm:mb-6">
|
||||
<img
|
||||
src="/logo_preta.png"
|
||||
alt="Photum Formaturas"
|
||||
className="h-24 sm:h-32 w-auto object-contain"
|
||||
className="h-16 sm:h-20 md:h-24 lg:h-32 w-auto object-contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Botões */}
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
<Button onClick={onEnter} className="w-full" size="lg">
|
||||
Entrar
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
|
||||
import React, { useState } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { Button } from '../components/Button';
|
||||
import { Input } from '../components/Input';
|
||||
import { UserRole } from '../types';
|
||||
import React, { useState } from "react";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import { Button } from "../components/Button";
|
||||
import { Input } from "../components/Input";
|
||||
import { UserRole } from "../types";
|
||||
|
||||
interface LoginProps {
|
||||
onNavigate?: (page: string) => void;
|
||||
|
|
@ -11,16 +10,16 @@ interface LoginProps {
|
|||
|
||||
export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
|
||||
const { login, availableUsers } = useAuth();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
setError("");
|
||||
|
||||
try {
|
||||
const success = await login(email, password);
|
||||
|
|
@ -28,62 +27,79 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
|
|||
// If real login throws, we catch it below.
|
||||
if (!success) {
|
||||
// Fallback for mock if it returns false without throwing
|
||||
setError('Credenciais inválidas, tente novamente ou tente um dos e-mails de demonstração.');
|
||||
setError(
|
||||
"Credenciais inválidas, tente novamente ou tente um dos e-mails de demonstração."
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Erro ao realizar login');
|
||||
setError(err.message || "Erro ao realizar login");
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const fillCredentials = (userEmail: string) => {
|
||||
setEmail(userEmail);
|
||||
setPassword('123456'); // Dummy password to pass HTML5 validation
|
||||
setPassword("123456"); // Dummy password to pass HTML5 validation
|
||||
};
|
||||
|
||||
const getRoleLabel = (role: UserRole) => {
|
||||
switch (role) {
|
||||
case UserRole.SUPERADMIN: return "Superadmin";
|
||||
case UserRole.BUSINESS_OWNER: return "Empresa";
|
||||
case UserRole.PHOTOGRAPHER: return "Fotógrafo";
|
||||
case UserRole.EVENT_OWNER: return "Cliente";
|
||||
default: return role;
|
||||
case UserRole.SUPERADMIN:
|
||||
return "Superadmin";
|
||||
case UserRole.BUSINESS_OWNER:
|
||||
return "Empresa";
|
||||
case UserRole.PHOTOGRAPHER:
|
||||
return "Fotógrafo";
|
||||
case UserRole.EVENT_OWNER:
|
||||
return "Cliente";
|
||||
default:
|
||||
return role;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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">
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-50 to-white p-3 sm:p-4 pt-20 sm:pt-24">
|
||||
<div className="w-full max-w-md fade-in relative z-10 space-y-4 sm:space-y-6">
|
||||
<div className="bg-white rounded-xl sm:rounded-2xl shadow-xl border border-gray-100 p-5 sm:p-6 md:p-8">
|
||||
{/* Logo dentro do card */}
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="flex justify-center mb-3 sm:mb-4">
|
||||
<img
|
||||
src="/logo.png"
|
||||
alt="Photum Formaturas"
|
||||
className="h-18 sm:h-30 w-auto max-w-[200px] object-contain"
|
||||
className="h-14 sm:h-16 md:h-20 w-auto max-w-[150px] sm:max-w-[180px] object-contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<p className="mt-2 text-xs sm:text-sm text-gray-600">
|
||||
Não tem uma conta?{' '}
|
||||
<span
|
||||
className="font-bold tracking-widest uppercase text-[10px] sm:text-xs"
|
||||
style={{ color: "#B9CF33" }}
|
||||
>
|
||||
Bem-vindo de volta
|
||||
</span>
|
||||
<h2 className="mt-1.5 sm:mt-2 text-xl sm:text-2xl md:text-3xl font-serif font-bold text-gray-900">
|
||||
Acesse sua conta
|
||||
</h2>
|
||||
<p className="mt-1.5 sm:mt-2 text-xs sm:text-sm text-gray-600">
|
||||
Não tem uma conta?{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onNavigate?.('cadastro')}
|
||||
onClick={() => onNavigate?.("cadastro")}
|
||||
className="font-medium hover:opacity-80 transition-opacity"
|
||||
style={{ color: '#B9CF33' }}
|
||||
style={{ color: "#B9CF33" }}
|
||||
>
|
||||
Cadastre-se
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form className="mt-6 sm:mt-8 space-y-4 sm:space-y-6" onSubmit={handleLogin}>
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
<form
|
||||
className="mt-5 sm:mt-6 md:mt-8 space-y-3 sm:space-y-4 md:space-y-6"
|
||||
onSubmit={handleLogin}
|
||||
>
|
||||
<div className="space-y-2.5 sm:space-y-3 md:space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-1.5 sm:mb-2">
|
||||
<label className="block text-[10px] sm:text-xs md:text-sm font-medium text-gray-700 mb-1 sm:mb-1.5">
|
||||
E-MAIL CORPORATIVO OU PESSOAL
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -92,28 +108,32 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
|
|||
placeholder="nome@exemplo.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full px-3 sm:px-4 py-2.5 sm:py-3 text-sm sm:text-base border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:border-transparent transition-all"
|
||||
style={{ focusRing: '2px solid #B9CF33' }}
|
||||
onFocus={(e) => e.target.style.borderColor = '#B9CF33'}
|
||||
onBlur={(e) => e.target.style.borderColor = '#d1d5db'}
|
||||
className="w-full px-2.5 sm:px-3 md:px-4 py-2 sm:py-2.5 md:py-3 text-xs sm:text-sm md:text-base border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:border-transparent transition-all"
|
||||
style={{ focusRing: "2px solid #B9CF33" }}
|
||||
onFocus={(e) => (e.target.style.borderColor = "#B9CF33")}
|
||||
onBlur={(e) => (e.target.style.borderColor = "#d1d5db")}
|
||||
/>
|
||||
{error && <span className="text-xs text-red-500 mt-1 block">{error}</span>}
|
||||
{error && (
|
||||
<span className="text-xs text-red-500 mt-1 block">
|
||||
{error}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-1.5 sm:mb-2">
|
||||
<label className="block text-[10px] sm:text-xs md:text-sm font-medium text-gray-700 mb-1 sm:mb-1.5">
|
||||
SENHA
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="••••••••"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-3 sm:px-4 py-2.5 sm:py-3 text-sm sm:text-base border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:border-transparent transition-all"
|
||||
style={{ focusRing: '2px solid #5A4B81' }}
|
||||
onFocus={(e) => e.target.style.borderColor = '#5A4B81'}
|
||||
onBlur={(e) => e.target.style.borderColor = '#d1d5db'}
|
||||
className="w-full px-2.5 sm:px-3 md:px-4 py-2 sm:py-2.5 md:py-3 text-xs sm:text-sm md:text-base border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:border-transparent transition-all"
|
||||
style={{ focusRing: "2px solid #5A4B81" }}
|
||||
onFocus={(e) => (e.target.style.borderColor = "#5A4B81")}
|
||||
onBlur={(e) => (e.target.style.borderColor = "#d1d5db")}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -121,13 +141,38 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
|
|||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{showPassword ? (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
|
|
@ -139,41 +184,63 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
|
|||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full px-6 sm:px-10 py-3 sm:py-4 text-white font-bold text-base sm:text-lg rounded-lg transition-all duration-300 transform hover:scale-[1.02] hover:shadow-xl active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style={{ backgroundColor: '#4E345F' }}
|
||||
style={{ backgroundColor: "#4E345F" }}
|
||||
>
|
||||
{isLoading ? 'Entrando...' : 'Entrar no Sistema'}
|
||||
{isLoading ? "Entrando..." : "Entrar no Sistema"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Demo Users Quick Select */}
|
||||
<div className="bg-white rounded-2xl shadow-xl border border-gray-100 p-6">
|
||||
<p className="text-[10px] sm:text-xs uppercase tracking-widest mb-3 sm:mb-4 text-center text-gray-400">Usuários de Demonstração (Clique para preencher)</p>
|
||||
<p className="text-[10px] sm:text-xs uppercase tracking-widest mb-3 sm:mb-4 text-center text-gray-400">
|
||||
Usuários de Demonstração (Clique para preencher)
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{availableUsers.map(user => (
|
||||
{availableUsers.map((user) => (
|
||||
<button
|
||||
key={user.id}
|
||||
onClick={() => fillCredentials(user.email)}
|
||||
className="w-full flex items-center justify-between p-3 sm:p-4 border-2 rounded-xl hover:bg-gray-50 transition-all duration-300 text-left group transform hover:scale-[1.01] active:scale-[0.99]"
|
||||
style={{ borderColor: '#e5e7eb' }}
|
||||
style={{ borderColor: "#e5e7eb" }}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = '#B9CF33';
|
||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(185, 207, 51, 0.15)';
|
||||
e.currentTarget.style.borderColor = "#B9CF33";
|
||||
e.currentTarget.style.boxShadow =
|
||||
"0 4px 12px rgba(185, 207, 51, 0.15)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = '#e5e7eb';
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
e.currentTarget.style.borderColor = "#e5e7eb";
|
||||
e.currentTarget.style.boxShadow = "none";
|
||||
}}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-sm sm:text-base font-bold text-gray-900 truncate">{user.name}</span>
|
||||
<span className="text-[10px] sm:text-xs uppercase tracking-wide font-semibold px-2 py-0.5 rounded-full whitespace-nowrap" style={{ backgroundColor: '#B9CF33', color: '#fff' }}>{getRoleLabel(user.role)}</span>
|
||||
<span className="text-sm sm:text-base font-bold text-gray-900 truncate">
|
||||
{user.name}
|
||||
</span>
|
||||
<span
|
||||
className="text-[10px] sm:text-xs uppercase tracking-wide font-semibold px-2 py-0.5 rounded-full whitespace-nowrap"
|
||||
style={{ backgroundColor: "#B9CF33", color: "#fff" }}
|
||||
>
|
||||
{getRoleLabel(user.role)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs sm:text-sm text-gray-500 truncate block">{user.email}</span>
|
||||
<span className="text-xs sm:text-sm text-gray-500 truncate block">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
<svg className="w-5 h-5 text-gray-400 group-hover:text-[#B9CF33] transition-colors flex-shrink-0 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-400 group-hover:text-[#B9CF33] transition-colors flex-shrink-0 ml-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
))}
|
||||
|
|
|
|||
401
frontend/pages/PhotographerFinance.tsx
Normal file
|
|
@ -0,0 +1,401 @@
|
|||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import {
|
||||
Download,
|
||||
ArrowUpDown,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
AlertCircle,
|
||||
} from "lucide-react";
|
||||
|
||||
interface PhotographerPayment {
|
||||
id: string;
|
||||
data: string;
|
||||
nomeEvento: string;
|
||||
tipoEvento: string;
|
||||
empresa: string;
|
||||
valorRecebido: number;
|
||||
dataPagamento: string;
|
||||
statusPagamento: "Pago" | "Pendente" | "Atrasado";
|
||||
}
|
||||
|
||||
type SortField = keyof PhotographerPayment | null;
|
||||
type SortDirection = "asc" | "desc" | null;
|
||||
|
||||
const PhotographerFinance: React.FC = () => {
|
||||
const [payments, setPayments] = useState<PhotographerPayment[]>([
|
||||
{
|
||||
id: "1",
|
||||
data: "2025-11-15",
|
||||
nomeEvento: "Formatura Medicina UFPR 2025",
|
||||
tipoEvento: "Formatura",
|
||||
empresa: "PhotoPro Studio",
|
||||
valorRecebido: 1500.0,
|
||||
dataPagamento: "2025-11-20",
|
||||
statusPagamento: "Pago",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
data: "2025-11-18",
|
||||
nomeEvento: "Formatura Direito PUC-PR 2025",
|
||||
tipoEvento: "Formatura",
|
||||
empresa: "PhotoPro Studio",
|
||||
valorRecebido: 1200.0,
|
||||
dataPagamento: "2025-11-25",
|
||||
statusPagamento: "Pago",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
data: "2025-12-01",
|
||||
nomeEvento: "Formatura Engenharia UTFPR 2025",
|
||||
tipoEvento: "Formatura",
|
||||
empresa: "Lens & Art",
|
||||
valorRecebido: 1800.0,
|
||||
dataPagamento: "2025-12-15",
|
||||
statusPagamento: "Pendente",
|
||||
},
|
||||
]);
|
||||
|
||||
const [sortField, setSortField] = useState<SortField>(null);
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>(null);
|
||||
const [apiError, setApiError] = useState<string>("");
|
||||
|
||||
// Load API data
|
||||
useEffect(() => {
|
||||
loadApiData();
|
||||
}, []);
|
||||
|
||||
const loadApiData = async () => {
|
||||
try {
|
||||
// TODO: Implementar chamada real da API
|
||||
// const response = await fetch("http://localhost:3000/api/photographer/payments");
|
||||
// const data = await response.json();
|
||||
// setPayments(data);
|
||||
} catch (error) {
|
||||
console.error("Erro ao carregar pagamentos:", error);
|
||||
setApiError(
|
||||
"Não foi possível carregar os dados da API. Usando dados de exemplo."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Sorting logic
|
||||
const handleSort = (field: keyof PhotographerPayment) => {
|
||||
if (sortField === field) {
|
||||
if (sortDirection === "asc") {
|
||||
setSortDirection("desc");
|
||||
} else if (sortDirection === "desc") {
|
||||
setSortDirection(null);
|
||||
setSortField(null);
|
||||
}
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortDirection("asc");
|
||||
}
|
||||
};
|
||||
|
||||
const getSortIcon = (field: keyof PhotographerPayment) => {
|
||||
if (sortField !== field) {
|
||||
return (
|
||||
<ArrowUpDown
|
||||
size={16}
|
||||
className="opacity-0 group-hover:opacity-50 transition-opacity"
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (sortDirection === "asc") {
|
||||
return <ArrowUp size={16} className="text-brand-gold" />;
|
||||
}
|
||||
if (sortDirection === "desc") {
|
||||
return <ArrowDown size={16} className="text-brand-gold" />;
|
||||
}
|
||||
return (
|
||||
<ArrowUpDown
|
||||
size={16}
|
||||
className="opacity-0 group-hover:opacity-50 transition-opacity"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const sortedPayments = useMemo(() => {
|
||||
if (!sortField || !sortDirection) return payments;
|
||||
|
||||
return [...payments].sort((a, b) => {
|
||||
const aValue = a[sortField];
|
||||
const bValue = b[sortField];
|
||||
|
||||
if (aValue === null || aValue === undefined) return 1;
|
||||
if (bValue === null || bValue === undefined) return -1;
|
||||
|
||||
let comparison = 0;
|
||||
if (typeof aValue === "string" && typeof bValue === "string") {
|
||||
comparison = aValue.localeCompare(bValue);
|
||||
} else if (typeof aValue === "number" && typeof bValue === "number") {
|
||||
comparison = aValue - bValue;
|
||||
} else if (typeof aValue === "boolean" && typeof bValue === "boolean") {
|
||||
comparison = aValue === bValue ? 0 : aValue ? 1 : -1;
|
||||
}
|
||||
|
||||
return sortDirection === "asc" ? comparison : -comparison;
|
||||
});
|
||||
}, [payments, sortField, sortDirection]);
|
||||
|
||||
// Export to CSV
|
||||
const handleExport = () => {
|
||||
const headers = [
|
||||
"Data Evento",
|
||||
"Nome Evento",
|
||||
"Tipo Evento",
|
||||
"Empresa",
|
||||
"Valor Recebido",
|
||||
"Data Pagamento",
|
||||
"Status",
|
||||
];
|
||||
|
||||
const csvContent = [
|
||||
headers.join(","),
|
||||
...sortedPayments.map((p) =>
|
||||
[
|
||||
p.data,
|
||||
`"${p.nomeEvento}"`,
|
||||
p.tipoEvento,
|
||||
p.empresa,
|
||||
p.valorRecebido.toFixed(2),
|
||||
p.dataPagamento,
|
||||
p.statusPagamento,
|
||||
].join(",")
|
||||
),
|
||||
].join("\n");
|
||||
|
||||
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
||||
const link = document.createElement("a");
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute("download", `meus_pagamentos_${Date.now()}.csv`);
|
||||
link.style.visibility = "hidden";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
// Calculate totals
|
||||
const totalRecebido = sortedPayments.reduce(
|
||||
(sum, p) => sum + p.valorRecebido,
|
||||
0
|
||||
);
|
||||
const totalPago = sortedPayments
|
||||
.filter((p) => p.statusPagamento === "Pago")
|
||||
.reduce((sum, p) => sum + p.valorRecebido, 0);
|
||||
const totalPendente = sortedPayments
|
||||
.filter((p) => p.statusPagamento === "Pendente")
|
||||
.reduce((sum, p) => sum + p.valorRecebido, 0);
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusColors = {
|
||||
Pago: "bg-green-100 text-green-800",
|
||||
Pendente: "bg-yellow-100 text-yellow-800",
|
||||
Atrasado: "bg-red-100 text-red-800",
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||||
statusColors[status as keyof typeof statusColors] ||
|
||||
"bg-gray-100 text-gray-800"
|
||||
}`}
|
||||
>
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 pt-20">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Meus Pagamentos</h1>
|
||||
<p className="mt-2 text-gray-600">
|
||||
Visualize todos os pagamentos recebidos pelos eventos fotografados
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<p className="text-sm text-gray-600 mb-1">Total Recebido</p>
|
||||
<p className="text-2xl font-bold text-gray-900">
|
||||
R$ {totalRecebido.toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<p className="text-sm text-gray-600 mb-1">Pagamentos Confirmados</p>
|
||||
<p className="text-2xl font-bold text-green-600">
|
||||
R$ {totalPago.toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<p className="text-sm text-gray-600 mb-1">Pagamentos Pendentes</p>
|
||||
<p className="text-2xl font-bold text-yellow-600">
|
||||
R$ {totalPendente.toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Card */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
{/* Actions Bar */}
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
Histórico de Pagamentos
|
||||
</h2>
|
||||
<button
|
||||
onClick={handleExport}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
<Download size={20} />
|
||||
Exportar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{apiError && (
|
||||
<div className="mx-6 mt-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg flex items-start gap-3">
|
||||
<AlertCircle
|
||||
className="text-yellow-600 flex-shrink-0 mt-0.5"
|
||||
size={20}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-yellow-800">Aviso</p>
|
||||
<p className="text-sm text-yellow-700 mt-1">{apiError}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Table */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left">
|
||||
<button
|
||||
onClick={() => handleSort("data")}
|
||||
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
|
||||
>
|
||||
Data Evento
|
||||
{getSortIcon("data")}
|
||||
</button>
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left">
|
||||
<button
|
||||
onClick={() => handleSort("nomeEvento")}
|
||||
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
|
||||
>
|
||||
Nome Evento
|
||||
{getSortIcon("nomeEvento")}
|
||||
</button>
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left">
|
||||
<button
|
||||
onClick={() => handleSort("tipoEvento")}
|
||||
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
|
||||
>
|
||||
Tipo Evento
|
||||
{getSortIcon("tipoEvento")}
|
||||
</button>
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left">
|
||||
<button
|
||||
onClick={() => handleSort("empresa")}
|
||||
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
|
||||
>
|
||||
Empresa
|
||||
{getSortIcon("empresa")}
|
||||
</button>
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left">
|
||||
<button
|
||||
onClick={() => handleSort("valorRecebido")}
|
||||
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
|
||||
>
|
||||
Valor Recebido
|
||||
{getSortIcon("valorRecebido")}
|
||||
</button>
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left">
|
||||
<button
|
||||
onClick={() => handleSort("dataPagamento")}
|
||||
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
|
||||
>
|
||||
Data Pagamento
|
||||
{getSortIcon("dataPagamento")}
|
||||
</button>
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left">
|
||||
<button
|
||||
onClick={() => handleSort("statusPagamento")}
|
||||
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
|
||||
>
|
||||
Status
|
||||
{getSortIcon("statusPagamento")}
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{sortedPayments.length === 0 ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={7}
|
||||
className="px-4 py-8 text-center text-gray-500"
|
||||
>
|
||||
Nenhum pagamento encontrado
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
sortedPayments.map((payment) => (
|
||||
<tr
|
||||
key={payment.id}
|
||||
className="hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm">
|
||||
{new Date(payment.data).toLocaleDateString("pt-BR")}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm">
|
||||
{payment.nomeEvento}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm">
|
||||
{payment.tipoEvento}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm">{payment.empresa}</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm font-medium text-green-600">
|
||||
R$ {payment.valorRecebido.toFixed(2)}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm">
|
||||
{new Date(payment.dataPagamento).toLocaleDateString(
|
||||
"pt-BR"
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm">
|
||||
{getStatusBadge(payment.statusPagamento)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-4 border-t border-gray-200 bg-gray-50">
|
||||
<p className="text-sm text-gray-600">
|
||||
Total de pagamentos: {sortedPayments.length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PhotographerFinance;
|
||||
|
|
@ -157,15 +157,15 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
|||
|
||||
<div className="text-center">
|
||||
<span
|
||||
className="font-bold tracking-widest uppercase text-xs sm:text-sm"
|
||||
className="font-bold tracking-widest uppercase text-[10px] sm:text-xs"
|
||||
style={{ color: "#B9CF33" }}
|
||||
>
|
||||
Comece agora
|
||||
</span>
|
||||
<h2 className="mt-2 text-2xl sm:text-3xl font-serif font-bold text-gray-900">
|
||||
<h2 className="mt-1.5 sm:mt-2 text-xl sm:text-2xl md: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">
|
||||
<p className="mt-1.5 sm:mt-2 text-xs sm:text-sm text-gray-600">
|
||||
Já tem uma conta?{" "}
|
||||
<button
|
||||
onClick={() => onNavigate("entrar")}
|
||||
|
|
@ -177,7 +177,28 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<form className="mt-6 space-y-4" onSubmit={handleSubmit}>
|
||||
<form
|
||||
className="mt-5 sm:mt-6 space-y-3 sm:space-y-4"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div className="flex items-start bg-blue-50 border border-blue-200 rounded-lg p-2.5 sm:p-3 md:p-4 mb-3 sm:mb-4">
|
||||
<div className="flex-1">
|
||||
<p className="text-xs sm:text-sm text-gray-700">
|
||||
<span className="font-medium text-xs sm:text-sm">
|
||||
Você é um profissional?
|
||||
</span>
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onNavigate("cadastro-profissional")}
|
||||
className="text-xs sm:text-sm mt-1 hover:opacity-80 transition-opacity underline font-medium"
|
||||
style={{ color: "#B9CF33" }}
|
||||
>
|
||||
Clique aqui para se cadastrar como profissional
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Input
|
||||
label="Nome Completo"
|
||||
|
|
@ -208,11 +229,11 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
|||
/>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
<label className="block text-[10px] sm:text-xs md:text-sm font-medium text-gray-700 mb-1">
|
||||
Empresa *
|
||||
</label>
|
||||
{isLoadingCompanies ? (
|
||||
<p className="text-sm text-gray-500">
|
||||
<p className="text-xs sm:text-sm text-gray-500">
|
||||
Carregando empresas...
|
||||
</p>
|
||||
) : (
|
||||
|
|
@ -220,7 +241,7 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
|||
required
|
||||
value={formData.empresaId}
|
||||
onChange={(e) => handleChange("empresaId", e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:border-transparent"
|
||||
className="w-full px-2.5 sm:px-3 md:px-4 py-1.5 sm:py-2 text-xs sm:text-sm border border-gray-300 rounded-lg focus:ring-2 focus:border-transparent"
|
||||
style={{ focusRing: "2px solid #B9CF33" }}
|
||||
>
|
||||
<option value="">Selecione uma empresa</option>
|
||||
|
|
@ -253,7 +274,7 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
|||
}
|
||||
error={
|
||||
error &&
|
||||
(error.includes("senha") || error.includes("coincidem"))
|
||||
(error.includes("senha") || error.includes("coincidem"))
|
||||
? error
|
||||
: undefined
|
||||
}
|
||||
|
|
@ -297,24 +318,6 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-start bg-gray-50 border border-gray-200 rounded-lg p-3 sm:p-4">
|
||||
<div className="flex-1">
|
||||
<p className="text-xs sm:text-sm text-gray-700">
|
||||
<span className="font-medium text-xs sm:text-sm">
|
||||
Você é um profissional?
|
||||
</span>
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onNavigate("cadastro-profissional")}
|
||||
className="text-xs sm:text-sm mt-1 hover:opacity-80 transition-opacity underline font-medium"
|
||||
style={{ color: "#B9CF33" }}
|
||||
>
|
||||
Clique aqui para se cadastrar como profissional
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error &&
|
||||
!error.includes("termos") &&
|
||||
!error.includes("senha") &&
|
||||
|
|
|
|||
490
frontend/pages/UniversityManagement.tsx
Normal file
|
|
@ -0,0 +1,490 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Download, Plus, ArrowUpDown, ArrowUp, ArrowDown, X, AlertCircle } from 'lucide-react';
|
||||
|
||||
interface University {
|
||||
id: string;
|
||||
empresa: string;
|
||||
nomeUniversidade: string;
|
||||
anoFormatura: number;
|
||||
semestre: number;
|
||||
cursos: string[];
|
||||
}
|
||||
|
||||
const UniversityManagement: React.FC = () => {
|
||||
const [universities, setUniversities] = useState<University[]>([
|
||||
{
|
||||
id: '1',
|
||||
empresa: 'PhotoPro Studio',
|
||||
nomeUniversidade: 'UFPR - Universidade Federal do Paraná',
|
||||
anoFormatura: 2025,
|
||||
semestre: 2,
|
||||
cursos: ['Medicina', 'Direito', 'Engenharia Civil']
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
empresa: 'Lens & Art',
|
||||
nomeUniversidade: 'PUC-PR - Pontifícia Universidade Católica do Paraná',
|
||||
anoFormatura: 2025,
|
||||
semestre: 1,
|
||||
cursos: ['Administração', 'Arquitetura', 'Psicologia']
|
||||
}
|
||||
]);
|
||||
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [selectedUniversity, setSelectedUniversity] = useState<University | null>(null);
|
||||
const [sortConfig, setSortConfig] = useState<{ key: keyof University; direction: 'asc' | 'desc' } | null>(null);
|
||||
|
||||
// Estados para dados da API
|
||||
const [cursos, setCursos] = useState<any[]>([]);
|
||||
const [empresas, setEmpresas] = useState<any[]>([]);
|
||||
const [apiError, setApiError] = useState<string>('');
|
||||
const [loadingApi, setLoadingApi] = useState(false);
|
||||
|
||||
// Form state
|
||||
const [formData, setFormData] = useState<Partial<University>>({
|
||||
empresa: '',
|
||||
nomeUniversidade: '',
|
||||
anoFormatura: new Date().getFullYear(),
|
||||
semestre: 1,
|
||||
cursos: []
|
||||
});
|
||||
|
||||
// Estados para seleção de cursos
|
||||
const [selectedCursos, setSelectedCursos] = useState<string[]>([]);
|
||||
const [cursoInput, setCursoInput] = useState('');
|
||||
|
||||
// Carregar dados da API
|
||||
const loadApiData = async () => {
|
||||
setLoadingApi(true);
|
||||
setApiError('');
|
||||
try {
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000';
|
||||
|
||||
// Carregar cursos
|
||||
try {
|
||||
const cursosRes = await fetch(`${API_BASE_URL}/api/cursos`);
|
||||
if (cursosRes.ok) {
|
||||
const cursosData = await cursosRes.json();
|
||||
setCursos(cursosData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar cursos:', error);
|
||||
}
|
||||
|
||||
// Carregar empresas
|
||||
try {
|
||||
const empRes = await fetch(`${API_BASE_URL}/api/empresas`);
|
||||
if (empRes.ok) {
|
||||
const empData = await empRes.json();
|
||||
setEmpresas(empData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar empresas:', error);
|
||||
}
|
||||
|
||||
if (cursos.length === 0 && empresas.length === 0) {
|
||||
setApiError('Backend não está rodando. Alguns campos podem não estar disponíveis.');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
setApiError('Backend não está rodando. Alguns campos podem não estar disponíveis.');
|
||||
} finally {
|
||||
setLoadingApi(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (showAddModal || showEditModal) {
|
||||
loadApiData();
|
||||
}
|
||||
}, [showAddModal, showEditModal]);
|
||||
|
||||
// Ordenação
|
||||
const handleSort = (key: keyof University) => {
|
||||
if (sortConfig && sortConfig.key === key) {
|
||||
if (sortConfig.direction === 'asc') {
|
||||
setSortConfig({ key, direction: 'desc' });
|
||||
} else {
|
||||
setSortConfig(null);
|
||||
}
|
||||
} else {
|
||||
setSortConfig({ key, direction: 'asc' });
|
||||
}
|
||||
};
|
||||
|
||||
const sortedUniversities = React.useMemo(() => {
|
||||
let sortableUniversities = [...universities];
|
||||
if (sortConfig !== null) {
|
||||
sortableUniversities.sort((a, b) => {
|
||||
const aValue = a[sortConfig.key];
|
||||
const bValue = b[sortConfig.key];
|
||||
|
||||
if (aValue < bValue) {
|
||||
return sortConfig.direction === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (aValue > bValue) {
|
||||
return sortConfig.direction === 'asc' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
return sortableUniversities;
|
||||
}, [universities, sortConfig]);
|
||||
|
||||
const getSortIcon = (key: keyof University) => {
|
||||
if (sortConfig?.key !== key) {
|
||||
return <ArrowUpDown size={14} className="opacity-0 group-hover:opacity-50 transition-opacity" />;
|
||||
}
|
||||
if (sortConfig.direction === 'asc') {
|
||||
return <ArrowUp size={14} className="text-brand-gold" />;
|
||||
}
|
||||
return <ArrowDown size={14} className="text-brand-gold" />;
|
||||
};
|
||||
|
||||
// Handlers
|
||||
const handleAddUniversity = () => {
|
||||
setFormData({
|
||||
empresa: '',
|
||||
nomeUniversidade: '',
|
||||
semestre: 1,
|
||||
anoFormatura: new Date().getFullYear(),
|
||||
cursos: []
|
||||
});
|
||||
setSelectedCursos([]);
|
||||
setShowAddModal(true);
|
||||
};
|
||||
|
||||
const handleEditUniversity = (university: University) => {
|
||||
setSelectedUniversity(university);
|
||||
setFormData(university);
|
||||
setSelectedCursos(university.cursos || []);
|
||||
setShowEditModal(true);
|
||||
};
|
||||
|
||||
const handleAddCurso = () => {
|
||||
if (cursoInput && !selectedCursos.includes(cursoInput)) {
|
||||
setSelectedCursos([...selectedCursos, cursoInput]);
|
||||
setCursoInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveCurso = (curso: string) => {
|
||||
setSelectedCursos(selectedCursos.filter(c => c !== curso));
|
||||
};
|
||||
|
||||
const handleSaveUniversity = () => {
|
||||
const updatedData = { ...formData, cursos: selectedCursos };
|
||||
|
||||
if (showEditModal && selectedUniversity) {
|
||||
setUniversities(universities.map(u =>
|
||||
u.id === selectedUniversity.id
|
||||
? { ...updatedData, id: selectedUniversity.id } as University
|
||||
: u
|
||||
));
|
||||
setShowEditModal(false);
|
||||
} else {
|
||||
const newUniversity: University = {
|
||||
...updatedData,
|
||||
id: Date.now().toString()
|
||||
} as University;
|
||||
setUniversities([...universities, newUniversity]);
|
||||
setShowAddModal(false);
|
||||
}
|
||||
setSelectedUniversity(null);
|
||||
setSelectedCursos([]);
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
const headers = ['Empresa', 'Nome da Universidade', 'Ano Formatura', 'Cursos'];
|
||||
|
||||
const csvContent = [
|
||||
headers.join(','),
|
||||
...universities.map(u => [
|
||||
u.empresa,
|
||||
`"${u.nomeUniversidade}"`,
|
||||
`${u.anoFormatura}.${u.semestre}`,
|
||||
`"${u.cursos.join(', ')}"`
|
||||
].join(','))
|
||||
].join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = `universidades_${new Date().toISOString().split('T')[0]}.csv`;
|
||||
link.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 pt-20 sm:pt-24 pb-8 sm:pb-12">
|
||||
<div className="max-w-[95%] mx-auto px-3 sm:px-4">
|
||||
{/* Header */}
|
||||
<div className="mb-4 sm:mb-6 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-0">
|
||||
<div>
|
||||
<h1 className="text-xl sm:text-2xl md:text-3xl font-serif font-bold text-brand-black mb-1 sm:mb-2">
|
||||
Gestão de Universidades
|
||||
</h1>
|
||||
<p className="text-xs sm:text-sm text-gray-600">
|
||||
Cadastro e gerenciamento de universidades
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2 sm:gap-3 w-full sm:w-auto">
|
||||
<button
|
||||
onClick={handleExport}
|
||||
className="flex items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-1.5 sm:py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors font-medium text-xs sm:text-sm flex-1 sm:flex-none justify-center"
|
||||
>
|
||||
<Download size={16} className="sm:w-[18px] sm:h-[18px]" />
|
||||
<span className="hidden sm:inline">Exportar</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAddUniversity}
|
||||
className="flex items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-1.5 sm:py-2 bg-brand-gold text-white rounded-md hover:bg-[#a5bd2e] transition-colors font-medium text-xs sm:text-sm flex-1 sm:flex-none justify-center"
|
||||
>
|
||||
<Plus size={16} className="sm:w-[18px] sm:h-[18px]" />
|
||||
<span className="hidden sm:inline">Cadastrar</span>
|
||||
<span className="sm:hidden">Nova Universidade</span>
|
||||
<span className="hidden sm:inline">Universidade</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabela */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-x-auto">
|
||||
<table className="w-full text-xs sm:text-sm min-w-[600px]">
|
||||
<thead className="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
{[
|
||||
{ key: 'empresa', label: 'Empresa' },
|
||||
{ key: 'nomeUniversidade', label: 'Nome da Universidade' },
|
||||
{ key: 'anoFormatura', label: 'Ano Formatura' },
|
||||
{ key: 'cursos', label: 'Cursos' }
|
||||
].map((column) => (
|
||||
<th
|
||||
key={column.key}
|
||||
onClick={() => handleSort(column.key as keyof University)}
|
||||
className="px-4 py-3 text-left font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors whitespace-nowrap group"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{column.label}
|
||||
{getSortIcon(column.key as keyof University)}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{sortedUniversities.map((university) => (
|
||||
<tr
|
||||
key={university.id}
|
||||
onClick={() => handleEditUniversity(university)}
|
||||
className="hover:bg-gray-50 cursor-pointer transition-colors"
|
||||
>
|
||||
<td className="px-4 py-3 whitespace-nowrap">{university.empresa}</td>
|
||||
<td className="px-4 py-3">{university.nomeUniversidade}</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap">{university.anoFormatura}.{university.semestre}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{university.cursos.map((curso, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs"
|
||||
>
|
||||
{curso}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{sortedUniversities.length === 0 && (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
Nenhuma universidade cadastrada
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal Adicionar/Editar */}
|
||||
{(showAddModal || showEditModal) && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="sticky top-0 bg-white border-b border-gray-200 p-6 flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold">
|
||||
{showEditModal ? 'Editar Universidade' : 'Cadastrar Universidade'}
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowAddModal(false);
|
||||
setShowEditModal(false);
|
||||
setSelectedUniversity(null);
|
||||
setSelectedCursos([]);
|
||||
}}
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{apiError && (
|
||||
<div className="mx-6 mt-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg flex items-start gap-3">
|
||||
<AlertCircle className="text-yellow-600 flex-shrink-0 mt-0.5" size={20} />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-yellow-800">Aviso</p>
|
||||
<p className="text-sm text-yellow-700 mt-1">{apiError}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-6">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{/* Empresa */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Empresa *
|
||||
</label>
|
||||
<select
|
||||
value={formData.empresa}
|
||||
onChange={(e) => setFormData({ ...formData, empresa: e.target.value })}
|
||||
disabled={loadingApi}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent disabled:bg-gray-50 disabled:text-gray-500"
|
||||
>
|
||||
<option value="">
|
||||
{loadingApi ? 'Carregando...' : empresas.length > 0 ? 'Selecione uma empresa' : 'Nenhuma empresa disponível'}
|
||||
</option>
|
||||
{empresas.map((emp) => (
|
||||
<option key={emp.id} value={emp.nome}>
|
||||
{emp.nome}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Nome da Universidade */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Nome da Universidade *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.nomeUniversidade}
|
||||
onChange={(e) => setFormData({ ...formData, nomeUniversidade: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent"
|
||||
placeholder="Ex: UFPR - Universidade Federal do Paraná"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Ano Formatura */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Ano *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.anoFormatura}
|
||||
onChange={(e) => setFormData({ ...formData, anoFormatura: parseInt(e.target.value) })}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent"
|
||||
placeholder="2025"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Semestre *
|
||||
</label>
|
||||
<select
|
||||
value={formData.semestre}
|
||||
onChange={(e) => setFormData({ ...formData, semestre: parseInt(e.target.value) })}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent"
|
||||
>
|
||||
<option value={1}>1</option>
|
||||
<option value={2}>2</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cursos */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Cursos *
|
||||
</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<select
|
||||
value={cursoInput}
|
||||
onChange={(e) => setCursoInput(e.target.value)}
|
||||
disabled={loadingApi}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent disabled:bg-gray-50 disabled:text-gray-500"
|
||||
>
|
||||
<option value="">
|
||||
{loadingApi ? 'Carregando...' : cursos.length > 0 ? 'Selecione um curso' : 'Nenhum curso disponível'}
|
||||
</option>
|
||||
{cursos.map((curso) => (
|
||||
<option key={curso.id} value={curso.nome}>
|
||||
{curso.nome}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddCurso}
|
||||
disabled={!cursoInput}
|
||||
className="px-4 py-2 bg-brand-gold text-white rounded-md hover:bg-[#a5bd2e] transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Adicionar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Lista de cursos selecionados */}
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
{selectedCursos.map((curso, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm flex items-center gap-2"
|
||||
>
|
||||
{curso}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveCurso(curso)}
|
||||
className="hover:text-blue-900"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{selectedCursos.length === 0 && (
|
||||
<p className="text-sm text-gray-500 mt-2">Nenhum curso selecionado</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Botões */}
|
||||
<div className="flex gap-3 mt-8">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowAddModal(false);
|
||||
setShowEditModal(false);
|
||||
setSelectedUniversity(null);
|
||||
setSelectedCursos([]);
|
||||
}}
|
||||
className="flex-1 px-4 py-3 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300 transition-colors font-medium"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSaveUniversity}
|
||||
className="flex-1 px-4 py-3 bg-brand-gold text-white rounded-md hover:bg-[#a5bd2e] transition-colors font-medium"
|
||||
>
|
||||
{showEditModal ? 'Salvar Alterações' : 'Cadastrar'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UniversityManagement;
|
||||
|
|
@ -108,14 +108,14 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
|||
).length;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 pt-20 pb-8">
|
||||
<div className="min-h-screen bg-gray-50 pt-20 sm:pt-24 md:pt-28 lg:pt-32 pb-8 sm:pb-12">
|
||||
<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">
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<h1 className="text-2xl sm:text-3xl font-serif font-bold text-brand-black">
|
||||
Aprovação de Cadastros
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
<p className="text-sm sm:text-base text-gray-600 mt-1">
|
||||
Gerencie os cadastros pendentes de aprovação
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 711 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 933 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB |
BIN
frontend/public/faviconlogo.ico
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
frontend/public/faviconlogo.png
Normal file
BIN
frontend/public/logofav.ico
Normal file
|
After Width: | Height: | Size: 191 KiB |