style: fix theme background inconsistency and header layout flicker via UserContext

This commit is contained in:
Luis Gabriel Ramos Robles 2026-06-12 20:55:28 +00:00
parent 22358e0df9
commit cf575c4957
8 changed files with 139 additions and 102 deletions

View file

@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { LocaleProvider } from "@/lib/i18n/LocaleContext"; import { LocaleProvider } from "@/lib/i18n/LocaleContext";
import { UserProvider } from "@/lib/auth/UserContext";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@ -27,7 +28,9 @@ export default function RootLayout({
return ( return (
<html lang="en" className={`${geistSans.variable} ${geistMono.variable}`}> <html lang="en" className={`${geistSans.variable} ${geistMono.variable}`}>
<body> <body>
<LocaleProvider>{children}</LocaleProvider> <LocaleProvider>
<UserProvider>{children}</UserProvider>
</LocaleProvider>
</body> </body>
</html> </html>
); );

View file

@ -4,6 +4,7 @@ import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import Header from '@/components/Header'; import Header from '@/components/Header';
import styles from './page.module.css'; import styles from './page.module.css';
import { useUser } from '@/lib/auth/UserContext';
interface ValidationError { interface ValidationError {
row: number; row: number;
@ -55,40 +56,29 @@ export default function SalesImportPage() {
const [validationErrors, setValidationErrors] = useState<ValidationError[]>([]); const [validationErrors, setValidationErrors] = useState<ValidationError[]>([]);
const [generalError, setGeneralError] = useState<string | null>(null); const [generalError, setGeneralError] = useState<string | null>(null);
const [statusMessage, setStatusMessage] = useState(''); const [statusMessage, setStatusMessage] = useState('');
const [isLoading, setIsLoading] = useState(true); const { role, user: currentUser, isLoading: contextLoading } = useUser();
const [role, setRole] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
fetch('/api/auth/me') if (!contextLoading) {
.then((res) => { if (!currentUser) {
if (!res.ok) {
router.push('/login'); router.push('/login');
return null; return;
} }
return res.json();
})
.then((data) => {
if (data && data.user) {
setRole(data.user.role);
const allowed = ['admin', 'analyst', 'commercial_leader']; const allowed = ['admin', 'analyst', 'commercial_leader'];
if (!allowed.includes(data.user.role)) { if (!allowed.includes(role || '')) {
router.push('/unauthorized'); router.push('/unauthorized');
return; return;
} }
setIsLoading(false);
} }
}) }, [contextLoading, currentUser, role, router]);
.catch((err) => {
console.error('Failed to get session user details', err);
router.push('/login');
});
useEffect(() => {
// Generate unique idempotency key for this session/upload instance // Generate unique idempotency key for this session/upload instance
const key = 'key-' + Date.now() + '-' + Math.random().toString(36).substring(2, 9); const key = 'key-' + Date.now() + '-' + Math.random().toString(36).substring(2, 9);
setIdempotencyKey(key); setIdempotencyKey(key);
}, []); }, []);
if (isLoading) { if (contextLoading) {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<Header activeTab="import" /> <Header activeTab="import" />

View file

@ -1,8 +1,10 @@
.container { .container {
min-height: 100vh; min-height: 100vh;
background-color: #0f172a; background: radial-gradient(circle at top right, hsla(var(--primary-h), var(--primary-s), var(--primary-l), 0.08), transparent 45%),
color: #f8fafc; var(--background);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; color: var(--foreground);
font-family: var(--font-sans);
transition: background var(--transition-slow);
padding-bottom: 3rem; padding-bottom: 3rem;
} }

View file

@ -6,6 +6,7 @@ import styles from './page.module.css';
import Header from '@/components/Header'; import Header from '@/components/Header';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useLocale } from '@/lib/i18n/LocaleContext'; import { useLocale } from '@/lib/i18n/LocaleContext';
import { useUser } from '@/lib/auth/UserContext';
interface SimulatedItem { interface SimulatedItem {
main: { main: {
@ -43,9 +44,7 @@ interface User {
export default function SimulationPage() { export default function SimulationPage() {
const router = useRouter(); const router = useRouter();
const { locale, t } = useLocale(); const { locale, t } = useLocale();
const [role, setRole] = useState<string | null>(null); const { role, user: currentUser, isLoading: contextLoading } = useUser();
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Form State // Form State
const [period, setPeriod] = useState('2026-06'); const [period, setPeriod] = useState('2026-06');
@ -67,29 +66,22 @@ export default function SimulationPage() {
const [plansMap, setPlansMap] = useState<Record<number, any>>({}); const [plansMap, setPlansMap] = useState<Record<number, any>>({});
useEffect(() => { useEffect(() => {
fetch('/api/auth/me') if (!contextLoading) {
.then((res) => { if (!currentUser) {
if (!res.ok) {
router.push('/login'); router.push('/login');
return null; return;
} }
return res.json(); if (role !== 'admin' && role !== 'analyst') {
})
.then((data) => {
if (data && data.user) {
setRole(data.user.role);
setCurrentUser(data.user);
if (data.user.role !== 'admin' && data.user.role !== 'analyst') {
router.push('/unauthorized'); router.push('/unauthorized');
return; return;
} }
} }
setIsLoading(false); }, [contextLoading, currentUser, role, router]);
})
.catch((err) => { useEffect(() => {
console.error('Failed to fetch auth state:', err); if (contextLoading || !currentUser || (role !== 'admin' && role !== 'analyst')) {
router.push('/login'); return;
}); }
// Load users list for username mapping // Load users list for username mapping
fetch('/api/users') fetch('/api/users')
@ -162,7 +154,7 @@ export default function SimulationPage() {
} }
}; };
if (isLoading) { if (contextLoading) {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<Header activeTab="simulation" /> <Header activeTab="simulation" />

View file

@ -1,8 +1,10 @@
.container { .container {
min-height: 100vh; min-height: 100vh;
background-color: #0f172a; background: radial-gradient(circle at top right, hsla(var(--primary-h), var(--primary-s), var(--primary-l), 0.08), transparent 45%),
color: #f8fafc; var(--background);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; color: var(--foreground);
font-family: var(--font-sans);
transition: background var(--transition-slow);
padding-bottom: 3rem; padding-bottom: 3rem;
} }

View file

@ -6,6 +6,7 @@ import styles from './page.module.css';
import Header from '@/components/Header'; import Header from '@/components/Header';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useLocale } from '@/lib/i18n/LocaleContext'; import { useLocale } from '@/lib/i18n/LocaleContext';
import { useUser } from '@/lib/auth/UserContext';
interface Settlement { interface Settlement {
id: number; id: number;
@ -40,7 +41,7 @@ interface Settlement {
export default function ApprovalsPage() { export default function ApprovalsPage() {
const router = useRouter(); const router = useRouter();
const { t } = useLocale(); const { t } = useLocale();
const [role, setRole] = useState<string | null>(null); const { role, user: currentUser, isLoading: contextLoading } = useUser();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [settlements, setSettlements] = useState<Settlement[]>([]); const [settlements, setSettlements] = useState<Settlement[]>([]);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -71,35 +72,23 @@ export default function ApprovalsPage() {
}; };
useEffect(() => { useEffect(() => {
fetch('/api/auth/me') if (!contextLoading) {
.then((res) => { if (!currentUser) {
if (!res.ok) {
router.push('/login'); router.push('/login');
return null; return;
} }
return res.json();
})
.then((data) => {
if (data && data.user) {
setRole(data.user.role);
if ( if (
data.user.role !== 'commercial_leader' && role !== 'commercial_leader' &&
data.user.role !== 'admin' && role !== 'admin' &&
data.user.role !== 'analyst' && role !== 'analyst' &&
data.user.role !== 'director' role !== 'director'
) { ) {
router.push('/unauthorized'); router.push('/unauthorized');
return; return;
} else { }
fetchSettlements(); fetchSettlements();
} }
} }, [contextLoading, currentUser, role, router]);
})
.catch((err) => {
console.error('Failed to get session user details', err);
router.push('/login');
});
}, []);
const handleApprove = async (id: number) => { const handleApprove = async (id: number) => {
setError(null); setError(null);

View file

@ -5,6 +5,7 @@ import Link from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import styles from './Header.module.css'; import styles from './Header.module.css';
import { useLocale } from '@/lib/i18n/LocaleContext'; import { useLocale } from '@/lib/i18n/LocaleContext';
import { useUser } from '@/lib/auth/UserContext';
interface HeaderProps { interface HeaderProps {
activeTab: 'plans' | 'goals' | 'import' | 'simulation' | 'approvals' | 'none'; activeTab: 'plans' | 'goals' | 'import' | 'simulation' | 'approvals' | 'none';
@ -13,25 +14,12 @@ interface HeaderProps {
export default function Header({ activeTab }: HeaderProps) { export default function Header({ activeTab }: HeaderProps) {
const router = useRouter(); const router = useRouter();
const { locale, setLocale, t } = useLocale(); const { locale, setLocale, t } = useLocale();
const [role, setRole] = React.useState<string | null>(null); const { role, refreshUser } = useUser();
React.useEffect(() => {
fetch('/api/auth/me')
.then(res => {
if (res.ok) return res.json();
throw new Error('Not authenticated');
})
.then(data => {
setRole(data.user?.role || null);
})
.catch(err => {
console.error('Failed to get session user details', err);
});
}, []);
const handleLogout = async () => { const handleLogout = async () => {
try { try {
await fetch('/api/auth/logout', { method: 'POST' }); await fetch('/api/auth/logout', { method: 'POST' });
await refreshUser();
router.push('/login'); router.push('/login');
router.refresh(); router.refresh();
} catch (err) { } catch (err) {

View file

@ -0,0 +1,71 @@
'use client';
import React, { createContext, useContext, useState, useEffect } from 'react';
interface User {
id: number;
username: string;
email: string;
role: string;
area: string;
hotelId: number;
regionId: number;
}
interface UserContextProps {
user: User | null;
role: string | null;
isLoading: boolean;
refreshUser: () => Promise<void>;
}
const UserContext = createContext<UserContextProps | undefined>(undefined);
export function UserProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [role, setRole] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const fetchUser = async () => {
try {
const res = await fetch('/api/auth/me');
if (res.ok) {
const data = await res.json();
if (data?.user) {
setUser(data.user);
setRole(data.user.role);
} else {
setUser(null);
setRole(null);
}
} else {
setUser(null);
setRole(null);
}
} catch (err) {
console.error('Error fetching user context:', err);
setUser(null);
setRole(null);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchUser();
}, []);
return (
<UserContext.Provider value={{ user, role, isLoading, refreshUser: fetchUser }}>
{children}
</UserContext.Provider>
);
}
export function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
}