Initial commit
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { motion } from "motion/react";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { Country } from "../data";
|
||||
|
||||
interface CountryCardProps {
|
||||
country: Country;
|
||||
onSelect: (country: Country) => void;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
export function CountryCard({ country, onSelect }: CountryCardProps) {
|
||||
return (
|
||||
<motion.article
|
||||
whileHover={{ scale: 1.01, transition: { duration: 0.15 } }}
|
||||
className="bg-white border border-[#D0D0D0] border-l-4 border-l-[#6CC24A] p-6 flex flex-col justify-between min-h-[220px] rounded-none shadow-none hover:bg-[#F5F5F5] transition-colors group"
|
||||
id={`card-${country.code.toLowerCase()}`}
|
||||
>
|
||||
<div>
|
||||
{/* Country Flag Container */}
|
||||
<div className="mb-4">
|
||||
<img
|
||||
alt={`Bandera de ${country.name}`}
|
||||
className="w-12 h-8 object-cover border border-[#D0D0D0] rounded-none"
|
||||
src={country.flagUrl}
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Country Name */}
|
||||
<h3 className="text-xl font-bold text-[#4F758B] mb-2" id={`title-${country.code.toLowerCase()}`}>
|
||||
{country.name}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
<button
|
||||
onClick={() => onSelect(country)}
|
||||
className="bg-[#4F758B] text-white text-xs font-semibold py-3 px-4 w-full rounded-none mt-6 hover:bg-[#365C71] active:bg-[#234b60] transition-colors text-left flex justify-between items-center group-hover:bg-[#365C71] cursor-pointer focus:outline-none"
|
||||
title={`Cruce de Cuentas GLM - ${country.code}`}
|
||||
id={`btn-cruce-${country.code.toLowerCase()}`}
|
||||
>
|
||||
<span>Cruce de Cuentas GLM - {country.code}</span>
|
||||
<ArrowRight className="w-4 h-4 text-white group-hover:translate-x-1 transition-transform" />
|
||||
</button>
|
||||
</motion.article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { useState } from "react";
|
||||
import { motion } from "motion/react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { IMAGES } from "../data";
|
||||
|
||||
interface LoginScreenProps {
|
||||
onLogin: () => Promise<void> | void;
|
||||
authError?: string;
|
||||
isSupabaseConfigured?: boolean;
|
||||
allowedEmails?: readonly string[];
|
||||
}
|
||||
|
||||
export function LoginScreen({
|
||||
onLogin,
|
||||
authError,
|
||||
isSupabaseConfigured = true,
|
||||
allowedEmails = [],
|
||||
}: LoginScreenProps) {
|
||||
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
||||
|
||||
const handleGoogleLogin = async () => {
|
||||
setIsLoggingIn(true);
|
||||
try {
|
||||
await onLogin();
|
||||
} finally {
|
||||
setIsLoggingIn(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen w-full flex items-center justify-center p-4 md:p-8 bg-[#f5f3f3]" id="login-screen-bg">
|
||||
{/* Main Login Card container */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
||||
className="w-full max-w-[480px] bg-white rounded-xl p-8 md:p-12 shadow-lg flex flex-col items-center"
|
||||
id="login-card"
|
||||
>
|
||||
<div className="w-full flex flex-col items-center text-center space-y-8 py-4">
|
||||
|
||||
{/* Company Brand Logo */}
|
||||
<div className="w-full flex justify-center pb-2" id="login-brand-logo-container">
|
||||
<img
|
||||
alt="GLM Logo"
|
||||
className="h-auto max-w-[250px]"
|
||||
src={IMAGES.loginLogo}
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div className="space-y-1 w-full">
|
||||
<h1 className="text-[32px] leading-[1.2] font-bold text-[#4f758b]" id="login-h1-title">
|
||||
Cruce de Cuentas GLM
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Google Button */}
|
||||
<div className="w-full pt-4 pb-2 space-y-3">
|
||||
<button
|
||||
onClick={handleGoogleLogin}
|
||||
disabled={isLoggingIn || !isSupabaseConfigured}
|
||||
className="w-full flex items-center justify-center gap-3 bg-[#6CC24A] hover:bg-[#5bb03a] text-white py-3 px-4 transition-all text-sm font-semibold rounded-lg focus:outline-none focus:ring-2 focus:ring-[#6CC24A] focus:ring-offset-2 cursor-pointer shadow-none disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
id="google-signin-btn"
|
||||
>
|
||||
{isLoggingIn ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 text-white animate-spin" />
|
||||
<span>Iniciando sesión...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Reference G white Logo */}
|
||||
<svg className="w-5 h-5 shrink-0" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
fill="#FFFFFF"
|
||||
/>
|
||||
<path
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
fill="#FFFFFF"
|
||||
/>
|
||||
<path
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
fill="#FFFFFF"
|
||||
/>
|
||||
<path
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
fill="#FFFFFF"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-[16px] font-bold text-white">Continuar con Google</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{authError ? (
|
||||
<p className="text-[12px] leading-[1.45] text-red-600 bg-red-50 border border-red-100 rounded-lg p-3">
|
||||
{authError}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Small text footer */}
|
||||
<div className="w-full pt-4 border-t border-[#D0D0D0] text-center" id="login-footer">
|
||||
<p className="text-[13px] leading-[1.4] text-slate-500 font-normal">
|
||||
Acceso restringido. Uso exclusivo para personal autorizado de GLM.
|
||||
</p>
|
||||
{allowedEmails.length > 0 ? (
|
||||
<p className="text-[11px] leading-[1.4] text-slate-400 font-normal mt-2">
|
||||
Acceso habilitado solo para usuarios autorizados.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import { X, ShieldAlert, ArrowLeft, Layers, Landmark, Info, CheckCircle2, ChevronRight } from "lucide-react";
|
||||
import { Country } from "../data";
|
||||
|
||||
interface NotificationModalProps {
|
||||
country: Country | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function NotificationModal({ country, onClose }: NotificationModalProps) {
|
||||
if (!country) return null;
|
||||
|
||||
const isGuatemala = country.code === "GT";
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
{/* Backdrop overlay */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={onClose}
|
||||
className="fixed inset-0 bg-slate-900/60 backdrop-blur-xs"
|
||||
/>
|
||||
|
||||
{/* Modal Content Card */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
transition={{ type: "spring", duration: 0.4 }}
|
||||
className={`relative w-full max-w-lg bg-white rounded-xl shadow-2xl overflow-hidden border-t-4 ${isGuatemala ? 'border-[#6CC24A]' : 'border-slate-300'}`}
|
||||
id={`modal-${country.code.toLowerCase()}`}
|
||||
>
|
||||
{/* Close button top right */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-4 right-4 p-1.5 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-full transition-colors focus:outline-none"
|
||||
title="Cerrar"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{/* Header Banner info */}
|
||||
<div className="bg-slate-50 p-6 border-b border-slate-100 flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-lg overflow-hidden shrink-0 border border-slate-200">
|
||||
<img
|
||||
src={country.flagUrl}
|
||||
alt={`Bandera de ${country.name}`}
|
||||
className="w-full h-full object-cover"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span className={`text-[10px] font-bold tracking-wider px-2.5 py-1 rounded-full uppercase ${isGuatemala ? 'text-[#6CC24A] bg-[#6CC24A]/10' : 'text-slate-500 bg-slate-100'}`}>
|
||||
{isGuatemala ? 'Iniciado' : 'No Iniciado'}
|
||||
</span>
|
||||
<h3 className="text-xl font-bold text-slate-800 leading-tight mt-1">
|
||||
{country.name} ({country.code})
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-6">
|
||||
{isGuatemala ? (
|
||||
// Guatemala Particular Content
|
||||
<div className="space-y-6">
|
||||
<div className="flex gap-3 bg-emerald-50/50 rounded-lg p-4 border border-emerald-100">
|
||||
<CheckCircle2 className="w-6 h-6 text-[#6CC24A] shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-semibold text-slate-800 text-sm">
|
||||
Módulo en Operación Inicial
|
||||
</h4>
|
||||
<p className="text-xs text-slate-600 mt-1 leading-relaxed">
|
||||
El sistema para Guatemala ha comenzado su fase técnica y de parametrización operativa.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h5 className="text-xs font-bold uppercase tracking-wider text-slate-400">
|
||||
Siguientes pasos en desarrollo:
|
||||
</h5>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
<div className="flex items-center gap-3 p-3.5 bg-yellow-50/40 border border-yellow-200 rounded-lg">
|
||||
<div className="p-2 bg-yellow-100/50 text-amber-700 rounded-md">
|
||||
<Layers className="w-5 h-5 animate-pulse" />
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="text-xs font-bold text-slate-800">Falta el estándar de nómina</h6>
|
||||
<p className="text-[11px] text-slate-600 mt-0.5">Adaptación de los campos reglamentarios locales.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 p-3.5 bg-yellow-50/40 border border-yellow-200 rounded-lg">
|
||||
<div className="p-2 bg-yellow-100/50 text-amber-700 rounded-md">
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="text-xs font-bold text-slate-800">Integrar al flujo de n8n</h6>
|
||||
<p className="text-[11px] text-slate-600 mt-0.5">Conexión de webhooks y procesamiento de conciliación en lote.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// Pending / No Iniciado Content for other countries
|
||||
<div className="space-y-4 py-4 text-center">
|
||||
<div className="w-16 h-16 bg-slate-50 border border-slate-200 rounded-full flex items-center justify-center mx-auto text-slate-400">
|
||||
<ShieldAlert className="w-8 h-8" />
|
||||
</div>
|
||||
<div className="space-y-1 max-w-sm mx-auto">
|
||||
<h4 className="font-bold text-slate-800 text-base">
|
||||
Módulo No Iniciado
|
||||
</h4>
|
||||
<p className="text-xs text-slate-500 leading-relaxed">
|
||||
Aún no se ha iniciado el proceso operativo ni la conciliación para {country.name}.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action buttons footer */}
|
||||
<div className="bg-slate-50 px-6 py-4 border-t border-slate-100 flex justify-end gap-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex items-center gap-2 px-5 py-2.5 bg-[#4F758B] hover:bg-[#365C71] text-white text-xs font-semibold rounded-lg shadow-sm transition-all cursor-pointer"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" /> Entendido, Volver
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { useState } from "react";
|
||||
import { motion } from "motion/react";
|
||||
import { LogOut, User } from "lucide-react";
|
||||
import { COUNTRIES, Country, IMAGES } from "../data";
|
||||
import { CountryCard } from "./CountryCard";
|
||||
import { NotificationModal } from "./NotificationModal";
|
||||
|
||||
interface PortalPrincipalProps {
|
||||
onLogout: () => void;
|
||||
userEmail?: string;
|
||||
}
|
||||
|
||||
export function PortalPrincipal({ onLogout, userEmail }: PortalPrincipalProps) {
|
||||
const [selectedCountry, setSelectedCountry] = useState<Country | null>(null);
|
||||
|
||||
return (
|
||||
<div className="bg-[#fbf9f8] text-[#1b1c1c] font-sans min-h-screen flex flex-col" id="portal-root">
|
||||
|
||||
{/* Main Content Canvas */}
|
||||
<main className="flex-grow pb-16 px-4 md:px-12 py-8 max-w-7xl w-full mx-auto" id="portal-main">
|
||||
|
||||
{/* Top Control Bar with Logout and User Profile */}
|
||||
<div className="flex justify-end mb-8 gap-2" id="portal-controls">
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-[#4F758B] hover:bg-[#365C71] text-white transition-all text-xs font-semibold rounded-lg shadow-sm cursor-pointer border border-[#4F758B]/10"
|
||||
title="Cerrar sesión"
|
||||
id="logout-btn"
|
||||
>
|
||||
<LogOut className="w-4 h-4 text-white" />
|
||||
<span className="text-white">Cerrar sesión</span>
|
||||
</button>
|
||||
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center text-[#4F758B] border border-slate-300" title={userEmail ? `Sesión activa: ${userEmail}` : "Usuario de GomezLee Marketing"}>
|
||||
<User className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hero Section (Asymmetric) */}
|
||||
<section className="grid grid-cols-1 md:grid-cols-12 gap-8 my-16">
|
||||
<div className="md:col-span-8 flex flex-col justify-center">
|
||||
<h1 className="text-[48px] leading-[1.1] tracking-tight font-bold text-[#365c71] mb-4">
|
||||
Cruce de Cuentas GLM
|
||||
</h1>
|
||||
<p className="text-[16px] leading-[1.6] text-slate-600 max-w-2xl">
|
||||
Plataforma centralizada para la conciliación de cuentas a nivel regional. Seleccione un país para comenzar el proceso operativo.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-4 hidden md:flex items-center justify-end">
|
||||
<div className="w-full flex items-center justify-end">
|
||||
<img
|
||||
alt="GLM Logo"
|
||||
className="max-w-full h-auto object-contain"
|
||||
src={IMAGES.portalLogo}
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-1 md:col-span-12 border-t border-[#6CC24A] mt-8"></div>
|
||||
</section>
|
||||
|
||||
{/* Country Grid Section */}
|
||||
<section className="mb-24" id="portales-por-pais">
|
||||
<h2 className="text-[28px] md:text-[32px] leading-[1.2] font-bold text-[#365c71] mb-8">
|
||||
Portales por país
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{COUNTRIES.map((country) => (
|
||||
<CountryCard
|
||||
key={country.code}
|
||||
country={country}
|
||||
onSelect={setSelectedCountry}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-white border-t border-slate-200 py-6 text-center text-xs text-slate-400 font-medium">
|
||||
<p>© {new Date().getFullYear()} GomezLee Marketing. Todos los derechos reservados.</p>
|
||||
</footer>
|
||||
|
||||
{/* Interactive Country Modal */}
|
||||
<NotificationModal
|
||||
country={selectedCountry}
|
||||
onClose={() => setSelectedCountry(null)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user