style: fix theme background inconsistency and header layout flicker via UserContext
This commit is contained in:
parent
22358e0df9
commit
cf575c4957
8 changed files with 139 additions and 102 deletions
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
71
src/lib/auth/UserContext.tsx
Normal file
71
src/lib/auth/UserContext.tsx
Normal 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;
|
||||
}
|
||||
Loading…
Reference in a new issue