diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 14fc3b1..c1ab183 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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 ( - {children} + + {children} + ); diff --git a/src/app/sales/import/page.tsx b/src/app/sales/import/page.tsx index fe13780..f307571 100644 --- a/src/app/sales/import/page.tsx +++ b/src/app/sales/import/page.tsx @@ -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([]); const [generalError, setGeneralError] = useState(null); const [statusMessage, setStatusMessage] = useState(''); - const [isLoading, setIsLoading] = useState(true); - const [role, setRole] = useState(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 (
diff --git a/src/app/sales/simulation/page.module.css b/src/app/sales/simulation/page.module.css index 8513580..b1ae60a 100644 --- a/src/app/sales/simulation/page.module.css +++ b/src/app/sales/simulation/page.module.css @@ -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; } diff --git a/src/app/sales/simulation/page.tsx b/src/app/sales/simulation/page.tsx index 60da8f5..b5ec957 100644 --- a/src/app/sales/simulation/page.tsx +++ b/src/app/sales/simulation/page.tsx @@ -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(null); - const [currentUser, setCurrentUser] = useState(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>({}); 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 (
diff --git a/src/app/settlements/approvals/page.module.css b/src/app/settlements/approvals/page.module.css index 5c5eb1b..11bdfcf 100644 --- a/src/app/settlements/approvals/page.module.css +++ b/src/app/settlements/approvals/page.module.css @@ -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; } diff --git a/src/app/settlements/approvals/page.tsx b/src/app/settlements/approvals/page.tsx index 0ec5f97..600bbd5 100644 --- a/src/app/settlements/approvals/page.tsx +++ b/src/app/settlements/approvals/page.tsx @@ -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(null); + const { role, user: currentUser, isLoading: contextLoading } = useUser(); const [isLoading, setIsLoading] = useState(true); const [settlements, setSettlements] = useState([]); const [error, setError] = useState(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); diff --git a/src/components/Header.tsx b/src/components/Header.tsx index edbd381..c909eb3 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -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(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) { diff --git a/src/lib/auth/UserContext.tsx b/src/lib/auth/UserContext.tsx new file mode 100644 index 0000000..6eb1422 --- /dev/null +++ b/src/lib/auth/UserContext.tsx @@ -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; +} + +const UserContext = createContext(undefined); + +export function UserProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(null); + const [role, setRole] = useState(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 ( + + {children} + + ); +} + +export function useUser() { + const context = useContext(UserContext); + if (!context) { + throw new Error('useUser must be used within a UserProvider'); + } + return context; +}