This commit is contained in:
Iliyan Angelov
2025-12-01 23:30:28 +02:00
parent f7d6f24e49
commit 86e78247c3
38 changed files with 3765 additions and 547 deletions

View File

@@ -1,18 +1,13 @@
import React from 'react';
import { Outlet, Link, useLocation, useNavigate } from 'react-router-dom';
import { Outlet, useNavigate } from 'react-router-dom';
import {
LayoutDashboard,
Sparkles,
LogOut,
Menu,
X,
User,
} from 'lucide-react';
import useAuthStore from '../store/useAuthStore';
import { useResponsive } from '../shared/hooks/useResponsive';
const HousekeepingLayout: React.FC = () => {
const { isMobile } = useResponsive();
const [sidebarOpen, setSidebarOpen] = React.useState(!isMobile);
const location = useLocation();
const navigate = useNavigate();
const { userInfo, logout } = useAuthStore();
@@ -25,96 +20,55 @@ const HousekeepingLayout: React.FC = () => {
}
};
const navigation = [
{ name: 'Dashboard', href: '/housekeeping/dashboard', icon: LayoutDashboard },
];
const isActive = (path: string) => location.pathname === path;
return (
<div className="flex h-screen bg-gradient-to-br from-slate-50 via-white to-slate-50">
{/* Mobile menu button */}
{isMobile && (
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
className="fixed top-4 left-4 z-50 p-2 bg-white rounded-lg shadow-lg"
>
{sidebarOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
)}
{/* Sidebar */}
<div
className={`
fixed lg:static inset-y-0 left-0 z-40
w-64 bg-white shadow-lg transform transition-transform duration-300 ease-in-out
${sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
`}
>
<div className="flex flex-col h-full">
{/* Logo/Brand */}
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<div className="flex items-center space-x-2">
<LayoutDashboard className="w-8 h-8 text-blue-600" />
<span className="text-xl font-bold text-gray-900">Housekeeping</span>
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-50 overflow-x-hidden">
{/* Luxury Top Navigation Bar */}
<header className="sticky top-0 z-50 bg-white/95 backdrop-blur-xl border-b border-[#d4af37]/20 shadow-sm">
<div className="w-full max-w-7xl mx-auto px-3 sm:px-4 md:px-6 lg:px-8">
<div className="flex items-center justify-between h-14 sm:h-16">
{/* Logo/Brand */}
<div className="flex items-center space-x-2 sm:space-x-3 min-w-0 flex-1">
<div className="relative flex-shrink-0">
<div className="absolute inset-0 bg-gradient-to-r from-[#d4af37] to-[#c9a227] rounded-lg blur-sm opacity-50"></div>
<div className="relative bg-gradient-to-r from-[#d4af37] to-[#c9a227] p-1.5 sm:p-2 rounded-lg">
<Sparkles className="w-4 h-4 sm:w-5 sm:h-5 md:w-6 md:h-6 text-white" />
</div>
</div>
<div className="min-w-0 flex-1">
<h1 className="text-sm sm:text-base md:text-lg lg:text-xl font-serif font-bold text-gray-900 tracking-tight truncate">
Enterprise Housekeeping
</h1>
<p className="hidden xs:block text-[10px] sm:text-xs text-gray-500 font-light truncate">Luxury Management System</p>
</div>
</div>
</div>
{/* Navigation */}
<nav className="flex-1 px-4 py-6 space-y-2">
{navigation.map((item) => {
const Icon = item.icon;
return (
<Link
key={item.name}
to={item.href}
onClick={() => isMobile && setSidebarOpen(false)}
className={`
flex items-center space-x-3 px-4 py-3 rounded-lg transition-colors
${isActive(item.href)
? 'bg-blue-50 text-blue-700 font-medium'
: 'text-gray-700 hover:bg-gray-50'
}
`}
>
<Icon className="w-5 h-5" />
<span>{item.name}</span>
</Link>
);
})}
</nav>
{/* User info and logout */}
<div className="p-4 border-t border-gray-200">
<div className="mb-3 px-4 py-2">
<p className="text-sm font-medium text-gray-900">{userInfo?.name || userInfo?.email || 'User'}</p>
<p className="text-xs text-gray-500 capitalize">{userInfo?.role || 'housekeeping'}</p>
{/* User Menu */}
<div className="flex items-center space-x-2 sm:space-x-3 flex-shrink-0">
<div className="hidden md:flex items-center space-x-2 lg:space-x-3 px-3 lg:px-4 py-1.5 lg:py-2 rounded-lg bg-gradient-to-r from-gray-50 to-gray-100/50 border border-gray-200/50">
<div className="w-7 h-7 lg:w-8 lg:h-8 rounded-full bg-gradient-to-br from-[#d4af37] to-[#c9a227] flex items-center justify-center flex-shrink-0">
<User className="w-3.5 h-3.5 lg:w-4 lg:h-4 text-white" />
</div>
<div className="text-left min-w-0 hidden lg:block">
<p className="text-xs lg:text-sm font-medium text-gray-900 truncate max-w-[120px]">{userInfo?.name || userInfo?.email || 'User'}</p>
<p className="text-[10px] lg:text-xs text-gray-500 capitalize truncate">{userInfo?.role || 'housekeeping'}</p>
</div>
</div>
<button
onClick={handleLogout}
className="flex items-center justify-center space-x-1 sm:space-x-2 px-2 sm:px-3 md:px-4 py-1.5 sm:py-2 text-gray-700 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-all duration-200 border border-transparent hover:border-gray-200 flex-shrink-0"
>
<LogOut className="w-4 h-4 flex-shrink-0" />
<span className="hidden sm:inline text-xs sm:text-sm font-medium">Logout</span>
</button>
</div>
<button
onClick={handleLogout}
className="w-full flex items-center space-x-3 px-4 py-2 text-gray-700 hover:bg-gray-50 rounded-lg transition-colors"
>
<LogOut className="w-5 h-5" />
<span>Logout</span>
</button>
</div>
</div>
</div>
{/* Overlay for mobile */}
{isMobile && sidebarOpen && (
<div
className="fixed inset-0 bg-black bg-opacity-50 z-30"
onClick={() => setSidebarOpen(false)}
/>
)}
</header>
{/* Main content */}
<div className="flex-1 overflow-auto lg:ml-0">
<div className={`min-h-screen ${isMobile ? 'pt-20' : 'pt-0'}`}>
<Outlet />
</div>
</div>
<main className="w-full overflow-x-hidden">
<Outlet />
</main>
</div>
);
};

