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 "./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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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');
|
|
||||||
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);
|
|
||||||
router.push('/login');
|
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
|
// 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" />
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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');
|
|
||||||
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);
|
|
||||||
router.push('/login');
|
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
|
// 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" />
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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');
|
|
||||||
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);
|
|
||||||
router.push('/login');
|
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) => {
|
const handleApprove = async (id: number) => {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
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