Implementa validação de horários para evitar conflitos no aceite de eventos, correções na sincronização de dados da agenda e melhorias na interface de gestão de equipe. Backend: - handler.go: Correção no retorno do endpoint [UpdateAssignmentStatus](cci:1://file:///c:/Projetos/photum/backend/internal/agenda/handler.go:279:0-313:1) para enviar JSON válido e evitar erros no frontend. - service.go: Implementação da lógica de validação de conflitos antes de aceitar um evento. - agenda.sql: Nova query `CheckProfessionalBusyDate` para verificação de sobreposição de horários. Frontend: - Dashboard.tsx: Adição de tooltip e texto para exibir o "Motivo da Rejeição" na gestão de equipe (Desktop/Mobile). - EventScheduler.tsx: Filtro para excluir profissionais com status 'REJEITADO' e correção na label de 'Pendente'. - EventDetails.tsx: Refatoração para usar estado global ([useData](cci:1://file:///c:/Projetos/photum/frontend/contexts/DataContext.tsx:1156:0-1160:2)), garantindo atualização imediata de datas e locais. - DataContext.tsx: Mapeamento do campo `local_evento` e melhoria no tratamento de erro otimista. - Ajustes gerais em ProfessionalDetailsModal, Login e correções de tipos.
431 lines
17 KiB
TypeScript
431 lines
17 KiB
TypeScript
import React, { useState } from "react";
|
|
import { useAuth } from "../contexts/AuthContext";
|
|
import { Button } from "../components/Button";
|
|
import { UserRole } from "../types";
|
|
import { X } from "lucide-react";
|
|
import { verifyAccessCode } from "../services/apiService";
|
|
|
|
interface LoginProps {
|
|
onNavigate?: (page: string) => void;
|
|
}
|
|
|
|
export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
|
|
const { login, availableUsers } = useAuth();
|
|
const [email, setEmail] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState("");
|
|
|
|
// Registration Modal States
|
|
const [showAccessCodeModal, setShowAccessCodeModal] = useState(false);
|
|
const [accessCode, setAccessCode] = useState("");
|
|
const [showProfessionalPrompt, setShowProfessionalPrompt] = useState(false);
|
|
const [codeError, setCodeError] = useState("");
|
|
// const MOCK_ACCESS_CODE = "PHOTUM2025"; // Removed mock
|
|
|
|
const handleRegisterClick = () => {
|
|
setShowProfessionalPrompt(true);
|
|
setAccessCode("");
|
|
setCodeError("");
|
|
};
|
|
|
|
const handleVerifyCode = async () => {
|
|
if (accessCode.trim() === "") {
|
|
setCodeError("Por favor, digite o código de acesso");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await verifyAccessCode(accessCode.toUpperCase());
|
|
if (res.data && res.data.valid) {
|
|
setShowAccessCodeModal(false);
|
|
window.location.href = "/cadastro";
|
|
} else {
|
|
setCodeError(res.data?.error || "Código de acesso inválido ou expirado");
|
|
}
|
|
} catch (e) {
|
|
setCodeError("Erro ao verificar código");
|
|
}
|
|
};
|
|
|
|
const handleProfessionalChoice = (isProfessional: boolean) => {
|
|
setShowProfessionalPrompt(false);
|
|
if (isProfessional) {
|
|
window.location.href = "/cadastro-profissional";
|
|
} else {
|
|
setShowAccessCodeModal(true);
|
|
}
|
|
};
|
|
|
|
const handleLogin = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setIsLoading(true);
|
|
setError("");
|
|
|
|
try {
|
|
const success = await login(email, password);
|
|
// If mock login returns true (it doesn't throw), we are good.
|
|
// 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."
|
|
);
|
|
}
|
|
} catch (err: any) {
|
|
setError(err.message || "Erro ao realizar login");
|
|
}
|
|
setIsLoading(false);
|
|
};
|
|
|
|
const fillCredentials = (userEmail: string) => {
|
|
setEmail(userEmail);
|
|
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;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<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-3 sm:mb-4">
|
|
<img
|
|
src="/logo.png"
|
|
alt="Photum Formaturas"
|
|
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-[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={handleRegisterClick}
|
|
className="font-medium hover:opacity-80 transition-opacity"
|
|
style={{ color: "#B9CF33" }}
|
|
>
|
|
Cadastre-se
|
|
</button>
|
|
</p>
|
|
</div>
|
|
|
|
<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-[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
|
|
type="email"
|
|
required
|
|
placeholder="nome@exemplo.com"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
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>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<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"}
|
|
placeholder="••••••••"
|
|
required
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
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"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
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>
|
|
) : (
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
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" }}
|
|
>
|
|
{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>
|
|
<div className="space-y-2">
|
|
{[
|
|
{ id: "1", name: "Dev Admin", email: "admin@photum.com", role: UserRole.SUPERADMIN },
|
|
{ id: "2", name: "PHOTUM CEO", email: "empresa@photum.com", role: UserRole.BUSINESS_OWNER },
|
|
{ id: "3", name: "COLABORADOR PHOTUM", email: "foto@photum.com", role: UserRole.PHOTOGRAPHER },
|
|
{ id: "4", name: "CLIENTE TESTE", email: "cliente@photum.com", role: UserRole.EVENT_OWNER },
|
|
].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" }}
|
|
onMouseEnter={(e) => {
|
|
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";
|
|
}}
|
|
>
|
|
<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>
|
|
</div>
|
|
<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>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
{/* Modal de Código de Acesso */}
|
|
{
|
|
showAccessCodeModal && (
|
|
<div
|
|
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"
|
|
onClick={() => setShowAccessCodeModal(false)}
|
|
>
|
|
<div
|
|
className="bg-white rounded-2xl shadow-2xl max-w-md w-full p-6 sm:p-8 animate-fade-in-scale"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="flex justify-between items-center mb-6">
|
|
<h2 className="text-2xl font-bold text-gray-900">
|
|
Código de Acesso
|
|
</h2>
|
|
<button
|
|
onClick={() => setShowAccessCodeModal(false)}
|
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
|
>
|
|
<X size={24} />
|
|
</button>
|
|
</div>
|
|
|
|
<p className="text-gray-600 mb-6">
|
|
Digite o código de acesso fornecido pela empresa para continuar
|
|
com o cadastro.
|
|
</p>
|
|
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Código de Acesso *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={accessCode}
|
|
onChange={(e) => {
|
|
setAccessCode(e.target.value.toUpperCase());
|
|
setCodeError("");
|
|
}}
|
|
onKeyPress={(e) => e.key === "Enter" && handleVerifyCode()}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#B9CF33] focus:border-transparent uppercase"
|
|
placeholder="Digite o código"
|
|
autoFocus
|
|
/>
|
|
{codeError && (
|
|
<p className="text-red-500 text-sm mt-2">{codeError}</p>
|
|
)}
|
|
</div>
|
|
|
|
<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> O código de acesso é fornecido pela
|
|
empresa e tem validade temporária.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex gap-3">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowAccessCodeModal(false)}
|
|
className="flex-1"
|
|
>
|
|
Cancelar
|
|
</Button>
|
|
<Button onClick={handleVerifyCode} className="flex-1">
|
|
Verificar
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
{/* Modal de Escolha de Tipo de Cadastro */}
|
|
{
|
|
showProfessionalPrompt && (
|
|
<div
|
|
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"
|
|
onClick={() => setShowProfessionalPrompt(false)}
|
|
>
|
|
<div
|
|
className="bg-white rounded-2xl shadow-2xl max-w-md w-full p-6 sm:p-8 animate-fade-in-scale"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="flex justify-between items-center mb-6">
|
|
<h2 className="text-2xl font-bold text-gray-900">
|
|
Tipo de Cadastro
|
|
</h2>
|
|
<button
|
|
onClick={() => setShowProfessionalPrompt(false)}
|
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
|
>
|
|
<X size={24} />
|
|
</button>
|
|
</div>
|
|
|
|
<p className="text-gray-600 mb-6">
|
|
Você é um profissional (fotógrafo/cinegrafista) ou um cliente?
|
|
</p>
|
|
|
|
<div className="space-y-3">
|
|
<button
|
|
onClick={() => handleProfessionalChoice(true)}
|
|
className="w-full p-4 border-2 border-[#492E61] bg-[#492E61]/5 hover:bg-[#492E61]/10 rounded-xl transition-colors text-left group"
|
|
>
|
|
<h3 className="font-semibold text-[#492E61] mb-1">
|
|
Sou Profissional
|
|
</h3>
|
|
<p className="text-sm text-gray-600">
|
|
Cadastro para fotógrafos e cinegrafistas
|
|
</p>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleProfessionalChoice(false)}
|
|
className="w-full p-4 border-2 border-[#B9CF33] bg-[#B9CF33]/5 hover:bg-[#B9CF33]/10 rounded-xl transition-colors text-left group"
|
|
>
|
|
<h3 className="font-semibold text-[#492E61] mb-1">
|
|
Sou Cliente
|
|
</h3>
|
|
<p className="text-sm text-gray-600">
|
|
Cadastro para solicitar serviços
|
|
</p>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
</div >
|
|
);
|
|
};
|