View File

@@ -93,12 +93,29 @@ const AccountantDashboardPage: React.FC = () => {
totalPayments: response.data.payments.length,
pendingPayments: pendingPayments.length,
}));
} else {
// Clear data if response is not successful
setRecentPayments([]);
setFinancialSummary(prev => ({
...prev,
totalRevenue: 0,
totalPayments: 0,
pendingPayments: 0,
}));
}
} catch (err: any) {
// Handle AbortError silently
if (err.name === 'AbortError') {
return;
}
// Clear data when API connection fails
setRecentPayments([]);
setFinancialSummary(prev => ({
...prev,
totalRevenue: 0,
totalPayments: 0,
pendingPayments: 0,
}));
logger.error('Error fetching payments', err);
} finally {
setLoadingPayments(false);
@@ -139,12 +156,29 @@ const AccountantDashboardPage: React.FC = () => {
paidInvoices: paidInvoices.length,
overdueInvoices: overdueInvoices.length,
}));
} else {
// Clear data if response is not successful
setRecentInvoices([]);
setFinancialSummary(prev => ({
...prev,
totalInvoices: 0,
paidInvoices: 0,
overdueInvoices: 0,
}));
}
} catch (err: any) {
// Handle AbortError silently
if (err.name === 'AbortError') {
return;
}
// Clear data when API connection fails
setRecentInvoices([]);
setFinancialSummary(prev => ({
...prev,
totalInvoices: 0,
paidInvoices: 0,
overdueInvoices: 0,
}));
logger.error('Error fetching invoices', err);
} finally {
setLoadingInvoices(false);

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import {
FileText,
Search,
@@ -75,6 +75,10 @@ const AuditLogsPage: React.FC = () => {
if (error.name === 'AbortError') {
return;
}
// Clear data when API connection fails
setLogs([]);
setTotalPages(1);
setTotalItems(0);
logger.error('Error fetching audit logs', error);
toast.error(error.response?.data?.message || 'Unable to load audit logs');
} finally {

View File

@@ -89,12 +89,17 @@ const DashboardPage: React.FC = () => {
const response = await paymentService.getPayments({ page: 1, limit: 5 });
if (response.success && response.data?.payments) {
setRecentPayments(response.data.payments);
} else {
// Clear data if response is not successful
setRecentPayments([]);
}
} catch (err: any) {
// Handle AbortError silently
if (err.name === 'AbortError') {
return;
}
// Clear data when API connection fails
setRecentPayments([]);
logger.error('Error fetching payments', err);
} finally {
setLoadingPayments(false);
@@ -125,12 +130,17 @@ const DashboardPage: React.FC = () => {
const response = await sessionService.getMySessions();
if (response.success && response.data?.sessions) {
setSessions(response.data.sessions || []);
} else {
// Clear data if response is not successful
setSessions([]);
}
} catch (err: any) {
// Handle AbortError silently
if (err.name === 'AbortError') {
return;
}
// Clear data when API connection fails
setSessions([]);
logger.error('Error fetching sessions', err);
} finally {
setLoadingSessions(false);

View File

@@ -65,9 +65,14 @@ const DashboardPage: React.FC = () => {
const response = await paymentService.getPayments({ page: 1, limit: 5 });
if (response.success && response.data?.payments) {
setRecentPayments(response.data.payments);
} else {
// Clear data if response is not successful
setRecentPayments([]);
}
} catch (err: any) {
if (err.name !== 'AbortError') {
// Clear data when API connection fails
setRecentPayments([]);
logger.error('Error fetching payments', err);
}
} finally {
@@ -101,9 +106,14 @@ const DashboardPage: React.FC = () => {
const response = await sessionService.getMySessions();
if (response.success && response.data?.sessions) {
setSessions(response.data.sessions || []);
} else {
// Clear data if response is not successful
setSessions([]);
}
} catch (err: any) {
if (err.name !== 'AbortError') {
// Clear data when API connection fails
setSessions([]);
logger.error('Error fetching sessions', err);
}
} finally {

View File

@@ -80,6 +80,14 @@ const RoomListPage: React.FC = () => {
if (err.name === 'AbortError') {
return;
}
// Clear data when API connection fails
setRooms([]);
setPagination({
total: 0,
page: 1,
limit: 12,
totalPages: 0,
});
logger.error('Error fetching rooms', err);
setError('Unable to load room list. Please try again.');
} finally {

File diff suppressed because it is too large Load Diff

View File

@@ -218,8 +218,13 @@ const ChatManagementPage: React.FC = () => {
const response = await chatService.listChats();
if (response.success) {
setChats(response.data);
} else {
// Clear data if response is not successful
setChats([]);
}
} catch (error: any) {
// Clear data when API connection fails
setChats([]);
toast.error(error.response?.data?.detail || 'Failed to load chats');
} finally {
setLoading(false);
@@ -232,8 +237,13 @@ const ChatManagementPage: React.FC = () => {
const response = await chatService.getMessages(chatId);
if (response.success) {
setMessages(response.data);
} else {
// Clear data if response is not successful
setMessages([]);
}
} catch (error: any) {
// Clear data when API connection fails
setMessages([]);
toast.error(error.response?.data?.detail || 'Failed to load messages');
} finally {
setLoadingMessages(false);

View File

@@ -73,12 +73,17 @@ const StaffDashboardPage: React.FC = () => {
const response = await paymentService.getPayments({ page: 1, limit: 5 });
if (response.success && response.data?.payments) {
setRecentPayments(response.data.payments);
} else {
// Clear data if response is not successful
setRecentPayments([]);
}
} catch (err: any) {
// Handle AbortError silently
if (err.name === 'AbortError') {
return;
}
// Clear data when API connection fails
setRecentPayments([]);
logger.error('Error fetching payments', err);
} finally {
setLoadingPayments(false);
@@ -109,12 +114,17 @@ const StaffDashboardPage: React.FC = () => {
const response = await bookingService.getAllBookings({ page: 1, limit: 5 });
if ((response.status === 'success' || response.success) && response.data?.bookings) {
setRecentBookings(response.data.bookings);
} else {
// Clear data if response is not successful
setRecentBookings([]);
}
} catch (err: any) {
// Handle AbortError silently
if (err.name === 'AbortError') {
return;
}
// Clear data when API connection fails
setRecentBookings([]);
logger.error('Error fetching bookings', err);
} finally {
setLoadingBookings(false);