Initial commit

This commit is contained in:
Isaac_Aracena
2026-06-12 17:41:53 -04:00
commit a7dcd5cba9
20 changed files with 5176 additions and 0 deletions
+47
View File
@@ -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>
);
}
+121
View File
@@ -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>
);
}
+140
View File
@@ -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>
);
}
+94
View File
@@ -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>
);
}