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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,6 +5,7 @@ import Link from 'next/link';
import { useRouter } from 'next/navigation';
import styles from './Header.module.css';
import { useLocale } from '@/lib/i18n/LocaleContext';
import { useUser } from '@/lib/auth/UserContext';
interface HeaderProps {
activeTab: 'plans' | 'goals' | 'import' | 'simulation' | 'approvals' | 'none';
@ -13,25 +14,12 @@ interface HeaderProps {
export default function Header({ activeTab }: HeaderProps) {
const router = useRouter();
const { locale, setLocale, t } = useLocale();
const [role, setRole] = React.useState<string | null>(null);
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 { role, refreshUser } = useUser();
const handleLogout = async () => {
try {
await fetch('/api/auth/logout', { method: 'POST' });
await refreshUser();
router.push('/login');
router.refresh();
} 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;
}