diff --git a/Frontend/src/pages/HomePage.tsx b/Frontend/src/pages/HomePage.tsx
index 4aadb957..723abf87 100644
--- a/Frontend/src/pages/HomePage.tsx
+++ b/Frontend/src/pages/HomePage.tsx
@@ -14,19 +14,23 @@ import {
} from '../components/rooms';
import {
bannerService,
- roomService
+ roomService,
+ pageContentService
} from '../services/api';
import type { Banner } from '../services/api/bannerService';
import type { Room } from '../services/api/roomService';
+import type { PageContent } from '../services/api/pageContentService';
const HomePage: React.FC = () => {
const [banners, setBanners] = useState([]);
const [featuredRooms, setFeaturedRooms] = useState([]);
const [newestRooms, setNewestRooms] = useState([]);
+ const [pageContent, setPageContent] = useState(null);
const [isLoadingBanners, setIsLoadingBanners] =
useState(true);
const [isLoadingRooms, setIsLoadingRooms] = useState(true);
const [isLoadingNewest, setIsLoadingNewest] = useState(true);
+ const [isLoadingContent, setIsLoadingContent] = useState(true);
const [error, setError] = useState(null);
// Combine featured and newest rooms, removing duplicates
@@ -48,6 +52,40 @@ const HomePage: React.FC = () => {
return Array.from(roomMap.values());
}, [featuredRooms, newestRooms]);
+ // Fetch page content
+ useEffect(() => {
+ const fetchPageContent = async () => {
+ try {
+ setIsLoadingContent(true);
+ const response = await pageContentService.getPageContent('home');
+ if (response.status === 'success' && response.data?.page_content) {
+ setPageContent(response.data.page_content);
+
+ // Update document title and meta tags
+ if (response.data.page_content.meta_title) {
+ document.title = response.data.page_content.meta_title;
+ }
+ if (response.data.page_content.meta_description) {
+ let metaDescription = document.querySelector('meta[name="description"]');
+ if (!metaDescription) {
+ metaDescription = document.createElement('meta');
+ metaDescription.setAttribute('name', 'description');
+ document.head.appendChild(metaDescription);
+ }
+ metaDescription.setAttribute('content', response.data.page_content.meta_description);
+ }
+ }
+ } catch (err: any) {
+ console.error('Error fetching page content:', err);
+ // Silently fail - use default content
+ } finally {
+ setIsLoadingContent(false);
+ }
+ };
+
+ fetchPageContent();
+ }, []);
+
// Fetch banners
useEffect(() => {
const fetchBanners = async () => {
@@ -184,10 +222,10 @@ const HomePage: React.FC = () => {
{/* Section Header - Centered */}
- Featured & Newest Rooms
+ {pageContent?.hero_title || 'Featured & Newest Rooms'}
- Discover our most popular accommodations and latest additions
+ {pageContent?.hero_subtitle || pageContent?.description || 'Discover our most popular accommodations and latest additions'}
{/* View All Rooms Button - Golden, Centered */}
diff --git a/Frontend/src/pages/admin/AnalyticsDashboardPage.tsx b/Frontend/src/pages/admin/AnalyticsDashboardPage.tsx
new file mode 100644
index 00000000..c40870e5
--- /dev/null
+++ b/Frontend/src/pages/admin/AnalyticsDashboardPage.tsx
@@ -0,0 +1,1186 @@
+import React, { useState, useEffect } from 'react';
+import {
+ BarChart3,
+ FileText,
+ Calendar,
+ Users,
+ Hotel,
+ TrendingUp,
+ Download,
+ Filter,
+ PieChart,
+ Search,
+ Eye,
+ Activity,
+ AlertCircle,
+ CheckCircle,
+ XCircle,
+ Info,
+ Sparkles,
+ ClipboardList,
+ X,
+ ChevronRight,
+ Star
+} from 'lucide-react';
+import { toast } from 'react-toastify';
+import { Loading, EmptyState } from '../../components/common';
+import Pagination from '../../components/common/Pagination';
+import CurrencyIcon from '../../components/common/CurrencyIcon';
+import { useAsync } from '../../hooks/useAsync';
+import { reportService, ReportData, reviewService, Review } from '../../services/api';
+import { auditService, AuditLog, AuditLogFilters } from '../../services/api/auditService';
+import { formatDate } from '../../utils/format';
+import { useFormatCurrency } from '../../hooks/useFormatCurrency';
+
+type AnalyticsTab = 'overview' | 'reports' | 'audit-logs' | 'reviews';
+
+const AnalyticsDashboardPage: React.FC = () => {
+ const { formatCurrency } = useFormatCurrency();
+ const [activeTab, setActiveTab] = useState
('overview');
+
+ // Reports State
+ const [dateRange, setDateRange] = useState({
+ from: '',
+ to: '',
+ });
+ const [reportType, setReportType] = useState<'daily' | 'weekly' | 'monthly' | 'yearly' | ''>('');
+
+ // Audit Logs State
+ const [logs, setLogs] = useState([]);
+ const [auditLoading, setAuditLoading] = useState(true);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [totalPages, setTotalPages] = useState(1);
+ const [totalItems, setTotalItems] = useState(0);
+ const [selectedLog, setSelectedLog] = useState(null);
+ const [showDetails, setShowDetails] = useState(false);
+ const itemsPerPage = 20;
+
+ const [auditFilters, setAuditFilters] = useState({
+ page: 1,
+ limit: itemsPerPage,
+ });
+
+ // Reviews State
+ const [reviews, setReviews] = useState([]);
+ const [reviewsLoading, setReviewsLoading] = useState(true);
+ const [reviewsFilters, setReviewsFilters] = useState({
+ status: '',
+ });
+ const [reviewsCurrentPage, setReviewsCurrentPage] = useState(1);
+ const [reviewsTotalPages, setReviewsTotalPages] = useState(1);
+ const [reviewsTotalItems, setReviewsTotalItems] = useState(0);
+ const reviewsPerPage = 5;
+
+ const fetchReports = async (): Promise => {
+ const params: any = {};
+ if (dateRange.from) params.from = dateRange.from;
+ if (dateRange.to) params.to = dateRange.to;
+ if (reportType) params.type = reportType;
+
+ const response = await reportService.getReports(params);
+ return response.data;
+ };
+
+ const {
+ data: reportData,
+ loading: reportsLoading,
+ error: reportsError,
+ execute: refetchReports
+ } = useAsync(fetchReports, {
+ immediate: true,
+ onError: (error: any) => {
+ toast.error(error.message || 'Unable to load reports');
+ }
+ });
+
+ useEffect(() => {
+ if (activeTab === 'audit-logs') {
+ fetchLogs();
+ } else if (activeTab === 'reviews') {
+ fetchReviews();
+ }
+ }, [activeTab, auditFilters, currentPage, reviewsFilters, reviewsCurrentPage]);
+
+ useEffect(() => {
+ setAuditFilters(prev => ({ ...prev, page: currentPage }));
+ }, [currentPage]);
+
+ useEffect(() => {
+ if (activeTab === 'reviews') {
+ setReviewsCurrentPage(1);
+ }
+ }, [reviewsFilters, activeTab]);
+
+ const fetchLogs = async () => {
+ try {
+ setAuditLoading(true);
+ const response = await auditService.getAuditLogs({
+ ...auditFilters,
+ page: currentPage,
+ limit: itemsPerPage,
+ });
+ setLogs(response.data.logs);
+ if (response.data.pagination) {
+ setTotalPages(response.data.pagination.totalPages);
+ setTotalItems(response.data.pagination.total);
+ }
+ } catch (error: any) {
+ console.error('Error fetching audit logs:', error);
+ toast.error(error.response?.data?.message || 'Unable to load audit logs');
+ } finally {
+ setAuditLoading(false);
+ }
+ };
+
+ const handleExport = async () => {
+ try {
+ const params: any = {};
+ if (dateRange.from) params.from = dateRange.from;
+ if (dateRange.to) params.to = dateRange.to;
+ if (reportType) params.type = reportType;
+
+ const blob = await reportService.exportReport(params);
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `report-${new Date().toISOString().split('T')[0]}.csv`;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+ document.body.removeChild(a);
+ toast.success('Report exported successfully');
+ } catch (error: any) {
+ toast.error(error.message || 'Failed to export report');
+ }
+ };
+
+ const handleFilter = () => {
+ refetchReports();
+ };
+
+ const handleAuditFilterChange = (key: keyof AuditLogFilters, value: any) => {
+ setAuditFilters(prev => ({ ...prev, [key]: value }));
+ setCurrentPage(1);
+ };
+
+ const handleSearch = (searchTerm: string) => {
+ handleAuditFilterChange('search', searchTerm || undefined);
+ };
+
+ const handleViewDetails = (log: AuditLog) => {
+ setSelectedLog(log);
+ setShowDetails(true);
+ };
+
+ const getStatusIcon = (status: string) => {
+ switch (status) {
+ case 'success':
+ return ;
+ case 'failed':
+ return ;
+ case 'error':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const getStatusBadge = (status: string) => {
+ const baseClasses = "px-3 py-1 rounded-full text-xs font-semibold";
+ switch (status) {
+ case 'success':
+ return `${baseClasses} bg-emerald-100 text-emerald-800 border border-emerald-200`;
+ case 'failed':
+ return `${baseClasses} bg-red-100 text-red-800 border border-red-200`;
+ case 'error':
+ return `${baseClasses} bg-amber-100 text-amber-800 border border-amber-200`;
+ default:
+ return `${baseClasses} bg-gray-100 text-gray-800 border border-gray-200`;
+ }
+ };
+
+ // Reviews Functions
+ const fetchReviews = async () => {
+ try {
+ setReviewsLoading(true);
+ const response = await reviewService.getReviews({
+ ...reviewsFilters,
+ page: reviewsCurrentPage,
+ limit: reviewsPerPage,
+ });
+ setReviews(response.data.reviews);
+ if (response.data.pagination) {
+ setReviewsTotalPages(response.data.pagination.totalPages);
+ setReviewsTotalItems(response.data.pagination.total);
+ }
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to load reviews list');
+ } finally {
+ setReviewsLoading(false);
+ }
+ };
+
+ const handleApproveReview = async (id: number) => {
+ try {
+ await reviewService.approveReview(id);
+ toast.success('Review approved successfully');
+ fetchReviews();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to approve review');
+ }
+ };
+
+ const handleRejectReview = async (id: number) => {
+ if (!window.confirm('Are you sure you want to reject this review?')) return;
+
+ try {
+ await reviewService.rejectReview(id);
+ toast.success('Review rejected successfully');
+ fetchReviews();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to reject review');
+ }
+ };
+
+ const getReviewStatusBadge = (status: string) => {
+ const badges: Record = {
+ pending: { bg: 'bg-gradient-to-r from-amber-50 to-yellow-50', text: 'text-amber-800', label: 'Pending', border: 'border-amber-200' },
+ approved: { bg: 'bg-gradient-to-r from-emerald-50 to-green-50', text: 'text-emerald-800', label: 'Approved', border: 'border-emerald-200' },
+ rejected: { bg: 'bg-gradient-to-r from-rose-50 to-red-50', text: 'text-rose-800', label: 'Rejected', border: 'border-rose-200' },
+ };
+ const badge = badges[status] || badges.pending;
+ return (
+
+ {badge.label}
+
+ );
+ };
+
+ const renderStars = (rating: number) => {
+ return (
+
+ {[1, 2, 3, 4, 5].map((star) => (
+
+ ))}
+
+ );
+ };
+
+ const tabs = [
+ { id: 'overview' as AnalyticsTab, label: 'Overview', icon: BarChart3 },
+ { id: 'reports' as AnalyticsTab, label: 'Reports', icon: FileText },
+ { id: 'audit-logs' as AnalyticsTab, label: 'Audit Logs', icon: ClipboardList },
+ { id: 'reviews' as AnalyticsTab, label: 'Reviews', icon: Star },
+ ];
+
+ return (
+
+
+ {/* Luxury Header */}
+
+
+
+
+
+
+
+
+
+ Analytics Dashboard
+
+
+
+
+ Comprehensive insights, reports, and system activity tracking
+
+
+
+
+
+ {/* Premium Tab Navigation */}
+
+
+ {tabs.map((tab) => {
+ const Icon = tab.icon;
+ const isActive = activeTab === tab.id;
+ return (
+
+ );
+ })}
+
+
+
+
+
+ {/* Overview Tab */}
+ {activeTab === 'overview' && (
+
+
setActiveTab('reports')}
+ className="group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-blue-100/50 p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:border-blue-300/60 overflow-hidden"
+ >
+
+
+
+
+
+
+
Reports & Analytics
+
+
+
+
+
+ View comprehensive reports, revenue analytics, and booking statistics
+
+
+
+
+
+
+
setActiveTab('audit-logs')}
+ className="group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-indigo-100/50 p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:border-indigo-300/60 overflow-hidden"
+ >
+
+
+
+
+ Track system activity, user actions, and security events
+
+
+
+
+
+
+
setActiveTab('reviews')}
+ className="group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-amber-100/50 p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:border-amber-300/60 overflow-hidden"
+ >
+
+
+
+
+ Manage customer reviews and ratings
+
+
+
+
+
+
+ )}
+
+ {/* Reports Tab */}
+ {activeTab === 'reports' && (
+
+ {reportsLoading && !reportData ? (
+
+ ) : reportsError && !reportData ? (
+
+
+
+ ) : (
+ <>
+ {/* Section Header */}
+
+
+
+
+
+
+
+
Reports & Analytics
+
+
+ View comprehensive reports and statistics for bookings, revenue, and performance
+
+
+
+
+
+
+ {/* Filters */}
+
+
+
+
+
+ setDateRange({ ...dateRange, from: e.target.value })}
+ className="w-full px-4 py-3 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition-all duration-200"
+ />
+
+
+
+ setDateRange({ ...dateRange, to: e.target.value })}
+ className="w-full px-4 py-3 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition-all duration-200"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ {reportData && (
+ <>
+ {/* Summary Cards */}
+
+ {[
+ { icon: Calendar, label: 'Total Bookings', value: reportData.total_bookings || 0, color: 'blue', border: 'border-blue-500' },
+ { icon: CurrencyIcon, label: 'Total Revenue', value: formatCurrency(reportData.total_revenue || 0), color: 'emerald', border: 'border-emerald-500' },
+ { icon: Users, label: 'Total Customers', value: reportData.total_customers || 0, color: 'purple', border: 'border-purple-500' },
+ { icon: Hotel, label: 'Available Rooms', value: reportData.available_rooms || 0, color: 'orange', border: 'border-orange-500', subtitle: `${reportData.occupied_rooms || 0} occupied` },
+ ].map(({ icon: Icon, label, value, color, border, subtitle }) => {
+ const colorClasses = {
+ blue: { bg: 'bg-blue-100', text: 'text-blue-600', gradient: 'from-blue-500 to-blue-600' },
+ emerald: { bg: 'bg-emerald-100', text: 'text-emerald-600', gradient: 'from-emerald-500 to-emerald-600' },
+ purple: { bg: 'bg-purple-100', text: 'text-purple-600', gradient: 'from-purple-500 to-purple-600' },
+ orange: { bg: 'bg-orange-100', text: 'text-orange-600', gradient: 'from-orange-500 to-orange-600' },
+ }[color];
+
+ return (
+
+
+
+ {Icon === CurrencyIcon ? (
+
+ ) : (
+
+ )}
+
+
+
+
{label}
+
+ {value}
+
+ {subtitle && (
+
{subtitle}
+ )}
+
+ );
+ })}
+
+
+ {/* Bookings by Status */}
+ {reportData.bookings_by_status && (
+
+
+
+ {Object.entries(reportData.bookings_by_status).map(([status, count]) => (
+
+
{count}
+
{status.replace('_', ' ')}
+
+ ))}
+
+
+ )}
+
+ {/* Revenue by Date */}
+ {reportData.revenue_by_date && reportData.revenue_by_date.length > 0 && (
+
+
+
+
+
+
Revenue by Date
+
+
+
+
+
+ | Date |
+ Bookings |
+ Revenue |
+
+
+
+ {reportData.revenue_by_date.map((item, index) => (
+
+ |
+ {formatDate(new Date(item.date), 'short')}
+ |
+ {item.bookings} |
+ {formatCurrency(item.revenue)} |
+
+ ))}
+
+
+
+
+ )}
+
+ {/* Top Rooms */}
+ {reportData.top_rooms && reportData.top_rooms.length > 0 && (
+
+
Top Performing Rooms
+
+
+
+
+ | Room Number |
+ Bookings |
+ Revenue |
+
+
+
+ {reportData.top_rooms.map((room) => (
+
+ | {room.room_number} |
+ {room.bookings} |
+ {formatCurrency(room.revenue)} |
+
+ ))}
+
+
+
+
+ )}
+
+ {/* Service Usage */}
+ {reportData.service_usage && reportData.service_usage.length > 0 && (
+
+
Service Usage
+
+
+
+
+ | Service Name |
+ Usage Count |
+ Total Revenue |
+
+
+
+ {reportData.service_usage.map((service) => (
+
+ | {service.service_name} |
+ {service.usage_count} |
+ {formatCurrency(service.total_revenue)} |
+
+ ))}
+
+
+
+
+ )}
+ >
+ )}
+ >
+ )}
+
+ )}
+
+ {/* Audit Logs Tab */}
+ {activeTab === 'audit-logs' && (
+
+ {/* Section Header */}
+
+
+
+
+ View all system activity, user actions, and security events in real-time
+
+
+
+
+ {/* Filters */}
+
+
+
+
+
+
+
+ handleSearch(e.target.value)}
+ className="w-full pl-10 pr-4 py-3 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 transition-all duration-200"
+ />
+
+
+
+
+ handleAuditFilterChange('action', e.target.value || undefined)}
+ className="w-full px-4 py-3 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 transition-all duration-200"
+ />
+
+
+
+ handleAuditFilterChange('resource_type', e.target.value || undefined)}
+ className="w-full px-4 py-3 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 transition-all duration-200"
+ />
+
+
+
+
+
+
+
+ handleAuditFilterChange('start_date', e.target.value || undefined)}
+ className="w-full px-4 py-3 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 transition-all duration-200"
+ />
+
+
+
+ handleAuditFilterChange('end_date', e.target.value || undefined)}
+ className="w-full px-4 py-3 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 transition-all duration-200"
+ />
+
+
+
+
+ {/* Stats Summary */}
+
+
+
+
+
Total Logs
+
{totalItems}
+
+
+
+
+
+
+
+
+
+
Current Page
+
{currentPage}
+
+
+
+
+
+
+ {/* Logs Table */}
+ {auditLoading && logs.length === 0 ? (
+
+ ) : logs.length === 0 ? (
+
+
+
+ ) : (
+ <>
+
+
+
+
+
+ | ID |
+ Action |
+ Resource |
+ User |
+ Status |
+ IP Address |
+ Date |
+ Actions |
+
+
+
+ {logs.map((log) => (
+
+ | #{log.id} |
+ {log.action} |
+
+ {log.resource_type}
+ {log.resource_id && (
+ ID: {log.resource_id}
+ )}
+ |
+
+ {log.user ? (
+
+ {log.user.full_name}
+ {log.user.email}
+
+ ) : (
+ System
+ )}
+ |
+
+
+ {getStatusIcon(log.status)}
+ {log.status}
+
+ |
+ {log.ip_address || '-'} |
+ {formatDate(log.created_at)} |
+
+
+ |
+
+ ))}
+
+
+
+
+
+ {/* Pagination */}
+ {totalPages > 1 && (
+
+ )}
+ >
+ )}
+
+ )}
+
+ {/* Reviews Tab */}
+ {activeTab === 'reviews' && (
+
+ {/* Section Header */}
+
+
+
+
+
+
+
Review Management
+
+
+ Approve and manage customer reviews and ratings
+
+
+
+
+ {/* Filters */}
+
+
+
+
+
+
+
+
+
+
+ {reviewsTotalItems} review{reviewsTotalItems !== 1 ? 's' : ''}
+
+
+
+
+
+
+ {/* Reviews Table */}
+ {reviewsLoading && reviews.length === 0 ? (
+
+ ) : reviews.length === 0 ? (
+
+
+
+ ) : (
+ <>
+
+
+
+
+
+ | User |
+ Room |
+ Rating |
+ Comment |
+ Created Date |
+ Status |
+ Actions |
+
+
+
+ {reviews.map((review) => (
+
+ |
+ {review.user?.name}
+ {review.user?.email && (
+ {review.user.email}
+ )}
+ |
+
+
+ Room {review.room?.room_number}
+
+ {review.room?.room_type?.name && (
+ {review.room.room_type.name}
+ )}
+ |
+
+
+ {renderStars(review.rating)}
+ ({review.rating})
+
+ |
+
+ {review.comment}
+ |
+
+
+ {new Date(review.created_at).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric'
+ })}
+
+ |
+
+ {getReviewStatusBadge(review.status)}
+ |
+
+ {review.status === 'pending' && (
+
+
+
+
+ )}
+ {review.status !== 'pending' && (
+ No actions available
+ )}
+ |
+
+ ))}
+
+
+
+
+
+ {/* Pagination */}
+ {reviewsTotalPages > 1 && (
+
+ )}
+ >
+ )}
+
+ )}
+
+ {/* Details Modal */}
+ {showDetails && selectedLog && (
+
+
+
+
+
Audit Log Details
+
+
+
+
+
+
+
+
#{selectedLog.id}
+
+
+
+
+ {getStatusIcon(selectedLog.status)}
+ {selectedLog.status}
+
+
+
+
+
{selectedLog.action}
+
+
+
+
{selectedLog.resource_type}
+
+ {selectedLog.resource_id && (
+
+
+
{selectedLog.resource_id}
+
+ )}
+
+
+
{formatDate(selectedLog.created_at)}
+
+ {selectedLog.user && (
+ <>
+
+
+
{selectedLog.user.full_name}
+
{selectedLog.user.email}
+
+
+
+
{selectedLog.user_id}
+
+ >
+ )}
+ {selectedLog.ip_address && (
+
+
+
{selectedLog.ip_address}
+
+ )}
+ {selectedLog.request_id && (
+
+
+
{selectedLog.request_id}
+
+ )}
+
+
+ {selectedLog.user_agent && (
+
+
+
{selectedLog.user_agent}
+
+ )}
+
+ {selectedLog.error_message && (
+
+
+
{selectedLog.error_message}
+
+ )}
+
+ {selectedLog.details && (
+
+
+
+ {JSON.stringify(selectedLog.details, null, 2)}
+
+
+ )}
+
+
+
+
+
+
+
+
+ )}
+
+
+ );
+};
+
+export default AnalyticsDashboardPage;
+
diff --git a/Frontend/src/pages/admin/BusinessDashboardPage.tsx b/Frontend/src/pages/admin/BusinessDashboardPage.tsx
new file mode 100644
index 00000000..337158a1
--- /dev/null
+++ b/Frontend/src/pages/admin/BusinessDashboardPage.tsx
@@ -0,0 +1,1179 @@
+import React, { useEffect, useState } from 'react';
+import {
+ FileText,
+ CreditCard,
+ Tag,
+ Search,
+ Plus,
+ Edit,
+ Trash2,
+ Eye,
+ Filter,
+ Sparkles,
+ ChevronRight,
+ X,
+ Download
+} from 'lucide-react';
+import { toast } from 'react-toastify';
+import { Loading, EmptyState } from '../../components/common';
+import Pagination from '../../components/common/Pagination';
+import { useFormatCurrency } from '../../hooks/useFormatCurrency';
+import { useCurrency } from '../../contexts/CurrencyContext';
+import { useNavigate } from 'react-router-dom';
+import { invoiceService, Invoice } from '../../services/api';
+import { paymentService, Payment } from '../../services/api';
+import { promotionService, Promotion } from '../../services/api';
+import { formatDate } from '../../utils/format';
+
+type BusinessTab = 'overview' | 'invoices' | 'payments' | 'promotions';
+
+const BusinessDashboardPage: React.FC = () => {
+ const { formatCurrency } = useFormatCurrency();
+ const { currency } = useCurrency();
+ const navigate = useNavigate();
+ const [activeTab, setActiveTab] = useState('overview');
+
+ // Invoices State
+ const [invoices, setInvoices] = useState([]);
+ const [invoicesLoading, setInvoicesLoading] = useState(true);
+ const [invoiceFilters, setInvoiceFilters] = useState({
+ search: '',
+ status: '',
+ });
+ const [invoicesCurrentPage, setInvoicesCurrentPage] = useState(1);
+ const [invoicesTotalPages, setInvoicesTotalPages] = useState(1);
+ const [invoicesTotalItems, setInvoicesTotalItems] = useState(0);
+ const invoicesPerPage = 10;
+
+ // Payments State
+ const [payments, setPayments] = useState([]);
+ const [paymentsLoading, setPaymentsLoading] = useState(true);
+ const [paymentFilters, setPaymentFilters] = useState({
+ search: '',
+ method: '',
+ from: '',
+ to: '',
+ });
+ const [paymentsCurrentPage, setPaymentsCurrentPage] = useState(1);
+ const [paymentsTotalPages, setPaymentsTotalPages] = useState(1);
+ const [paymentsTotalItems, setPaymentsTotalItems] = useState(0);
+ const paymentsPerPage = 5;
+
+ // Promotions State
+ const [promotions, setPromotions] = useState([]);
+ const [promotionsLoading, setPromotionsLoading] = useState(true);
+ const [showPromotionModal, setShowPromotionModal] = useState(false);
+ const [editingPromotion, setEditingPromotion] = useState(null);
+ const [promotionFilters, setPromotionFilters] = useState({
+ search: '',
+ status: '',
+ type: '',
+ });
+ const [promotionsCurrentPage, setPromotionsCurrentPage] = useState(1);
+ const [promotionsTotalPages, setPromotionsTotalPages] = useState(1);
+ const [promotionsTotalItems, setPromotionsTotalItems] = useState(0);
+ const promotionsPerPage = 5;
+
+ const [promotionFormData, setPromotionFormData] = useState({
+ code: '',
+ name: '',
+ description: '',
+ discount_type: 'percentage' as 'percentage' | 'fixed',
+ discount_value: 0,
+ min_booking_amount: 0,
+ max_discount_amount: 0,
+ start_date: '',
+ end_date: '',
+ usage_limit: 0,
+ status: 'active' as 'active' | 'inactive' | 'expired',
+ });
+
+ useEffect(() => {
+ if (activeTab === 'invoices') {
+ fetchInvoices();
+ } else if (activeTab === 'payments') {
+ fetchPayments();
+ } else if (activeTab === 'promotions') {
+ fetchPromotions();
+ }
+ }, [activeTab, invoiceFilters, invoicesCurrentPage, paymentFilters, paymentsCurrentPage, promotionFilters, promotionsCurrentPage]);
+
+ useEffect(() => {
+ if (activeTab === 'invoices') {
+ setInvoicesCurrentPage(1);
+ }
+ }, [invoiceFilters]);
+
+ useEffect(() => {
+ if (activeTab === 'payments') {
+ setPaymentsCurrentPage(1);
+ }
+ }, [paymentFilters]);
+
+ useEffect(() => {
+ if (activeTab === 'promotions') {
+ setPromotionsCurrentPage(1);
+ }
+ }, [promotionFilters]);
+
+ // Invoices Functions
+ const fetchInvoices = async () => {
+ try {
+ setInvoicesLoading(true);
+ const response = await invoiceService.getInvoices({
+ status: invoiceFilters.status || undefined,
+ page: invoicesCurrentPage,
+ limit: invoicesPerPage,
+ });
+
+ if (response.status === 'success' && response.data) {
+ let invoiceList = response.data.invoices || [];
+
+ if (invoiceFilters.search) {
+ invoiceList = invoiceList.filter((inv) =>
+ inv.invoice_number.toLowerCase().includes(invoiceFilters.search.toLowerCase()) ||
+ inv.customer_name.toLowerCase().includes(invoiceFilters.search.toLowerCase()) ||
+ inv.customer_email.toLowerCase().includes(invoiceFilters.search.toLowerCase())
+ );
+ }
+
+ setInvoices(invoiceList);
+ setInvoicesTotalPages(response.data.total_pages || 1);
+ setInvoicesTotalItems(response.data.total || 0);
+ }
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to load invoices');
+ } finally {
+ setInvoicesLoading(false);
+ }
+ };
+
+ const handleDeleteInvoice = async (id: number) => {
+ if (!window.confirm('Are you sure you want to delete this invoice?')) {
+ return;
+ }
+
+ try {
+ await invoiceService.deleteInvoice(id);
+ toast.success('Invoice deleted successfully');
+ fetchInvoices();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to delete invoice');
+ }
+ };
+
+ const getInvoiceStatusBadge = (status: string) => {
+ const badges: Record = {
+ draft: { bg: 'bg-gradient-to-r from-slate-50 to-gray-50', text: 'text-slate-700', label: 'Draft', border: 'border-slate-200' },
+ sent: { bg: 'bg-gradient-to-r from-blue-50 to-indigo-50', text: 'text-blue-800', label: 'Sent', border: 'border-blue-200' },
+ paid: { bg: 'bg-gradient-to-r from-emerald-50 to-green-50', text: 'text-emerald-800', label: 'Paid', border: 'border-emerald-200' },
+ overdue: { bg: 'bg-gradient-to-r from-rose-50 to-red-50', text: 'text-rose-800', label: 'Overdue', border: 'border-rose-200' },
+ cancelled: { bg: 'bg-gradient-to-r from-slate-50 to-gray-50', text: 'text-slate-700', label: 'Cancelled', border: 'border-slate-200' },
+ };
+ return badges[status] || badges.draft;
+ };
+
+ // Payments Functions
+ const fetchPayments = async () => {
+ try {
+ setPaymentsLoading(true);
+ const response = await paymentService.getPayments({
+ ...paymentFilters,
+ page: paymentsCurrentPage,
+ limit: paymentsPerPage,
+ });
+ setPayments(response.data.payments);
+ if (response.data.pagination) {
+ setPaymentsTotalPages(response.data.pagination.totalPages);
+ setPaymentsTotalItems(response.data.pagination.total);
+ }
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to load payments list');
+ } finally {
+ setPaymentsLoading(false);
+ }
+ };
+
+ const getPaymentMethodBadge = (method: string) => {
+ const badges: Record = {
+ cash: { bg: 'bg-gradient-to-r from-emerald-50 to-green-50', text: 'text-emerald-800', label: 'Cash', border: 'border-emerald-200' },
+ bank_transfer: { bg: 'bg-gradient-to-r from-blue-50 to-indigo-50', text: 'text-blue-800', label: 'Bank transfer', border: 'border-blue-200' },
+ stripe: { bg: 'bg-gradient-to-r from-indigo-50 to-purple-50', text: 'text-indigo-800', label: 'Stripe', border: 'border-indigo-200' },
+ credit_card: { bg: 'bg-gradient-to-r from-purple-50 to-pink-50', text: 'text-purple-800', label: 'Credit card', border: 'border-purple-200' },
+ };
+ const badge = badges[method] || badges.cash;
+ return (
+
+ {badge.label}
+
+ );
+ };
+
+ // Promotions Functions
+ const fetchPromotions = async () => {
+ try {
+ setPromotionsLoading(true);
+ const response = await promotionService.getPromotions({
+ ...promotionFilters,
+ page: promotionsCurrentPage,
+ limit: promotionsPerPage,
+ });
+ setPromotions(response.data.promotions);
+ if (response.data.pagination) {
+ setPromotionsTotalPages(response.data.pagination.totalPages);
+ setPromotionsTotalItems(response.data.pagination.total);
+ }
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to load promotions list');
+ } finally {
+ setPromotionsLoading(false);
+ }
+ };
+
+ const handlePromotionSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ try {
+ if (editingPromotion) {
+ await promotionService.updatePromotion(editingPromotion.id, promotionFormData);
+ toast.success('Promotion updated successfully');
+ } else {
+ await promotionService.createPromotion(promotionFormData);
+ toast.success('Promotion added successfully');
+ }
+ setShowPromotionModal(false);
+ resetPromotionForm();
+ fetchPromotions();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'An error occurred');
+ }
+ };
+
+ const handleEditPromotion = (promotion: Promotion) => {
+ setEditingPromotion(promotion);
+ setPromotionFormData({
+ code: promotion.code,
+ name: promotion.name,
+ description: promotion.description || '',
+ discount_type: promotion.discount_type,
+ discount_value: promotion.discount_value,
+ min_booking_amount: promotion.min_booking_amount || 0,
+ max_discount_amount: promotion.max_discount_amount || 0,
+ start_date: promotion.start_date?.split('T')[0] || '',
+ end_date: promotion.end_date?.split('T')[0] || '',
+ usage_limit: promotion.usage_limit || 0,
+ status: promotion.status,
+ });
+ setShowPromotionModal(true);
+ };
+
+ const handleDeletePromotion = async (id: number) => {
+ if (!window.confirm('Are you sure you want to delete this promotion?')) return;
+
+ try {
+ await promotionService.deletePromotion(id);
+ toast.success('Promotion deleted successfully');
+ fetchPromotions();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to delete promotion');
+ }
+ };
+
+ const resetPromotionForm = () => {
+ setEditingPromotion(null);
+ setPromotionFormData({
+ code: '',
+ name: '',
+ description: '',
+ discount_type: 'percentage',
+ discount_value: 0,
+ min_booking_amount: 0,
+ max_discount_amount: 0,
+ start_date: '',
+ end_date: '',
+ usage_limit: 0,
+ status: 'active',
+ });
+ };
+
+ const getPromotionStatusBadge = (status: string) => {
+ const badges: Record = {
+ active: { bg: 'bg-gradient-to-r from-emerald-50 to-green-50', text: 'text-emerald-800', label: 'Active', border: 'border-emerald-200' },
+ inactive: { bg: 'bg-gradient-to-r from-slate-50 to-gray-50', text: 'text-slate-700', label: 'Inactive', border: 'border-slate-200' },
+ expired: { bg: 'bg-gradient-to-r from-rose-50 to-red-50', text: 'text-rose-800', label: 'Expired', border: 'border-rose-200' },
+ };
+ const badge = badges[status] || badges.active;
+ return (
+
+ {badge.label}
+
+ );
+ };
+
+ const tabs = [
+ { id: 'overview' as BusinessTab, label: 'Overview', icon: FileText },
+ { id: 'invoices' as BusinessTab, label: 'Invoices', icon: FileText },
+ { id: 'payments' as BusinessTab, label: 'Payments', icon: CreditCard },
+ { id: 'promotions' as BusinessTab, label: 'Promotions', icon: Tag },
+ ];
+
+ return (
+
+
+ {/* Luxury Header */}
+
+
+
+
+
+
+
+
+
+ Business Dashboard
+
+
+
+
+ Manage invoices, payments, and promotional campaigns
+
+
+
+
+
+ {/* Premium Tab Navigation */}
+
+
+ {tabs.map((tab) => {
+ const Icon = tab.icon;
+ const isActive = activeTab === tab.id;
+ return (
+
+ );
+ })}
+
+
+
+
+
+ {/* Overview Tab */}
+ {activeTab === 'overview' && (
+
+
setActiveTab('invoices')}
+ className="group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-blue-100/50 p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:border-blue-300/60 overflow-hidden"
+ >
+
+
+
+
+ Manage and track all invoices and billing
+
+
+
+
+
+
+
setActiveTab('payments')}
+ className="group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-emerald-100/50 p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:border-emerald-300/60 overflow-hidden"
+ >
+
+
+
+
+ Track payment transactions and revenue
+
+
+
+
+
+
+
setActiveTab('promotions')}
+ className="group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-purple-100/50 p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:border-purple-300/60 overflow-hidden"
+ >
+
+
+
+
+ Manage discount codes and campaigns
+
+
+
+ View Promotions
+
+
+
+
+
+
+
+ )}
+
+ {/* Invoices Tab */}
+ {activeTab === 'invoices' && (
+
+ {/* Section Header */}
+
+
+
+
+
+
+
+
Invoice Management
+
+
+ Manage and track all invoices and billing information
+
+
+
+
+
+
+ {/* Filters */}
+
+
+
+
+
+ setInvoiceFilters({ ...invoiceFilters, search: e.target.value })}
+ className="w-full pl-12 pr-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100 transition-all duration-200 text-gray-700 placeholder-gray-400 font-medium shadow-sm hover:shadow-md"
+ />
+
+
+
+
+
+ {invoicesTotalItems} invoice{invoicesTotalItems !== 1 ? 's' : ''}
+
+
+
+
+
+ {/* Invoices Table */}
+ {invoicesLoading && invoices.length === 0 ? (
+
+ ) : (
+
+
+
+
+
+ | Invoice # |
+ Customer |
+ Booking |
+ Amount |
+ Status |
+ Due Date |
+ Actions |
+
+
+
+ {invoices.length > 0 ? (
+ invoices.map((invoice) => {
+ const statusBadge = getInvoiceStatusBadge(invoice.status);
+ return (
+
+ |
+
+
+ {invoice.invoice_number}
+
+ |
+
+ {invoice.customer_name}
+ {invoice.customer_email}
+ |
+
+ #{invoice.booking_id}
+ |
+
+
+ {formatCurrency(invoice.total_amount)}
+
+ {invoice.balance_due > 0 && (
+
+ Due: {formatCurrency(invoice.balance_due)}
+
+ )}
+ |
+
+
+ {statusBadge.label}
+
+ |
+
+ {formatDate(invoice.due_date, 'short')}
+ |
+
+
+
+
+
+
+ |
+
+ );
+ })
+ ) : (
+
+ |
+
+ |
+
+ )}
+
+
+
+ {invoicesTotalPages > 1 && (
+
+ )}
+
+ )}
+
+ )}
+
+ {/* Payments Tab */}
+ {activeTab === 'payments' && (
+
+ {/* Section Header */}
+
+
+
+
+
+
+
Payment Management
+
+
+ Track payment transactions and revenue streams
+
+
+
+
+ {/* Filters */}
+
+
+ {/* Payments Table */}
+ {paymentsLoading && payments.length === 0 ? (
+
+ ) : (
+ <>
+
+
+
+
+
+ | Transaction ID |
+ Booking Number |
+ Customer |
+ Method |
+ Amount |
+ Payment Date |
+
+
+
+ {payments.map((payment) => (
+
+ |
+ {payment.transaction_id || `PAY-${payment.id}`}
+ |
+
+ {payment.booking?.booking_number}
+ |
+
+ {payment.booking?.user?.name}
+ |
+
+ {getPaymentMethodBadge(payment.payment_method)}
+ |
+
+
+ {formatCurrency(payment.amount)}
+
+ |
+
+
+ {new Date(payment.payment_date || payment.createdAt).toLocaleDateString('en-US')}
+
+ |
+
+ ))}
+
+
+
+
+
+
+ {/* Summary Card */}
+
+
+
+
Total Revenue
+
+ {formatCurrency(payments.reduce((sum, p) => sum + p.amount, 0))}
+
+
Total {payments.length} transaction{payments.length !== 1 ? 's' : ''}
+
+
+
+
+ >
+ )}
+
+ )}
+
+ {/* Promotions Tab */}
+ {activeTab === 'promotions' && (
+
+ {/* Section Header */}
+
+
+
+
+
+
+
+
Promotion Management
+
+
+ Manage discount codes and promotional campaigns
+
+
+
+
+
+
+ {/* Filters */}
+
+
+
+
+
+ setPromotionFilters({ ...promotionFilters, search: e.target.value })}
+ className="w-full pl-12 pr-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200 text-gray-700 placeholder-gray-400 font-medium shadow-sm hover:shadow-md"
+ />
+
+
+
+
+
+
+ {/* Promotions Table */}
+ {promotionsLoading && promotions.length === 0 ? (
+
+ ) : (
+
+
+
+
+
+ | Code |
+ Program Name |
+ Value |
+ Period |
+ Used |
+ Status |
+ Actions |
+
+
+
+ {promotions.map((promotion) => (
+
+
+
+
+
+
+ {promotion.code}
+
+ |
+
+ {promotion.name}
+ {promotion.description}
+ |
+
+
+ {promotion.discount_type === 'percentage'
+ ? `${promotion.discount_value}%`
+ : formatCurrency(promotion.discount_value)}
+
+ |
+
+
+ {promotion.start_date ? new Date(promotion.start_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : 'N/A'}
+ →
+ {promotion.end_date ? new Date(promotion.end_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : 'N/A'}
+
+ |
+
+
+ {promotion.used_count || 0}
+ /
+ {promotion.usage_limit || '∞'}
+
+ |
+
+ {getPromotionStatusBadge(promotion.status)}
+ |
+
+
+
+
+
+ |
+
+ ))}
+
+
+
+
+
+ )}
+
+ {/* Promotion Modal */}
+ {showPromotionModal && (
+
+
+ {/* Modal Header */}
+
+
+
+
+ {editingPromotion ? 'Update Promotion' : 'Add New Promotion'}
+
+
+ {editingPromotion ? 'Modify promotion details' : 'Create a new promotion program'}
+
+
+
+
+
+
+ {/* Modal Content */}
+
+
+
+ )}
+
+ )}
+
+
+ );
+};
+
+export default BusinessDashboardPage;
+
diff --git a/Frontend/src/pages/admin/PageContentDashboard.tsx b/Frontend/src/pages/admin/PageContentDashboard.tsx
new file mode 100644
index 00000000..1ca92592
--- /dev/null
+++ b/Frontend/src/pages/admin/PageContentDashboard.tsx
@@ -0,0 +1,1458 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Home,
+ Mail,
+ Info,
+ FileText,
+ Search,
+ Save,
+ Globe,
+ Facebook,
+ Twitter,
+ Instagram,
+ Linkedin,
+ Youtube,
+ MapPin,
+ Phone,
+ X,
+ Plus,
+ Trash2,
+ Image as ImageIcon,
+ Eye,
+ Edit,
+ Upload,
+ Loader2,
+ Check,
+ XCircle
+} from 'lucide-react';
+import { pageContentService, PageContent, PageType, UpdatePageContentData, bannerService, Banner } from '../../services/api';
+import { toast } from 'react-toastify';
+import Loading from '../../components/common/Loading';
+import { ConfirmationDialog } from '../../components/common';
+
+type ContentTab = 'overview' | 'home' | 'contact' | 'about' | 'footer' | 'seo';
+
+const PageContentDashboard: React.FC = () => {
+ const [activeTab, setActiveTab] = useState('overview');
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [pageContents, setPageContents] = useState>({
+ home: null,
+ contact: null,
+ about: null,
+ footer: null,
+ seo: null,
+ });
+
+ // Form states for each page
+ const [homeData, setHomeData] = useState({});
+ const [contactData, setContactData] = useState({});
+ const [aboutData, setAboutData] = useState({});
+ const [footerData, setFooterData] = useState({});
+ const [seoData, setSeoData] = useState({});
+
+ // Banner management state
+ const [banners, setBanners] = useState([]);
+ const [loadingBanners, setLoadingBanners] = useState(false);
+ const [showBannerModal, setShowBannerModal] = useState(false);
+ const [editingBanner, setEditingBanner] = useState(null);
+ const [bannerFormData, setBannerFormData] = useState({
+ title: '',
+ description: '',
+ image_url: '',
+ link: '',
+ position: 'home',
+ display_order: 0,
+ is_active: true,
+ start_date: '',
+ end_date: '',
+ });
+ const [imageFile, setImageFile] = useState(null);
+ const [imagePreview, setImagePreview] = useState(null);
+ const [uploadingImage, setUploadingImage] = useState(false);
+ const [useFileUpload, setUseFileUpload] = useState(true);
+ const [deleteConfirm, setDeleteConfirm] = useState<{ show: boolean; id: number | null }>({ show: false, id: null });
+
+ useEffect(() => {
+ fetchAllPageContents();
+ if (activeTab === 'home') {
+ fetchBanners();
+ }
+ }, [activeTab]);
+
+ const fetchAllPageContents = async () => {
+ try {
+ setLoading(true);
+ const response = await pageContentService.getAllPageContents();
+ const contents = response.data.page_contents || [];
+
+ const contentsMap: Record = {
+ home: null,
+ contact: null,
+ about: null,
+ footer: null,
+ seo: null,
+ };
+
+ contents.forEach((content) => {
+ if (content.page_type in contentsMap) {
+ contentsMap[content.page_type as PageType] = content;
+ }
+ });
+
+ setPageContents(contentsMap);
+
+ // Initialize form data
+ initializeFormData(contentsMap);
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to load page contents');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const initializeFormData = (contents: Record) => {
+ // Home
+ if (contents.home) {
+ setHomeData({
+ title: contents.home.title || '',
+ subtitle: contents.home.subtitle || '',
+ description: contents.home.description || '',
+ content: contents.home.content || '',
+ hero_title: contents.home.hero_title || '',
+ hero_subtitle: contents.home.hero_subtitle || '',
+ hero_image: contents.home.hero_image || '',
+ meta_title: contents.home.meta_title || '',
+ meta_description: contents.home.meta_description || '',
+ meta_keywords: contents.home.meta_keywords || '',
+ og_title: contents.home.og_title || '',
+ og_description: contents.home.og_description || '',
+ og_image: contents.home.og_image || '',
+ });
+ }
+
+ // Contact
+ if (contents.contact) {
+ setContactData({
+ title: contents.contact.title || '',
+ subtitle: contents.contact.subtitle || '',
+ description: contents.contact.description || '',
+ content: contents.contact.content || '',
+ contact_info: contents.contact.contact_info || { phone: '', email: '', address: '' },
+ map_url: contents.contact.map_url || '',
+ meta_title: contents.contact.meta_title || '',
+ meta_description: contents.contact.meta_description || '',
+ });
+ }
+
+ // About
+ if (contents.about) {
+ setAboutData({
+ title: contents.about.title || '',
+ subtitle: contents.about.subtitle || '',
+ description: contents.about.description || '',
+ content: contents.about.content || '',
+ story_content: contents.about.story_content || '',
+ values: contents.about.values || [],
+ features: contents.about.features || [],
+ meta_title: contents.about.meta_title || '',
+ meta_description: contents.about.meta_description || '',
+ });
+ }
+
+ // Footer
+ if (contents.footer) {
+ setFooterData({
+ title: contents.footer.title || '',
+ description: contents.footer.description || '',
+ contact_info: contents.footer.contact_info || { phone: '', email: '', address: '' },
+ social_links: contents.footer.social_links || {},
+ footer_links: contents.footer.footer_links || { quick_links: [], support_links: [] },
+ meta_title: contents.footer.meta_title || '',
+ meta_description: contents.footer.meta_description || '',
+ });
+ }
+
+ // SEO
+ if (contents.seo) {
+ setSeoData({
+ meta_title: contents.seo.meta_title || '',
+ meta_description: contents.seo.meta_description || '',
+ meta_keywords: contents.seo.meta_keywords || '',
+ og_title: contents.seo.og_title || '',
+ og_description: contents.seo.og_description || '',
+ og_image: contents.seo.og_image || '',
+ canonical_url: contents.seo.canonical_url || '',
+ });
+ }
+ };
+
+ const handleSave = async (pageType: PageType, data: UpdatePageContentData) => {
+ try {
+ setSaving(true);
+ await pageContentService.updatePageContent(pageType, data);
+ toast.success(`${pageType.charAt(0).toUpperCase() + pageType.slice(1)} content saved successfully`);
+ await fetchAllPageContents();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || `Failed to save ${pageType} content`);
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ // Banner management functions
+ const fetchBanners = async () => {
+ try {
+ setLoadingBanners(true);
+ const response = await bannerService.getAllBanners({ position: 'home' });
+ if (response.success || response.status === 'success') {
+ setBanners(response.data?.banners || []);
+ }
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Failed to load banners');
+ } finally {
+ setLoadingBanners(false);
+ }
+ };
+
+ const handleImageChange = async (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ if (file.size > 5 * 1024 * 1024) {
+ toast.error('Image size must be less than 5MB');
+ return;
+ }
+
+ setImageFile(file);
+
+ // Create preview
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ setImagePreview(reader.result as string);
+ };
+ reader.readAsDataURL(file);
+
+ // Upload image immediately
+ try {
+ setUploadingImage(true);
+ const response = await bannerService.uploadBannerImage(file);
+ if (response.status === 'success' || response.success) {
+ setBannerFormData({ ...bannerFormData, image_url: response.data.image_url });
+ toast.success('Image uploaded successfully');
+ }
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Failed to upload image');
+ setImageFile(null);
+ setImagePreview(null);
+ } finally {
+ setUploadingImage(false);
+ }
+ };
+
+ const handleBannerSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!bannerFormData.image_url && !imageFile) {
+ toast.error('Please upload an image or provide an image URL');
+ return;
+ }
+
+ try {
+ let imageUrl = bannerFormData.image_url;
+ if (imageFile && !imageUrl) {
+ setUploadingImage(true);
+ const uploadResponse = await bannerService.uploadBannerImage(imageFile);
+ if (uploadResponse.status === 'success' || uploadResponse.success) {
+ imageUrl = uploadResponse.data.image_url;
+ } else {
+ throw new Error('Failed to upload image');
+ }
+ setUploadingImage(false);
+ }
+
+ const submitData = {
+ ...bannerFormData,
+ image_url: imageUrl,
+ start_date: bannerFormData.start_date || undefined,
+ end_date: bannerFormData.end_date || undefined,
+ };
+
+ if (editingBanner) {
+ await bannerService.updateBanner(editingBanner.id, submitData);
+ toast.success('Banner updated successfully');
+ } else {
+ await bannerService.createBanner(submitData);
+ toast.success('Banner created successfully');
+ }
+ setShowBannerModal(false);
+ resetBannerForm();
+ fetchBanners();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'An error occurred');
+ setUploadingImage(false);
+ }
+ };
+
+ const handleEditBanner = (banner: Banner) => {
+ setEditingBanner(banner);
+ setBannerFormData({
+ title: banner.title || '',
+ description: '',
+ image_url: banner.image_url || '',
+ link: banner.link || '',
+ position: banner.position || 'home',
+ display_order: banner.display_order || 0,
+ is_active: banner.is_active ?? true,
+ start_date: banner.start_date ? banner.start_date.split('T')[0] : '',
+ end_date: banner.end_date ? banner.end_date.split('T')[0] : '',
+ });
+ setImageFile(null);
+ const previewUrl = banner.image_url
+ ? (banner.image_url.startsWith('http')
+ ? banner.image_url
+ : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${banner.image_url}`)
+ : null;
+ setImagePreview(previewUrl);
+ setUseFileUpload(false);
+ setShowBannerModal(true);
+ };
+
+ const handleDeleteBanner = async () => {
+ if (!deleteConfirm.id) return;
+
+ try {
+ await bannerService.deleteBanner(deleteConfirm.id);
+ toast.success('Banner deleted successfully');
+ setDeleteConfirm({ show: false, id: null });
+ fetchBanners();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Failed to delete banner');
+ }
+ };
+
+ const resetBannerForm = () => {
+ setBannerFormData({
+ title: '',
+ description: '',
+ image_url: '',
+ link: '',
+ position: 'home',
+ display_order: 0,
+ is_active: true,
+ start_date: '',
+ end_date: '',
+ });
+ setImageFile(null);
+ setImagePreview(null);
+ setUseFileUpload(true);
+ setEditingBanner(null);
+ };
+
+ const toggleBannerActive = async (banner: Banner) => {
+ try {
+ await bannerService.updateBanner(banner.id, {
+ is_active: !banner.is_active,
+ });
+ toast.success(`Banner ${!banner.is_active ? 'activated' : 'deactivated'}`);
+ fetchBanners();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Failed to update banner');
+ }
+ };
+
+ const tabs = [
+ { id: 'overview' as ContentTab, label: 'Overview', icon: Home },
+ { id: 'home' as ContentTab, label: 'Home', icon: Home },
+ { id: 'contact' as ContentTab, label: 'Contact', icon: Mail },
+ { id: 'about' as ContentTab, label: 'About', icon: Info },
+ { id: 'footer' as ContentTab, label: 'Footer', icon: FileText },
+ { id: 'seo' as ContentTab, label: 'SEO', icon: Search },
+ ];
+
+ if (loading) {
+ return ;
+ }
+
+ return (
+
+
+ {/* Luxury Header */}
+
+
+
+
+
+
+
+
+
+ Page Content Management
+
+
+
+ Centralized control for all frontend pages and SEO optimization
+
+
+
+
+
+ {/* Premium Tab Navigation */}
+
+
+ {tabs.map((tab) => {
+ const Icon = tab.icon;
+ const isActive = activeTab === tab.id;
+ return (
+
+ );
+ })}
+
+
+
+
+
+ {/* Overview Tab */}
+ {activeTab === 'overview' && (
+
+ {[
+ { id: 'home' as PageType, label: 'Home Page', icon: Home, color: 'blue', description: 'Manage hero section, featured content' },
+ { id: 'contact' as PageType, label: 'Contact Page', icon: Mail, color: 'green', description: 'Manage contact information and form' },
+ { id: 'about' as PageType, label: 'About Page', icon: Info, color: 'amber', description: 'Manage story, values, and features' },
+ { id: 'footer' as PageType, label: 'Footer', icon: FileText, color: 'purple', description: 'Manage footer links and social media' },
+ { id: 'seo' as PageType, label: 'SEO Settings', icon: Search, color: 'indigo', description: 'Manage meta tags and SEO optimization' },
+ ].map((page) => {
+ const Icon = page.icon;
+ const hasContent = pageContents[page.id] !== null;
+ return (
+
setActiveTab(page.id as ContentTab)}
+ className={`group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 overflow-hidden ${
+ page.color === 'blue' ? 'border-blue-100/50 hover:border-blue-300/60' :
+ page.color === 'green' ? 'border-green-100/50 hover:border-green-300/60' :
+ page.color === 'amber' ? 'border-amber-100/50 hover:border-amber-300/60' :
+ page.color === 'purple' ? 'border-purple-100/50 hover:border-purple-300/60' :
+ 'border-indigo-100/50 hover:border-indigo-300/60'
+ }`}
+ >
+
+
+
+
+ {hasContent && (
+
+
+ Active
+
+ )}
+
+
+ {page.description}
+
+
+
+ {hasContent ? 'Edit Content' : 'Add Content'}
+ →
+
+
+
+
+ );
+ })}
+
+ )}
+
+ {/* Home Tab */}
+ {activeTab === 'home' && (
+
+ {/* Home Page Content Section */}
+
+
Home Page Content
+
+
+
+
+ setHomeData({ ...homeData, hero_title: e.target.value })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ placeholder="Welcome to Luxury Hotel"
+ />
+
+
+
+
+ setHomeData({ ...homeData, hero_subtitle: e.target.value })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ placeholder="Experience unparalleled luxury"
+ />
+
+
+
+
+ setHomeData({ ...homeData, hero_image: e.target.value })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ placeholder="https://example.com/hero-image.jpg"
+ />
+
+
+
+
+ setHomeData({ ...homeData, title: e.target.value })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Banner Carousel Management Section */}
+
+
+
+
Banner Carousel
+
Manage homepage banner carousel images
+
+
+
+
+ {loadingBanners ? (
+
+
+
+ ) : banners.length === 0 ? (
+
+
+
No banners found
+
Add your first banner to get started
+
+ ) : (
+
+
+
+
+ | Image |
+ Title |
+ Order |
+ Status |
+ Actions |
+
+
+
+ {banners
+ .sort((a, b) => a.display_order - b.display_order)
+ .map((banner) => (
+
+
+ {banner.image_url ? (
+
+ ) : (
+
+
+
+ )}
+ |
+
+ {banner.title}
+ {banner.link && (
+ {banner.link}
+ )}
+ |
+
+ {banner.display_order}
+ |
+
+
+ |
+
+
+
+
+
+ |
+
+ ))}
+
+
+
+ )}
+
+
+ {/* Banner Modal */}
+ {showBannerModal && (
+
+
setShowBannerModal(false)}>
+
+
+
+
+ {editingBanner ? 'Edit Banner' : 'Add Banner'}
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Delete Confirmation Dialog */}
+
setDeleteConfirm({ show: false, id: null })}
+ onConfirm={handleDeleteBanner}
+ title="Delete Banner"
+ message="Are you sure you want to delete this banner? This action cannot be undone."
+ confirmText="Delete"
+ cancelText="Cancel"
+ variant="danger"
+ />
+
+ )}
+
+ {/* Contact Tab */}
+ {activeTab === 'contact' && (
+
+
+
Contact Page Content
+
+
+
+
+ setContactData({ ...contactData, title: e.target.value })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ />
+
+
+
+
+ setContactData({ ...contactData, subtitle: e.target.value })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ />
+
+
+
+
+
+
+
+
Contact Information
+
+
+
+
+
Google Maps
+
+
+
setContactData({ ...contactData, map_url: e.target.value })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ placeholder="https://www.google.com/maps/embed?pb=..."
+ />
+
+ Paste the Google Maps embed URL here. You can get this by clicking "Share" → "Embed a map" on Google Maps.
+
+ {contactData.map_url && (
+
+
+
+ )}
+
+
+
+
+
+
+ setContactData({ ...contactData, meta_title: e.target.value })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* About Tab */}
+ {activeTab === 'about' && (
+
+
+
About Page Content
+
+
+
+
+ setAboutData({ ...aboutData, title: e.target.value })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ />
+
+
+
+
+ setAboutData({ ...aboutData, subtitle: e.target.value })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setAboutData({ ...aboutData, meta_title: e.target.value })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Footer Tab */}
+ {activeTab === 'footer' && (
+
+
+
Footer Content
+
+
+
+
+ setFooterData({ ...footerData, title: e.target.value })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ />
+
+
+
+
+
+
+
+
Contact Information
+
+
+
+
+
Social Media Links
+
+
+
+ setFooterData({
+ ...footerData,
+ social_links: { ...footerData.social_links, facebook: e.target.value }
+ })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ placeholder="https://facebook.com/..."
+ />
+
+
+
+ setFooterData({
+ ...footerData,
+ social_links: { ...footerData.social_links, twitter: e.target.value }
+ })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ placeholder="https://twitter.com/..."
+ />
+
+
+
+ setFooterData({
+ ...footerData,
+ social_links: { ...footerData.social_links, instagram: e.target.value }
+ })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ placeholder="https://instagram.com/..."
+ />
+
+
+
+ setFooterData({
+ ...footerData,
+ social_links: { ...footerData.social_links, linkedin: e.target.value }
+ })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ placeholder="https://linkedin.com/..."
+ />
+
+
+
+ setFooterData({
+ ...footerData,
+ social_links: { ...footerData.social_links, youtube: e.target.value }
+ })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ placeholder="https://youtube.com/..."
+ />
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* SEO Tab */}
+ {activeTab === 'seo' && (
+
+
+
SEO Optimization
+
+
+
+
+ Note: SEO settings here apply globally. Individual pages can also have their own SEO settings.
+
+
+
+
+
+
setSeoData({ ...seoData, meta_title: e.target.value })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ placeholder="Default meta title for the website"
+ />
+
Recommended: 50-60 characters
+
+
+
+
+
+
+
+
+
setSeoData({ ...seoData, meta_keywords: e.target.value })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ placeholder="keyword1, keyword2, keyword3"
+ />
+
Comma-separated keywords
+
+
+
+
Open Graph (OG) Tags
+
+
+
+
+
+
+
+
+
setSeoData({ ...seoData, canonical_url: e.target.value })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ placeholder="https://example.com"
+ />
+
Default canonical URL for the website
+
+
+
+
+
+
+
+
+ )}
+
+
+ );
+};
+
+export default PageContentDashboard;
+
diff --git a/Frontend/src/pages/admin/ReceptionDashboardPage.tsx b/Frontend/src/pages/admin/ReceptionDashboardPage.tsx
new file mode 100644
index 00000000..49fa227f
--- /dev/null
+++ b/Frontend/src/pages/admin/ReceptionDashboardPage.tsx
@@ -0,0 +1,2961 @@
+import React, { useState, useEffect } from 'react';
+import {
+ LogIn,
+ LogOut,
+ Search,
+ User,
+ Hotel,
+ CheckCircle,
+ AlertCircle,
+ FileText,
+ CreditCard,
+ Printer,
+ Sparkles,
+ ChevronRight,
+ Eye,
+ XCircle,
+ Loader2,
+ Plus,
+ Edit,
+ Trash2,
+ X,
+ Upload,
+ Image as ImageIcon,
+ Check,
+ Calendar,
+ Wrench
+} from 'lucide-react';
+import { bookingService, Booking, roomService, Room, serviceService, Service } from '../../services/api';
+import { toast } from 'react-toastify';
+import Loading from '../../components/common/Loading';
+import CurrencyIcon from '../../components/common/CurrencyIcon';
+import Pagination from '../../components/common/Pagination';
+import apiClient from '../../services/api/apiClient';
+import { useFormatCurrency } from '../../hooks/useFormatCurrency';
+import { parseDateLocal } from '../../utils/format';
+
+type ReceptionTab = 'overview' | 'check-in' | 'check-out' | 'bookings' | 'rooms' | 'services';
+
+interface GuestInfo {
+ name: string;
+ id_number: string;
+ phone: string;
+}
+
+interface ServiceItem {
+ service_name: string;
+ quantity: number;
+ price: number;
+ total: number;
+}
+
+const ReceptionDashboardPage: React.FC = () => {
+ const { formatCurrency } = useFormatCurrency();
+ const [activeTab, setActiveTab] = useState('overview');
+
+ // Check-in State
+ const [checkInBookingNumber, setCheckInBookingNumber] = useState('');
+ const [checkInBooking, setCheckInBooking] = useState(null);
+ const [checkInLoading, setCheckInLoading] = useState(false);
+ const [checkInSearching, setCheckInSearching] = useState(false);
+ const [actualRoomNumber, setActualRoomNumber] = useState('');
+ const [guests, setGuests] = useState([{ name: '', id_number: '', phone: '' }]);
+ const [extraPersons, setExtraPersons] = useState(0);
+ const [children, setChildren] = useState(0);
+ const [additionalFee, setAdditionalFee] = useState(0);
+
+ // Check-out State
+ const [checkOutBookingNumber, setCheckOutBookingNumber] = useState('');
+ const [checkOutBooking, setCheckOutBooking] = useState(null);
+ const [checkOutLoading, setCheckOutLoading] = useState(false);
+ const [checkOutSearching, setCheckOutSearching] = useState(false);
+ const [checkOutServices, setCheckOutServices] = useState([]);
+ const [paymentMethod, setPaymentMethod] = useState<'cash' | 'stripe'>('cash');
+ const [discount, setDiscount] = useState(0);
+ const [showInvoice, setShowInvoice] = useState(false);
+
+ // Bookings Management State
+ const [bookings, setBookings] = useState([]);
+ const [bookingsLoading, setBookingsLoading] = useState(true);
+ const [selectedBooking, setSelectedBooking] = useState(null);
+ const [showBookingDetailModal, setShowBookingDetailModal] = useState(false);
+ const [updatingBookingId, setUpdatingBookingId] = useState(null);
+ const [cancellingBookingId, setCancellingBookingId] = useState(null);
+ const [bookingFilters, setBookingFilters] = useState({
+ search: '',
+ status: '',
+ });
+ const [bookingCurrentPage, setBookingCurrentPage] = useState(1);
+ const [bookingTotalPages, setBookingTotalPages] = useState(1);
+ const [bookingTotalItems, setBookingTotalItems] = useState(0);
+ const bookingItemsPerPage = 5;
+
+ // Rooms Management State
+ const [rooms, setRooms] = useState([]);
+ const [roomsLoading, setRoomsLoading] = useState(true);
+ const [showRoomModal, setShowRoomModal] = useState(false);
+ const [editingRoom, setEditingRoom] = useState(null);
+ const [selectedRooms, setSelectedRooms] = useState([]);
+ const [roomFilters, setRoomFilters] = useState({
+ search: '',
+ status: '',
+ type: '',
+ });
+ const [roomCurrentPage, setRoomCurrentPage] = useState(1);
+ const [roomTotalPages, setRoomTotalPages] = useState(1);
+ const [roomTotalItems, setRoomTotalItems] = useState(0);
+ const roomItemsPerPage = 5;
+ const [roomFormData, setRoomFormData] = useState({
+ room_number: '',
+ floor: 1,
+ room_type_id: 1,
+ status: 'available' as 'available' | 'occupied' | 'maintenance',
+ featured: false,
+ price: '',
+ description: '',
+ capacity: '',
+ room_size: '',
+ view: '',
+ amenities: [] as string[],
+ });
+ const [availableAmenities, setAvailableAmenities] = useState([]);
+ const [roomTypes, setRoomTypes] = useState>([]);
+ const [uploadingImages, setUploadingImages] = useState(false);
+ const [selectedFiles, setSelectedFiles] = useState([]);
+
+ // Services Management State
+ const [services, setServices] = useState([]);
+ const [servicesLoading, setServicesLoading] = useState(true);
+ const [showServiceModal, setShowServiceModal] = useState(false);
+ const [editingService, setEditingService] = useState(null);
+ const [serviceFilters, setServiceFilters] = useState({
+ search: '',
+ status: '',
+ });
+ const [serviceCurrentPage, setServiceCurrentPage] = useState(1);
+ const [serviceTotalPages, setServiceTotalPages] = useState(1);
+ const [serviceTotalItems, setServiceTotalItems] = useState(0);
+ const serviceItemsPerPage = 5;
+ const [serviceFormData, setServiceFormData] = useState({
+ name: '',
+ description: '',
+ price: 0,
+ unit: 'time',
+ status: 'active' as 'active' | 'inactive',
+ });
+
+ // Check-in Functions
+ const handleCheckInSearch = async () => {
+ if (!checkInBookingNumber.trim()) {
+ toast.error('Please enter booking number');
+ return;
+ }
+
+ try {
+ setCheckInSearching(true);
+ const response = await bookingService.checkBookingByNumber(checkInBookingNumber);
+ setCheckInBooking(response.data.booking);
+ setActualRoomNumber(response.data.booking.room?.room_number || '');
+ toast.success('Booking found');
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Booking not found');
+ setCheckInBooking(null);
+ } finally {
+ setCheckInSearching(false);
+ }
+ };
+
+ const handleAddGuest = () => {
+ setGuests([...guests, { name: '', id_number: '', phone: '' }]);
+ };
+
+ const handleRemoveGuest = (index: number) => {
+ if (guests.length > 1) {
+ setGuests(guests.filter((_, i) => i !== index));
+ }
+ };
+
+ const handleGuestChange = (index: number, field: keyof GuestInfo, value: string) => {
+ const newGuests = [...guests];
+ newGuests[index][field] = value;
+ setGuests(newGuests);
+ };
+
+ const calculateCheckInAdditionalFee = () => {
+ const extraPersonFee = extraPersons * 200000;
+ const childrenFee = children * 100000;
+ const total = extraPersonFee + childrenFee;
+ setAdditionalFee(total);
+ return total;
+ };
+
+ const handleCheckIn = async () => {
+ if (!checkInBooking) return;
+
+ if (!actualRoomNumber.trim()) {
+ toast.error('Please enter actual room number');
+ return;
+ }
+
+ const mainGuest = guests[0];
+ if (!mainGuest.name || !mainGuest.id_number || !mainGuest.phone) {
+ toast.error('Please fill in all main guest information');
+ return;
+ }
+
+ try {
+ setCheckInLoading(true);
+ calculateCheckInAdditionalFee();
+
+ await bookingService.updateBooking(checkInBooking.id, {
+ status: 'checked_in',
+ } as any);
+
+ toast.success('Check-in successful');
+
+ setCheckInBooking(null);
+ setCheckInBookingNumber('');
+ setActualRoomNumber('');
+ setGuests([{ name: '', id_number: '', phone: '' }]);
+ setExtraPersons(0);
+ setChildren(0);
+ setAdditionalFee(0);
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'An error occurred during check-in');
+ } finally {
+ setCheckInLoading(false);
+ }
+ };
+
+ // Check-out Functions
+ const handleCheckOutSearch = async () => {
+ if (!checkOutBookingNumber.trim()) {
+ toast.error('Please enter booking number');
+ return;
+ }
+
+ try {
+ setCheckOutSearching(true);
+ const response = await bookingService.checkBookingByNumber(checkOutBookingNumber);
+ const foundBooking = response.data.booking;
+
+ if (foundBooking.status !== 'checked_in') {
+ toast.warning('Only checked-in bookings can be checked out');
+ }
+
+ setCheckOutBooking(foundBooking);
+
+ setCheckOutServices([
+ { service_name: 'Laundry', quantity: 2, price: 50000, total: 100000 },
+ { service_name: 'Minibar', quantity: 1, price: 150000, total: 150000 },
+ ]);
+
+ toast.success('Booking found');
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Booking not found');
+ setCheckOutBooking(null);
+ } finally {
+ setCheckOutSearching(false);
+ }
+ };
+
+ const calculateRoomFee = () => {
+ if (!checkOutBooking) return 0;
+ return checkOutBooking.total_price || 0;
+ };
+
+ const calculateServiceFee = () => {
+ return checkOutServices.reduce((sum, service) => sum + service.total, 0);
+ };
+
+ const calculateCheckOutAdditionalFee = () => {
+ return 0;
+ };
+
+ const calculateDeposit = () => {
+ return checkOutBooking?.total_price ? checkOutBooking.total_price * 0.3 : 0;
+ };
+
+ const calculateSubtotal = () => {
+ return calculateRoomFee() + calculateServiceFee() + calculateCheckOutAdditionalFee();
+ };
+
+ const calculateTotal = () => {
+ return calculateSubtotal() - discount;
+ };
+
+ const calculateRemaining = () => {
+ return calculateTotal() - calculateDeposit();
+ };
+
+ const handleCheckOut = async () => {
+ if (!checkOutBooking) return;
+
+ if (calculateRemaining() < 0) {
+ toast.error('Invalid refund amount');
+ return;
+ }
+
+ try {
+ setCheckOutLoading(true);
+
+ await bookingService.updateBooking(checkOutBooking.id, {
+ status: 'checked_out',
+ } as any);
+
+ toast.success('Check-out successful');
+ setShowInvoice(true);
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'An error occurred during check-out');
+ } finally {
+ setCheckOutLoading(false);
+ }
+ };
+
+ const handlePrintInvoice = () => {
+ window.print();
+ };
+
+ const resetCheckOutForm = () => {
+ setCheckOutBooking(null);
+ setCheckOutBookingNumber('');
+ setCheckOutServices([]);
+ setDiscount(0);
+ setPaymentMethod('cash');
+ setShowInvoice(false);
+ };
+
+ // Bookings Management Functions
+ useEffect(() => {
+ setBookingCurrentPage(1);
+ }, [bookingFilters]);
+
+ useEffect(() => {
+ if (activeTab === 'bookings') {
+ fetchBookings();
+ }
+ }, [bookingFilters, bookingCurrentPage, activeTab]);
+
+ const fetchBookings = async () => {
+ try {
+ setBookingsLoading(true);
+ const response = await bookingService.getAllBookings({
+ ...bookingFilters,
+ page: bookingCurrentPage,
+ limit: bookingItemsPerPage,
+ });
+ setBookings(response.data.bookings);
+ if (response.data.pagination) {
+ setBookingTotalPages(response.data.pagination.totalPages);
+ setBookingTotalItems(response.data.pagination.total);
+ }
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to load bookings list');
+ } finally {
+ setBookingsLoading(false);
+ }
+ };
+
+ const handleUpdateBookingStatus = async (id: number, status: string) => {
+ try {
+ setUpdatingBookingId(id);
+ await bookingService.updateBooking(id, { status } as any);
+ toast.success('Status updated successfully');
+ await fetchBookings();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to update status');
+ } finally {
+ setUpdatingBookingId(null);
+ }
+ };
+
+ const handleCancelBooking = async (id: number) => {
+ if (!window.confirm('Are you sure you want to cancel this booking?')) return;
+
+ try {
+ setCancellingBookingId(id);
+ await bookingService.cancelBooking(id);
+ toast.success('Booking cancelled successfully');
+ await fetchBookings();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to cancel booking');
+ } finally {
+ setCancellingBookingId(null);
+ }
+ };
+
+ const getBookingStatusBadge = (status: string) => {
+ const badges: Record = {
+ pending: {
+ bg: 'bg-gradient-to-r from-amber-50 to-yellow-50',
+ text: 'text-amber-800',
+ label: 'Pending confirmation',
+ border: 'border-amber-200'
+ },
+ confirmed: {
+ bg: 'bg-gradient-to-r from-blue-50 to-indigo-50',
+ text: 'text-blue-800',
+ label: 'Confirmed',
+ border: 'border-blue-200'
+ },
+ checked_in: {
+ bg: 'bg-gradient-to-r from-emerald-50 to-green-50',
+ text: 'text-emerald-800',
+ label: 'Checked in',
+ border: 'border-emerald-200'
+ },
+ checked_out: {
+ bg: 'bg-gradient-to-r from-slate-50 to-gray-50',
+ text: 'text-slate-700',
+ label: 'Checked out',
+ border: 'border-slate-200'
+ },
+ cancelled: {
+ bg: 'bg-gradient-to-r from-rose-50 to-red-50',
+ text: 'text-rose-800',
+ label: 'Cancelled',
+ border: 'border-rose-200'
+ },
+ };
+ const badge = badges[status] || badges.pending;
+ return (
+
+ {badge.label}
+
+ );
+ };
+
+ // Rooms Management Functions
+ useEffect(() => {
+ setRoomCurrentPage(1);
+ setSelectedRooms([]);
+ }, [roomFilters]);
+
+ useEffect(() => {
+ if (activeTab === 'rooms') {
+ fetchRooms();
+ fetchAvailableAmenities();
+ }
+ }, [roomFilters, roomCurrentPage, activeTab]);
+
+ useEffect(() => {
+ const fetchAllRoomTypes = async () => {
+ try {
+ const response = await roomService.getRooms({ limit: 100, page: 1 });
+ const allUniqueRoomTypes = new Map();
+ response.data.rooms.forEach((room: Room) => {
+ if (room.room_type && !allUniqueRoomTypes.has(room.room_type.id)) {
+ allUniqueRoomTypes.set(room.room_type.id, {
+ id: room.room_type.id,
+ name: room.room_type.name,
+ });
+ }
+ });
+
+ if (response.data.pagination && response.data.pagination.totalPages > 1) {
+ const totalPages = response.data.pagination.totalPages;
+ for (let page = 2; page <= totalPages; page++) {
+ try {
+ const pageResponse = await roomService.getRooms({ limit: 100, page });
+ pageResponse.data.rooms.forEach((room: Room) => {
+ if (room.room_type && !allUniqueRoomTypes.has(room.room_type.id)) {
+ allUniqueRoomTypes.set(room.room_type.id, {
+ id: room.room_type.id,
+ name: room.room_type.name,
+ });
+ }
+ });
+ } catch (err) {
+ console.error(`Failed to fetch page ${page}:`, err);
+ }
+ }
+ }
+
+ if (allUniqueRoomTypes.size > 0) {
+ const roomTypesList = Array.from(allUniqueRoomTypes.values());
+ setRoomTypes(roomTypesList);
+ if (!editingRoom && roomFormData.room_type_id === 1 && roomTypesList.length > 0) {
+ setRoomFormData(prev => ({ ...prev, room_type_id: roomTypesList[0].id }));
+ }
+ }
+ } catch (err) {
+ console.error('Failed to fetch room types:', err);
+ }
+ };
+ if (activeTab === 'rooms') {
+ fetchAllRoomTypes();
+ }
+ }, [activeTab]);
+
+ const fetchAvailableAmenities = async () => {
+ try {
+ const response = await roomService.getAmenities();
+ if (response.data?.amenities) {
+ setAvailableAmenities(response.data.amenities);
+ }
+ } catch (error) {
+ console.error('Failed to fetch amenities:', error);
+ }
+ };
+
+ const fetchRooms = async () => {
+ try {
+ setRoomsLoading(true);
+ const response = await roomService.getRooms({
+ ...roomFilters,
+ page: roomCurrentPage,
+ limit: roomItemsPerPage,
+ });
+ setRooms(response.data.rooms);
+ if (response.data.pagination) {
+ setRoomTotalPages(response.data.pagination.totalPages);
+ setRoomTotalItems(response.data.pagination.total);
+ }
+
+ const uniqueRoomTypes = new Map();
+ response.data.rooms.forEach((room: Room) => {
+ if (room.room_type && !uniqueRoomTypes.has(room.room_type.id)) {
+ uniqueRoomTypes.set(room.room_type.id, {
+ id: room.room_type.id,
+ name: room.room_type.name,
+ });
+ }
+ });
+ setRoomTypes(Array.from(uniqueRoomTypes.values()));
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to load rooms list');
+ } finally {
+ setRoomsLoading(false);
+ }
+ };
+
+ const handleRoomSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ try {
+ if (editingRoom) {
+ const updateData = {
+ ...roomFormData,
+ price: roomFormData.price ? parseFloat(roomFormData.price) : undefined,
+ description: roomFormData.description || undefined,
+ capacity: roomFormData.capacity ? parseInt(roomFormData.capacity) : undefined,
+ room_size: roomFormData.room_size || undefined,
+ view: roomFormData.view || undefined,
+ amenities: Array.isArray(roomFormData.amenities) ? roomFormData.amenities : [],
+ };
+ await roomService.updateRoom(editingRoom.id, updateData);
+ toast.success('Room updated successfully');
+ await fetchRooms();
+ try {
+ const updatedRoom = await roomService.getRoomByNumber(editingRoom.room_number);
+ setEditingRoom(updatedRoom.data.room);
+ } catch (err) {
+ console.error('Failed to refresh room data:', err);
+ }
+ } else {
+ const createData = {
+ ...roomFormData,
+ price: roomFormData.price ? parseFloat(roomFormData.price) : undefined,
+ description: roomFormData.description || undefined,
+ capacity: roomFormData.capacity ? parseInt(roomFormData.capacity) : undefined,
+ room_size: roomFormData.room_size || undefined,
+ view: roomFormData.view || undefined,
+ amenities: Array.isArray(roomFormData.amenities) ? roomFormData.amenities : [],
+ };
+ const response = await roomService.createRoom(createData);
+ toast.success('Room added successfully');
+
+ if (response.data?.room) {
+ if (selectedFiles.length > 0) {
+ try {
+ setUploadingImages(true);
+ const uploadFormData = new FormData();
+ selectedFiles.forEach(file => {
+ uploadFormData.append('images', file);
+ });
+
+ await apiClient.post(`/rooms/${response.data.room.id}/images`, uploadFormData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+
+ toast.success('Images uploaded successfully');
+ setSelectedFiles([]);
+ const updatedRoom = await roomService.getRoomByNumber(response.data.room.room_number);
+ setEditingRoom(updatedRoom.data.room);
+ } catch (uploadError: any) {
+ toast.error(uploadError.response?.data?.message || 'Room created but failed to upload images');
+ const updatedRoom = await roomService.getRoomByNumber(response.data.room.room_number);
+ setEditingRoom(updatedRoom.data.room);
+ } finally {
+ setUploadingImages(false);
+ }
+ } else {
+ const updatedRoom = await roomService.getRoomByNumber(response.data.room.room_number);
+ setEditingRoom(updatedRoom.data.room);
+ }
+
+ setRoomFormData({
+ room_number: response.data.room.room_number,
+ floor: response.data.room.floor,
+ room_type_id: response.data.room.room_type_id,
+ status: response.data.room.status,
+ featured: response.data.room.featured,
+ price: response.data.room.price?.toString() || '',
+ description: response.data.room.description || '',
+ capacity: response.data.room.capacity?.toString() || '',
+ room_size: response.data.room.room_size || '',
+ view: response.data.room.view || '',
+ amenities: response.data.room.amenities || [],
+ });
+
+ await fetchRooms();
+ return;
+ }
+ }
+ setShowRoomModal(false);
+ resetRoomForm();
+ fetchRooms();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'An error occurred');
+ }
+ };
+
+ const handleEditRoom = async (room: Room) => {
+ setEditingRoom(room);
+
+ let amenitiesArray: string[] = [];
+ if (room.amenities) {
+ if (Array.isArray(room.amenities)) {
+ amenitiesArray = room.amenities;
+ } else if (typeof room.amenities === 'string') {
+ try {
+ const parsed = JSON.parse(room.amenities);
+ amenitiesArray = Array.isArray(parsed) ? parsed : [];
+ } catch {
+ const amenitiesStr: string = room.amenities;
+ amenitiesArray = amenitiesStr.split(',').map((a: string) => a.trim()).filter(Boolean);
+ }
+ }
+ }
+
+ setRoomFormData({
+ room_number: room.room_number,
+ floor: room.floor,
+ room_type_id: room.room_type_id,
+ status: room.status,
+ featured: room.featured,
+ price: room.price?.toString() || '',
+ description: room.description || '',
+ capacity: room.capacity?.toString() || '',
+ room_size: room.room_size || '',
+ view: room.view || '',
+ amenities: amenitiesArray,
+ });
+
+ setShowRoomModal(true);
+
+ try {
+ const fullRoom = await roomService.getRoomByNumber(room.room_number);
+ const roomData = fullRoom.data.room;
+
+ let updatedAmenitiesArray: string[] = [];
+ if (roomData.amenities) {
+ if (Array.isArray(roomData.amenities)) {
+ updatedAmenitiesArray = roomData.amenities;
+ } else if (typeof roomData.amenities === 'string') {
+ try {
+ const parsed = JSON.parse(roomData.amenities);
+ updatedAmenitiesArray = Array.isArray(parsed) ? parsed : [];
+ } catch {
+ const amenitiesStr: string = roomData.amenities;
+ updatedAmenitiesArray = amenitiesStr.split(',').map((a: string) => a.trim()).filter(Boolean);
+ }
+ }
+ }
+
+ setRoomFormData({
+ room_number: roomData.room_number,
+ floor: roomData.floor,
+ room_type_id: roomData.room_type_id,
+ status: roomData.status,
+ featured: roomData.featured,
+ price: roomData.price?.toString() || '',
+ description: roomData.description || '',
+ capacity: roomData.capacity?.toString() || '',
+ room_size: roomData.room_size || '',
+ view: roomData.view || '',
+ amenities: updatedAmenitiesArray,
+ });
+
+ setEditingRoom(roomData);
+ } catch (error) {
+ console.error('Failed to fetch full room details:', error);
+ }
+ };
+
+ const handleDeleteRoom = async (id: number) => {
+ if (!window.confirm('Are you sure you want to delete this room?')) return;
+
+ try {
+ await roomService.deleteRoom(id);
+ toast.success('Room deleted successfully');
+ setSelectedRooms(selectedRooms.filter(roomId => roomId !== id));
+ fetchRooms();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to delete room');
+ }
+ };
+
+ const handleBulkDeleteRooms = async () => {
+ if (selectedRooms.length === 0) {
+ toast.warning('Please select at least one room to delete');
+ return;
+ }
+
+ if (!window.confirm(`Are you sure you want to delete ${selectedRooms.length} room(s)?`)) return;
+
+ try {
+ await roomService.bulkDeleteRooms(selectedRooms);
+ toast.success(`Successfully deleted ${selectedRooms.length} room(s)`);
+ setSelectedRooms([]);
+ fetchRooms();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || error.response?.data?.detail || 'Unable to delete rooms');
+ }
+ };
+
+ const handleSelectRoom = (roomId: number) => {
+ setSelectedRooms(prev =>
+ prev.includes(roomId)
+ ? prev.filter(id => id !== roomId)
+ : [...prev, roomId]
+ );
+ };
+
+ const handleSelectAllRooms = () => {
+ if (selectedRooms.length === rooms.length) {
+ setSelectedRooms([]);
+ } else {
+ setSelectedRooms(rooms.map(room => room.id));
+ }
+ };
+
+ const resetRoomForm = () => {
+ setEditingRoom(null);
+ setRoomFormData({
+ room_number: '',
+ floor: 1,
+ room_type_id: 1,
+ status: 'available',
+ featured: false,
+ price: '',
+ description: '',
+ capacity: '',
+ room_size: '',
+ view: '',
+ amenities: [],
+ });
+ setSelectedFiles([]);
+ setUploadingImages(false);
+ };
+
+ const toggleAmenity = (amenity: string) => {
+ setRoomFormData(prev => ({
+ ...prev,
+ amenities: prev.amenities.includes(amenity)
+ ? prev.amenities.filter(a => a !== amenity)
+ : [...prev.amenities, amenity]
+ }));
+ };
+
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ if (e.target.files) {
+ const files = Array.from(e.target.files);
+ setSelectedFiles(files);
+ }
+ };
+
+ const handleUploadImages = async () => {
+ if (!editingRoom || selectedFiles.length === 0) return;
+
+ try {
+ setUploadingImages(true);
+ const formData = new FormData();
+ selectedFiles.forEach(file => {
+ formData.append('images', file);
+ });
+
+ await apiClient.post(`/rooms/${editingRoom.id}/images`, formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+
+ toast.success('Images uploaded successfully');
+ setSelectedFiles([]);
+ fetchRooms();
+
+ const response = await roomService.getRoomByNumber(editingRoom.room_number);
+ setEditingRoom(response.data.room);
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to upload images');
+ } finally {
+ setUploadingImages(false);
+ }
+ };
+
+ const handleDeleteImage = async (imageUrl: string) => {
+ if (!editingRoom) return;
+ if (!window.confirm('Are you sure you want to delete this image?')) return;
+
+ try {
+ let imagePath = imageUrl;
+ if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
+ try {
+ const url = new URL(imageUrl);
+ imagePath = url.pathname;
+ } catch (e) {
+ const match = imageUrl.match(/(\/uploads\/.*)/);
+ imagePath = match ? match[1] : imageUrl;
+ }
+ }
+
+ await apiClient.delete(`/rooms/${editingRoom.id}/images`, {
+ params: { image_url: imagePath },
+ });
+
+ toast.success('Image deleted successfully');
+ fetchRooms();
+
+ const response = await roomService.getRoomByNumber(editingRoom.room_number);
+ setEditingRoom(response.data.room);
+ } catch (error: any) {
+ console.error('Error deleting image:', error);
+ toast.error(error.response?.data?.message || error.response?.data?.detail || 'Unable to delete image');
+ }
+ };
+
+ const getRoomStatusBadge = (status: string) => {
+ const badges: Record = {
+ available: {
+ bg: 'bg-gradient-to-r from-emerald-50 to-green-50',
+ text: 'text-emerald-800',
+ label: 'Available',
+ border: 'border-emerald-200'
+ },
+ occupied: {
+ bg: 'bg-gradient-to-r from-blue-50 to-indigo-50',
+ text: 'text-blue-800',
+ label: 'Occupied',
+ border: 'border-blue-200'
+ },
+ maintenance: {
+ bg: 'bg-gradient-to-r from-amber-50 to-yellow-50',
+ text: 'text-amber-800',
+ label: 'Maintenance',
+ border: 'border-amber-200'
+ },
+ };
+ const badge = badges[status] || badges.available;
+ return (
+
+ {badge.label}
+
+ );
+ };
+
+ // Services Management Functions
+ useEffect(() => {
+ setServiceCurrentPage(1);
+ }, [serviceFilters]);
+
+ useEffect(() => {
+ if (activeTab === 'services') {
+ fetchServices();
+ }
+ }, [serviceFilters, serviceCurrentPage, activeTab]);
+
+ const fetchServices = async () => {
+ try {
+ setServicesLoading(true);
+ const response = await serviceService.getServices({
+ ...serviceFilters,
+ page: serviceCurrentPage,
+ limit: serviceItemsPerPage,
+ });
+ setServices(response.data.services);
+ if (response.data.pagination) {
+ setServiceTotalPages(response.data.pagination.totalPages);
+ setServiceTotalItems(response.data.pagination.total);
+ }
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to load services list');
+ } finally {
+ setServicesLoading(false);
+ }
+ };
+
+ const handleServiceSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ try {
+ if (editingService) {
+ await serviceService.updateService(editingService.id, serviceFormData);
+ toast.success('Service updated successfully');
+ } else {
+ await serviceService.createService(serviceFormData);
+ toast.success('Service added successfully');
+ }
+ setShowServiceModal(false);
+ resetServiceForm();
+ fetchServices();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'An error occurred');
+ }
+ };
+
+ const handleEditService = (service: Service) => {
+ setEditingService(service);
+ setServiceFormData({
+ name: service.name,
+ description: service.description || '',
+ price: service.price,
+ unit: service.unit || 'time',
+ status: service.status,
+ });
+ setShowServiceModal(true);
+ };
+
+ const handleDeleteService = async (id: number) => {
+ if (!window.confirm('Are you sure you want to delete this service?')) return;
+
+ try {
+ await serviceService.deleteService(id);
+ toast.success('Service deleted successfully');
+ fetchServices();
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Unable to delete service');
+ }
+ };
+
+ const resetServiceForm = () => {
+ setEditingService(null);
+ setServiceFormData({
+ name: '',
+ description: '',
+ price: 0,
+ unit: 'time',
+ status: 'active',
+ });
+ };
+
+ const getServiceStatusBadge = (status: string) => {
+ return status === 'active' ? (
+
+ Active
+
+ ) : (
+
+ Inactive
+
+ );
+ };
+
+ const tabs = [
+ { id: 'overview' as ReceptionTab, label: 'Overview', icon: LogIn },
+ { id: 'check-in' as ReceptionTab, label: 'Check-in', icon: LogIn },
+ { id: 'check-out' as ReceptionTab, label: 'Check-out', icon: LogOut },
+ { id: 'bookings' as ReceptionTab, label: 'Bookings', icon: Calendar },
+ { id: 'rooms' as ReceptionTab, label: 'Rooms', icon: Hotel },
+ { id: 'services' as ReceptionTab, label: 'Services', icon: Wrench },
+ ];
+
+ return (
+
+
+ {/* Luxury Header */}
+
+
+
+
+
+
+
+
+
+ Reception Dashboard
+
+
+
+
+ Manage guest check-in and check-out processes
+
+
+
+
+
+ {/* Premium Tab Navigation */}
+
+
+ {tabs.map((tab) => {
+ const Icon = tab.icon;
+ const isActive = activeTab === tab.id;
+ return (
+
+ );
+ })}
+
+
+
+
+
+ {/* Overview Tab */}
+ {activeTab === 'overview' && (
+
+
setActiveTab('check-in')}
+ className="group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-emerald-100/50 p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:border-emerald-300/60 overflow-hidden"
+ >
+
+
+
+
+ Process guest check-in and room assignment
+
+
+
+
+
+
+
setActiveTab('check-out')}
+ className="group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-teal-100/50 p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:border-teal-300/60 overflow-hidden"
+ >
+
+
+
+
+ Process guest check-out and payment collection
+
+
+
+ Start Check-out
+
+
+
+
+
+
+
+
setActiveTab('bookings')}
+ className="group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-blue-100/50 p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:border-blue-300/60 overflow-hidden"
+ >
+
+
+
+
+ Manage and track all hotel bookings
+
+
+
+ Manage Bookings
+
+
+
+
+
+
+
+
setActiveTab('rooms')}
+ className="group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-amber-100/50 p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:border-amber-300/60 overflow-hidden"
+ >
+
+
+
+
+ Manage hotel room information and availability
+
+
+
+
+
+
+
setActiveTab('services')}
+ className="group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-purple-100/50 p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:border-purple-300/60 overflow-hidden"
+ >
+
+
+
+
+ Manage hotel services and amenities
+
+
+
+ Manage Services
+
+
+
+
+
+
+
+ )}
+
+ {/* Check-in Tab */}
+ {activeTab === 'check-in' && (
+
+ {checkInLoading && (
+
+ )}
+
+ {/* Section Header */}
+
+
+
+
+ Customer check-in process and room assignment
+
+
+
+
+ {/* Search Booking */}
+
+
+
+ 1
+
+ Search Booking
+
+
+
+
+ setCheckInBookingNumber(e.target.value)}
+ onKeyPress={(e) => e.key === 'Enter' && handleCheckInSearch()}
+ placeholder="Enter booking number"
+ className="w-full pl-12 pr-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100 transition-all duration-200 text-gray-700 placeholder-gray-400 font-medium shadow-sm hover:shadow-md"
+ />
+
+
+
+
+
+ {/* Booking Info */}
+ {checkInBooking && (
+ <>
+
+
+
+
+
+ Booking Information
+
+
+
+
+ Booking Number:
+ {checkInBooking.booking_number}
+
+
+ Customer:
+ {checkInBooking.user?.full_name}
+
+
+ Email:
+ {checkInBooking.user?.email}
+
+
+ Phone:
+ {checkInBooking.user?.phone_number || 'N/A'}
+
+
+
+
+ Room Type:
+ {checkInBooking.room?.room_type?.name || 'N/A'}
+
+
+ Check-in:
+ {checkInBooking.check_in_date ? parseDateLocal(checkInBooking.check_in_date).toLocaleDateString('en-US') : 'N/A'}
+
+
+ Check-out:
+ {checkInBooking.check_out_date ? parseDateLocal(checkInBooking.check_out_date).toLocaleDateString('en-US') : 'N/A'}
+
+
+ Number of Guests:
+ {checkInBooking.guest_count} guest(s)
+
+
+
+
+ {checkInBooking.status !== 'confirmed' && (
+
+
+
+
Warning
+
+ Booking status: {checkInBooking.status}. Only check-in confirmed bookings.
+
+
+
+ )}
+
+
+ {/* Assign Room */}
+
+
+
+
+
+ Assign Actual Room Number
+
+
+
+
setActualRoomNumber(e.target.value)}
+ placeholder="e.g: 101, 202, 305"
+ className="w-full px-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100 transition-all duration-200 text-gray-700 font-medium shadow-sm hover:shadow-md"
+ />
+
+ Enter the actual room number to assign to the guest
+
+
+
+
+ {/* Guest Information */}
+
+
+
+
+
+ Guest Information
+
+
+ {guests.map((guest, index) => (
+
+
+
+ {index === 0 ? 'Main Guest' : `Guest ${index + 1}`}
+ {index === 0 && *}
+
+ {index > 0 && (
+
+ )}
+
+
+
+ ))}
+
+
+
+
+ {/* Additional Charges */}
+
+
Additional Fees (if any)
+
+
+
+
{
+ setExtraPersons(parseInt(e.target.value) || 0);
+ calculateCheckInAdditionalFee();
+ }}
+ className="w-full px-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100 transition-all duration-200 text-gray-700 font-medium shadow-sm"
+ />
+
€50/person
+
+
+
+
{
+ setChildren(parseInt(e.target.value) || 0);
+ calculateCheckInAdditionalFee();
+ }}
+ className="w-full px-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-emerald-400 focus:ring-4 focus:ring-emerald-100 transition-all duration-200 text-gray-700 font-medium shadow-sm"
+ />
+
€25/child
+
+
+
+
+ {formatCurrency(calculateCheckInAdditionalFee())}
+
+
+
+
+
+ {/* Summary & Action */}
+
+
+
+
+
Confirm Check-in
+
+ Guest: {checkInBooking.user?.full_name} |
+ Room: {actualRoomNumber || 'Not assigned'}
+ {additionalFee > 0 && (
+ <> | Additional Fee: {formatCurrency(additionalFee)}>
+ )}
+
+
+
+
+
+ >
+ )}
+
+ {/* Empty State */}
+ {!checkInBooking && !checkInSearching && (
+
+
+
+
+
No booking selected
+
+ Please enter booking number above to start check-in process
+
+
+ )}
+
+ )}
+
+ {/* Check-out Tab */}
+ {activeTab === 'check-out' && (
+
+ {checkOutLoading && (
+
+ )}
+
+ {/* Section Header */}
+
+
+
+
+ Payment and check-out process for guests
+
+
+
+
+ {/* Search Booking */}
+ {!showInvoice && (
+
+
+
+ 1
+
+ Search Booking
+
+
+
+
+ setCheckOutBookingNumber(e.target.value)}
+ onKeyPress={(e) => e.key === 'Enter' && handleCheckOutSearch()}
+ placeholder="Enter booking number or room number"
+ className="w-full pl-12 pr-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-teal-400 focus:ring-4 focus:ring-teal-100 transition-all duration-200 text-gray-700 placeholder-gray-400 font-medium shadow-sm hover:shadow-md"
+ />
+
+
+
+
+ )}
+
+ {/* Invoice */}
+ {checkOutBooking && !showInvoice && (
+ <>
+ {/* Booking Info */}
+
+
+
+
+
+ Booking Information
+
+
+
+
+ Booking number:
+ {checkOutBooking.booking_number}
+
+
+ Customer:
+ {checkOutBooking.user?.full_name}
+
+
+ Room number:
+ {checkOutBooking.room?.room_number}
+
+
+
+
+ Check-in:
+ {checkOutBooking.check_in_date ? parseDateLocal(checkOutBooking.check_in_date).toLocaleDateString('en-US') : 'N/A'}
+
+
+ Check-out:
+ {checkOutBooking.check_out_date ? parseDateLocal(checkOutBooking.check_out_date).toLocaleDateString('en-US') : 'N/A'}
+
+
+ Nights:
+
+ {checkOutBooking.check_in_date && checkOutBooking.check_out_date
+ ? Math.ceil((parseDateLocal(checkOutBooking.check_out_date).getTime() - parseDateLocal(checkOutBooking.check_in_date).getTime()) / (1000 * 60 * 60 * 24))
+ : 0} night(s)
+
+
+
+
+
+
+ {/* Bill Details */}
+
+
+
+
+
+ Invoice Details
+
+
+ {/* Room Fee */}
+
+
Room Fee
+
+
+ {checkOutBooking.room?.room_type?.name || 'Room'}
+ {formatCurrency(calculateRoomFee())}
+
+
+
+
+ {/* Service Fee */}
+ {checkOutServices.length > 0 && (
+
+
Services Used
+
+ {checkOutServices.map((service, index) => (
+
+
+ {service.service_name} (x{service.quantity})
+
+ {formatCurrency(service.total)}
+
+ ))}
+
+ Total services:
+ {formatCurrency(calculateServiceFee())}
+
+
+
+ )}
+
+ {/* Additional Fee */}
+ {calculateCheckOutAdditionalFee() > 0 && (
+
+
Additional Fees
+
+
+ Extra person/children fee
+ {formatCurrency(calculateCheckOutAdditionalFee())}
+
+
+
+ )}
+
+ {/* Discount */}
+
+
Discount
+ setDiscount(parseFloat(e.target.value) || 0)}
+ placeholder="Enter discount amount"
+ className="w-full px-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-teal-400 focus:ring-4 focus:ring-teal-100 transition-all duration-200 text-gray-700 font-medium shadow-sm"
+ />
+
+
+ {/* Summary */}
+
+
+ Subtotal:
+ {formatCurrency(calculateSubtotal())}
+
+ {discount > 0 && (
+
+ Discount:
+ -{formatCurrency(discount)}
+
+ )}
+
+ Total:
+ {formatCurrency(calculateTotal())}
+
+
+ Deposit paid:
+ -{formatCurrency(calculateDeposit())}
+
+
+ Remaining payment:
+ {formatCurrency(calculateRemaining())}
+
+
+
+
+ {/* Payment Method */}
+
+
+
+
+
+ Payment Method
+
+
+
+
+
+
+
+ {/* Action */}
+
+
+
+
+
Confirm Check-out
+
+ Total payment: {formatCurrency(calculateRemaining())}
+
+
+
+
+
+ >
+ )}
+
+ {/* Invoice Display */}
+ {showInvoice && checkOutBooking && (
+
+
+
PAYMENT INVOICE
+
Check-out successful
+
+
+
+
+
+
Booking number:
+
{checkOutBooking.booking_number}
+
+
+
Check-out date:
+
{new Date().toLocaleString('en-US')}
+
+
+
Customer:
+
{checkOutBooking.user?.full_name}
+
+
+
Payment method:
+
+ {paymentMethod === 'cash' ? 'Cash' : 'Stripe'}
+
+
+
+
+
+
+
+ Total payment:
+ {formatCurrency(calculateRemaining())}
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Empty State */}
+ {!checkOutBooking && !checkOutSearching && !showInvoice && (
+
+
+
+
+
No booking selected
+
+ Please enter booking number to start check-out process
+
+
+ )}
+
+ )}
+
+ {/* Bookings Tab */}
+ {activeTab === 'bookings' && (
+
+ {bookingsLoading &&
}
+
+ {/* Section Header */}
+
+
+
+
+
+
+
Bookings Management
+
+
+ Manage and track all hotel bookings with precision
+
+
+
+
+ {/* Filters */}
+
+
+
+
+ setBookingFilters({ ...bookingFilters, search: e.target.value })}
+ className="w-full pl-12 pr-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all duration-200 text-gray-700 placeholder-gray-400 font-medium shadow-sm hover:shadow-md"
+ />
+
+
+
+
+
+ {/* Bookings Table */}
+
+
+
+
+
+ | Booking Number |
+ Customer |
+ Room |
+ Check-in/out |
+ Total Price |
+ Status |
+ Actions |
+
+
+
+ {bookings.map((booking) => (
+
+ |
+
+ {booking.booking_number}
+
+ |
+
+ {booking.guest_info?.full_name || booking.user?.name}
+ {booking.guest_info?.email || booking.user?.email}
+ |
+
+
+ Room {booking.room?.room_number}
+ •
+ {booking.room?.room_type?.name}
+
+ |
+
+
+ {parseDateLocal(booking.check_in_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
+
+
+ → {parseDateLocal(booking.check_out_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
+
+ |
+
+
+ {formatCurrency(booking.total_price)}
+
+ |
+
+ {getBookingStatusBadge(booking.status)}
+ |
+
+
+
+ {booking.status === 'pending' && (
+ <>
+
+
+ >
+ )}
+ {booking.status === 'confirmed' && (
+
+ )}
+
+ |
+
+ ))}
+
+
+
+
+
+
+ {/* Booking Detail Modal */}
+ {showBookingDetailModal && selectedBooking && (
+
+
+
+
+
+
Booking Details
+
Comprehensive booking information
+
+
+
+
+
+
+
+
+
+
+
{selectedBooking.booking_number}
+
+
+
+
{getBookingStatusBadge(selectedBooking.status)}
+
+
+
+
+
+
+
{selectedBooking.guest_info?.full_name || selectedBooking.user?.name}
+
{selectedBooking.guest_info?.email || selectedBooking.user?.email}
+
{selectedBooking.guest_info?.phone || selectedBooking.user?.phone}
+
+
+
+
+
+
+ Room {selectedBooking.room?.room_number}
+ •
+ {selectedBooking.room?.room_type?.name}
+
+
+
+
+
+
+
{parseDateLocal(selectedBooking.check_in_date).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}
+
+
+
+
{parseDateLocal(selectedBooking.check_out_date).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}
+
+
+
+
+
+
{selectedBooking.guest_count} guest{selectedBooking.guest_count !== 1 ? 's' : ''}
+
+
+
+
+
+ {formatCurrency(selectedBooking.total_price)}
+
+
+
+ {selectedBooking.notes && (
+
+
+
{selectedBooking.notes}
+
+ )}
+
+
+
+
+
+
+
+
+ )}
+
+ )}
+
+ {/* Rooms Tab */}
+ {activeTab === 'rooms' && (
+
+ {roomsLoading &&
}
+
+ {/* Section Header */}
+
+
+
+
+
+
+
+
Room Management
+
+
+ Manage hotel room information and availability
+
+
+
+ {selectedRooms.length > 0 && (
+
+ )}
+
+
+
+
+
+ {/* Filters */}
+
+
+
+
+ setRoomFilters({ ...roomFilters, search: e.target.value })}
+ className="w-full pl-12 pr-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100 transition-all duration-200 text-gray-700 placeholder-gray-400 font-medium shadow-sm hover:shadow-md"
+ />
+
+
+
+
+
+
+ {/* Rooms Table */}
+
+
+ {/* Room Modal - Simplified version, will need full implementation */}
+ {showRoomModal && (
+
+
+
+
+
+ {editingRoom ? 'Update Room' : 'Add New Room'}
+
+
+ {editingRoom ? 'Modify room details and amenities' : 'Create a new luxurious room'}
+
+
+
+
+
+
+
+ {/* Image Upload Section - Only show when editing */}
+ {editingRoom && (
+
+
+
+
+ Room Images
+
+
+ {/* Helper function to normalize image URLs */}
+ {(() => {
+ // Get API base URL from environment or default
+ const apiBaseUrl = import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000';
+
+ const normalizeImageUrl = (img: string): string => {
+ if (!img) return '';
+ // If already a full URL, return as-is
+ if (img.startsWith('http://') || img.startsWith('https://')) {
+ return img;
+ }
+ // Normalize relative paths
+ const cleanPath = img.startsWith('/') ? img : `/${img}`;
+ return `${apiBaseUrl}${cleanPath}`;
+ };
+
+ // Get all images - prioritize room images over room type images
+ const roomImages = editingRoom.images || [];
+ const roomTypeImages = editingRoom.room_type?.images || [];
+
+ // Normalize all image paths for comparison
+ const normalizeForComparison = (img: string): string => {
+ if (!img) return '';
+ // Extract just the path part for comparison
+ if (img.startsWith('http://') || img.startsWith('https://')) {
+ try {
+ const url = new URL(img);
+ return url.pathname;
+ } catch {
+ const match = img.match(/(\/uploads\/.*)/);
+ return match ? match[1] : img;
+ }
+ }
+ return img.startsWith('/') ? img : `/${img}`;
+ };
+
+ // Combine images: room images first, then room type images that aren't already in room images
+ const normalizedRoomImages = roomImages.map(normalizeForComparison);
+ const allImages = [
+ ...roomImages,
+ ...roomTypeImages.filter(img => {
+ const normalized = normalizeForComparison(img);
+ return !normalizedRoomImages.includes(normalized);
+ })
+ ];
+
+ return (
+ <>
+ {/* Current Images */}
+ {allImages.length > 0 ? (
+
+
+ Current Images ({allImages.length}):
+
+
+ {allImages.map((img, index) => {
+ const imageUrl = normalizeImageUrl(img);
+ // Check if this is a room image by comparing normalized paths
+ const normalizedImg = normalizeForComparison(img);
+ const normalizedRoomImgs = roomImages.map(normalizeForComparison);
+ const isRoomImage = normalizedRoomImgs.includes(normalizedImg);
+
+ return (
+
+
+

{
+ // Fallback if image fails to load
+ (e.target as HTMLImageElement).src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgZmlsbD0iIzMzMzMzMyIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTQiIGZpbGw9IiM2NjY2NjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj5JbWFnZSBub3QgZm91bmQ8L3RleHQ+PC9zdmc+';
+ }}
+ />
+
+ {isRoomImage && (
+ <>
+
+ Room Image
+
+
+ >
+ )}
+
+ );
+ })}
+
+
+ ) : (
+
+
+
+ No images uploaded yet. Upload images below to display them here.
+
+
+ )}
+ >
+ );
+ })()}
+
+ {/* Upload New Images */}
+
+
+
+
+
+
+
+
+ {selectedFiles.length > 0 && (
+
+ {selectedFiles.length} file(s) selected
+
+ )}
+
+
+ )}
+
+ {/* Bottom Cancel/Close Button */}
+
+
+
+
+
+ )}
+
+ )}
+
+ {/* Services Tab */}
+ {activeTab === 'services' && (
+
+ {servicesLoading &&
}
+
+ {/* Section Header */}
+
+
+
+
+
+
+
+
Service Management
+
+
+ Manage hotel services and amenities
+
+
+
+
+
+
+ {/* Filters */}
+
+
+
+
+ setServiceFilters({ ...serviceFilters, search: e.target.value })}
+ className="w-full pl-12 pr-4 py-3.5 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200 text-gray-700 placeholder-gray-400 font-medium shadow-sm hover:shadow-md"
+ />
+
+
+
+
+
+ {/* Services Table */}
+
+
+
+
+
+ | Service Name |
+ Description |
+ Price |
+ Unit |
+ Status |
+ Actions |
+
+
+
+ {services.map((service) => (
+
+ |
+ {service.name}
+ |
+
+ {service.description}
+ |
+
+
+ {formatCurrency(service.price)}
+
+ |
+
+ {service.unit}
+ |
+
+ {getServiceStatusBadge(service.status)}
+ |
+
+
+
+
+
+ |
+
+ ))}
+
+
+
+
+
+
+ {/* Service Modal */}
+ {showServiceModal && (
+
+
+ {/* Modal Header */}
+
+
+
+
+ {editingService ? 'Update Service' : 'Add New Service'}
+
+
+ {editingService ? 'Modify service information' : 'Create a new service'}
+
+
+
+
+
+
+ {/* Modal Content */}
+
+
+
+ )}
+
+ )}
+
+
+ );
+};
+
+export default ReceptionDashboardPage;
+
diff --git a/Frontend/src/pages/admin/SettingsPage.tsx b/Frontend/src/pages/admin/SettingsPage.tsx
new file mode 100644
index 00000000..e4e64898
--- /dev/null
+++ b/Frontend/src/pages/admin/SettingsPage.tsx
@@ -0,0 +1,937 @@
+import React, { useEffect, useState } from 'react';
+import {
+ Settings,
+ Shield,
+ DollarSign,
+ CreditCard,
+ Save,
+ Info,
+ Globe,
+ SlidersHorizontal,
+ Eye,
+ EyeOff,
+ Lock,
+ Key,
+ Cookie,
+ Coins,
+ Sparkles
+} from 'lucide-react';
+import { toast } from 'react-toastify';
+import adminPrivacyService, {
+ CookieIntegrationSettings,
+ CookieIntegrationSettingsResponse,
+ CookiePolicySettings,
+ CookiePolicySettingsResponse,
+} from '../../services/api/adminPrivacyService';
+import systemSettingsService, {
+ PlatformCurrencyResponse,
+ StripeSettingsResponse,
+ UpdateStripeSettingsRequest,
+} from '../../services/api/systemSettingsService';
+import { useCurrency } from '../../contexts/CurrencyContext';
+import { Loading } from '../../components/common';
+import { getCurrencySymbol } from '../../utils/format';
+
+type SettingsTab = 'general' | 'cookie' | 'currency' | 'payment';
+
+const SettingsPage: React.FC = () => {
+ const { currency, supportedCurrencies, refreshCurrency } = useCurrency();
+ const [activeTab, setActiveTab] = useState('general');
+
+ // Cookie Settings State
+ const [policy, setPolicy] = useState({
+ analytics_enabled: true,
+ marketing_enabled: true,
+ preferences_enabled: true,
+ });
+ const [integrations, setIntegrations] = useState({
+ ga_measurement_id: '',
+ fb_pixel_id: '',
+ });
+ const [policyMeta, setPolicyMeta] = useState<
+ Pick | null
+ >(null);
+ const [integrationMeta, setIntegrationMeta] = useState<
+ Pick | null
+ >(null);
+
+ // Currency Settings State
+ const [selectedCurrency, setSelectedCurrency] = useState(currency);
+ const [currencyInfo, setCurrencyInfo] = useState(null);
+
+ // Stripe Settings State
+ const [stripeSettings, setStripeSettings] = useState(null);
+ const [formData, setFormData] = useState({
+ stripe_secret_key: '',
+ stripe_publishable_key: '',
+ stripe_webhook_secret: '',
+ });
+ const [showSecretKey, setShowSecretKey] = useState(false);
+ const [showWebhookSecret, setShowWebhookSecret] = useState(false);
+
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+
+ const currencyNames: Record = {
+ VND: 'Vietnamese Dong',
+ USD: 'US Dollar',
+ EUR: 'Euro',
+ GBP: 'British Pound',
+ JPY: 'Japanese Yen',
+ CNY: 'Chinese Yuan',
+ KRW: 'South Korean Won',
+ SGD: 'Singapore Dollar',
+ THB: 'Thai Baht',
+ AUD: 'Australian Dollar',
+ CAD: 'Canadian Dollar',
+ };
+
+ const getCurrencyDisplayName = (code: string): string => {
+ const name = currencyNames[code] || code;
+ const symbol = getCurrencySymbol(code);
+ return `${name} (${symbol})`;
+ };
+
+ // Load all settings
+ useEffect(() => {
+ loadAllSettings();
+ }, []);
+
+ useEffect(() => {
+ setSelectedCurrency(currency);
+ }, [currency]);
+
+ const loadAllSettings = async () => {
+ try {
+ setLoading(true);
+ const [policyRes, integrationRes, currencyRes, stripeRes] = await Promise.all([
+ adminPrivacyService.getCookiePolicy(),
+ adminPrivacyService.getIntegrations(),
+ systemSettingsService.getPlatformCurrency(),
+ systemSettingsService.getStripeSettings(),
+ ]);
+
+ setPolicy(policyRes.data);
+ setPolicyMeta({
+ updated_at: policyRes.updated_at,
+ updated_by: policyRes.updated_by,
+ });
+ setIntegrations(integrationRes.data || {});
+ setIntegrationMeta({
+ updated_at: integrationRes.updated_at,
+ updated_by: integrationRes.updated_by,
+ });
+
+ setCurrencyInfo(currencyRes.data);
+ setSelectedCurrency(currencyRes.data.currency);
+
+ setStripeSettings(stripeRes.data);
+ setFormData({
+ stripe_secret_key: '',
+ stripe_publishable_key: stripeRes.data.stripe_publishable_key || '',
+ stripe_webhook_secret: '',
+ });
+ } catch (error: any) {
+ toast.error(error.message || 'Failed to load settings');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Cookie Settings Handlers
+ const handleToggle = (key: keyof CookiePolicySettings) => {
+ setPolicy((prev) => ({
+ ...prev,
+ [key]: !prev[key],
+ }));
+ };
+
+ const handleSaveCookie = async () => {
+ try {
+ setSaving(true);
+ const [policyRes, integrationRes] = await Promise.all([
+ adminPrivacyService.updateCookiePolicy(policy),
+ adminPrivacyService.updateIntegrations(integrations),
+ ]);
+ setPolicy(policyRes.data);
+ setPolicyMeta({
+ updated_at: policyRes.updated_at,
+ updated_by: policyRes.updated_by,
+ });
+ setIntegrations(integrationRes.data || {});
+ setIntegrationMeta({
+ updated_at: integrationRes.updated_at,
+ updated_by: integrationRes.updated_by,
+ });
+ toast.success('Cookie policy and integrations updated successfully');
+ } catch (error: any) {
+ toast.error(error.message || 'Failed to update cookie settings');
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ // Currency Settings Handlers
+ const handleSaveCurrency = async () => {
+ try {
+ setSaving(true);
+ await systemSettingsService.updatePlatformCurrency(selectedCurrency);
+ await refreshCurrency();
+ await loadAllSettings();
+ toast.success('Platform currency updated successfully');
+ } catch (error: any) {
+ toast.error(error.message || 'Failed to update platform currency');
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ // Stripe Settings Handlers
+ const handleSaveStripe = async () => {
+ try {
+ setSaving(true);
+ const updateData: UpdateStripeSettingsRequest = {};
+
+ if (formData.stripe_secret_key && formData.stripe_secret_key.trim()) {
+ updateData.stripe_secret_key = formData.stripe_secret_key.trim();
+ }
+
+ if (formData.stripe_publishable_key && formData.stripe_publishable_key.trim()) {
+ updateData.stripe_publishable_key = formData.stripe_publishable_key.trim();
+ }
+
+ if (formData.stripe_webhook_secret && formData.stripe_webhook_secret.trim()) {
+ updateData.stripe_webhook_secret = formData.stripe_webhook_secret.trim();
+ }
+
+ await systemSettingsService.updateStripeSettings(updateData);
+ await loadAllSettings();
+
+ setFormData({
+ ...formData,
+ stripe_secret_key: '',
+ stripe_webhook_secret: '',
+ });
+
+ toast.success('Stripe settings updated successfully');
+ } catch (error: any) {
+ toast.error(
+ error.response?.data?.message ||
+ error.response?.data?.detail ||
+ 'Failed to update Stripe settings'
+ );
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ if (loading) {
+ return ;
+ }
+
+ const tabs = [
+ { id: 'general' as SettingsTab, label: 'Overview', icon: Settings },
+ { id: 'cookie' as SettingsTab, label: 'Privacy & Cookies', icon: Cookie },
+ { id: 'currency' as SettingsTab, label: 'Currency', icon: Coins },
+ { id: 'payment' as SettingsTab, label: 'Payment', icon: CreditCard },
+ ];
+
+ return (
+
+
+ {/* Luxury Header */}
+
+ {/* Background decorative elements */}
+
+
+
+
+
+
+
+
+ Settings Dashboard
+
+
+
+
+ Centralized control center for all platform configurations and system settings
+
+
+
+
+
+ {/* Premium Tab Navigation */}
+
+
+ {tabs.map((tab) => {
+ const Icon = tab.icon;
+ const isActive = activeTab === tab.id;
+ return (
+
+ );
+ })}
+
+
+
+
+
+ {/* General Overview Tab */}
+ {activeTab === 'general' && (
+
+
setActiveTab('cookie')}
+ className="group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-blue-100/50 p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:border-blue-300/60 overflow-hidden"
+ >
+
+
+
+
+ Manage cookie preferences, analytics integrations, and privacy controls
+
+
+
+ Last updated
+
+ {policyMeta?.updated_at
+ ? new Date(policyMeta.updated_at).toLocaleDateString()
+ : 'Never'}
+
+
+
+
+
+
+
+
setActiveTab('currency')}
+ className="group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-emerald-100/50 p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:border-emerald-300/60 overflow-hidden"
+ >
+
+
+
+
+ Configure platform-wide currency settings and display preferences
+
+
+
+ Current currency
+
+ {currency}
+
+
+
+
+
+
+
+
setActiveTab('payment')}
+ className="group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-indigo-100/50 p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:border-indigo-300/60 overflow-hidden"
+ >
+
+
+
+
+ Manage Stripe payment processing credentials and webhook settings
+
+
+
+ Status
+
+ {stripeSettings?.has_secret_key && stripeSettings?.has_publishable_key ? '✓ Configured' : 'Not configured'}
+
+
+
+
+
+
+
+ )}
+
+ {/* Cookie & Privacy Tab */}
+ {activeTab === 'cookie' && (
+
+ {/* Section Header */}
+
+
+
+
+
+
+
+
Privacy & Cookie Controls
+
+
+ Define which cookie categories are allowed in the application. Control user consent preferences and analytics integrations.
+
+
+
+
+
+
+ {/* Info Card */}
+
+
+
+
+
+
+ How these settings affect the guest experience
+
+
+ Disabling a category here prevents it from being offered to guests as part of the cookie consent flow. For example, if marketing cookies are disabled, the website should not load marketing pixels even if a guest previously opted in.
+
+
+
+
+
+ {/* Cookie Toggles */}
+
+ {[
+ { key: 'analytics_enabled' as keyof CookiePolicySettings, label: 'Analytics Cookies', desc: 'Anonymous traffic and performance measurement', color: 'emerald', icon: SlidersHorizontal },
+ { key: 'marketing_enabled' as keyof CookiePolicySettings, label: 'Marketing Cookies', desc: 'Personalised offers and remarketing campaigns', color: 'pink', icon: SlidersHorizontal },
+ { key: 'preferences_enabled' as keyof CookiePolicySettings, label: 'Preference Cookies', desc: 'Remember guest choices like language and currency', color: 'indigo', icon: SlidersHorizontal },
+ ].map(({ key, label, desc, color, icon: Icon }) => {
+ const isEnabled = policy[key];
+ const colorClasses = {
+ emerald: {
+ bg: 'from-emerald-500 to-emerald-600',
+ shadow: 'shadow-emerald-500/30',
+ iconBg: 'bg-emerald-50 border-emerald-100',
+ iconColor: 'text-emerald-600',
+ },
+ pink: {
+ bg: 'from-pink-500 to-pink-600',
+ shadow: 'shadow-pink-500/30',
+ iconBg: 'bg-pink-50 border-pink-100',
+ iconColor: 'text-pink-600',
+ },
+ indigo: {
+ bg: 'from-indigo-500 to-indigo-600',
+ shadow: 'shadow-indigo-500/30',
+ iconBg: 'bg-indigo-50 border-indigo-100',
+ iconColor: 'text-indigo-600',
+ },
+ }[color];
+
+ return (
+
+
+
+
+
+
+
+
+ );
+ })}
+
+
+ {/* Integration IDs */}
+
+
+
+
+
+
+
+
Third-Party Integrations
+
+ Configure IDs for supported analytics and marketing platforms. The application will only load these when both the policy and user consent allow it.
+
+
+
+
+
+
+
+
+ setIntegrations((prev) => ({
+ ...prev,
+ ga_measurement_id: e.target.value || undefined,
+ }))
+ }
+ placeholder="G-XXXXXXXXXX"
+ className="w-full px-4 py-3 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm"
+ />
+
+ Example: G-ABCDE12345
+
+
+
+
+
+
+ setIntegrations((prev) => ({
+ ...prev,
+ fb_pixel_id: e.target.value || undefined,
+ }))
+ }
+ placeholder="123456789012345"
+ className="w-full px-4 py-3 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm"
+ />
+
+ Numeric ID from your Meta Pixel configuration
+
+
+
+
+
+
+ )}
+
+ {/* Currency Tab */}
+ {activeTab === 'currency' && (
+
+ {/* Section Header */}
+
+
+
+
+
+
+
+
Platform Currency Settings
+
+
+ Set the default currency that will be displayed across all dashboards and pages throughout the platform
+
+
+
+
+
+
+ {/* Info Card */}
+
+
+
+
+
+
+ How platform currency works
+
+
+ The platform currency you select here will be used to display all prices, amounts, and financial information across the entire application. This includes customer-facing pages, admin dashboards, reports, and booking pages. All users will see prices in the selected currency.
+
+
+
+
+
+ {/* Currency Selection */}
+
+
+
+
+
+
+
+
Select Platform Currency
+
+ Choose the currency that will be used throughout the platform for displaying all monetary values
+
+
+
+
+
+
+
+
+
+ Current platform currency:
+ {currency}
+
+
+
+
+
+
+ )}
+
+ {/* Payment Tab */}
+ {activeTab === 'payment' && (
+
+ {/* Section Header */}
+
+
+
+
+
+
+
+
Stripe Payment Settings
+
+
+ Configure your Stripe account credentials to enable secure card payments. All payments will be processed through your Stripe account.
+
+
+
+
+
+
+ {/* Info Card */}
+
+
+
+
+
+
+ How Stripe payments work
+
+
+ Stripe handles all card payments securely. You need to provide your Stripe API keys from your Stripe Dashboard. The secret key is used to process payments on the backend, while the publishable key is used in the frontend payment forms. The webhook secret is required to verify webhook events from Stripe.
+
+
+
+ Note: Leave fields empty to keep existing values. Only enter new values when you want to update them.
+
+
+
+
+
+
+ {/* Stripe Settings Form */}
+
+
+
+
+
+ {/* Secret Key */}
+
+
+
+
+ setFormData({ ...formData, stripe_secret_key: e.target.value })
+ }
+ placeholder={
+ stripeSettings?.has_secret_key
+ ? `Current: ${stripeSettings.stripe_secret_key_masked || '****'}`
+ : 'sk_test_... or sk_live_...'
+ }
+ className="w-full px-4 py-3.5 pr-12 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm font-mono"
+ />
+
+
+
+
Used to process payments on the backend. Must start with
+
sk_
+ {stripeSettings?.has_secret_key && (
+
+
+ Currently configured
+
+ )}
+
+
+
+ {/* Publishable Key */}
+
+
+
+ setFormData({
+ ...formData,
+ stripe_publishable_key: e.target.value,
+ })
+ }
+ placeholder="pk_test_... or pk_live_..."
+ className="w-full px-4 py-3.5 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm font-mono"
+ />
+
+
Used in frontend payment forms. Must start with
+
pk_
+ {stripeSettings?.has_publishable_key && (
+
+
+ Currently configured
+
+ )}
+
+
+
+ {/* Webhook Secret */}
+
+
+
+
+ setFormData({
+ ...formData,
+ stripe_webhook_secret: e.target.value,
+ })
+ }
+ placeholder={
+ stripeSettings?.has_webhook_secret
+ ? `Current: ${stripeSettings.stripe_webhook_secret_masked || '****'}`
+ : 'whsec_...'
+ }
+ className="w-full px-4 py-3.5 pr-12 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm font-mono"
+ />
+
+
+
+
Used to verify webhook events from Stripe. Must start with
+
whsec_
+ {stripeSettings?.has_webhook_secret && (
+
+
+ Currently configured
+
+ )}
+
+
+
+
+ {/* Webhook URL Info */}
+
+
+
+
Webhook Endpoint URL
+
+ Configure this URL in your{' '}
+
+ Stripe Webhooks Dashboard
+
+ :
+
+
+ {window.location.origin}/api/payments/stripe/webhook
+
+
+ Make sure to subscribe to payment_intent.succeeded and{' '}
+ payment_intent.payment_failed events.
+
+
+
+
+
+
+ )}
+
+
+ );
+};
+
+export default SettingsPage;
diff --git a/Frontend/src/services/api/index.ts b/Frontend/src/services/api/index.ts
index a85d5c79..bd8852ac 100644
--- a/Frontend/src/services/api/index.ts
+++ b/Frontend/src/services/api/index.ts
@@ -35,6 +35,8 @@ export type * from './promotionService';
export { default as reportService } from './reportService';
export { default as dashboardService } from './dashboardService';
export { default as auditService } from './auditService';
+export { default as pageContentService } from './pageContentService';
export type { CustomerDashboardStats, CustomerDashboardResponse } from './dashboardService';
export type * from './reportService';
export type * from './auditService';
+export type * from './pageContentService';
diff --git a/Frontend/src/services/api/pageContentService.ts b/Frontend/src/services/api/pageContentService.ts
new file mode 100644
index 00000000..c005fb1a
--- /dev/null
+++ b/Frontend/src/services/api/pageContentService.ts
@@ -0,0 +1,168 @@
+import apiClient from './apiClient';
+
+export type PageType = 'home' | 'contact' | 'about' | 'footer' | 'seo';
+
+export interface PageContent {
+ id?: number;
+ page_type: PageType;
+ title?: string;
+ subtitle?: string;
+ description?: string;
+ content?: string;
+ meta_title?: string;
+ meta_description?: string;
+ meta_keywords?: string;
+ og_title?: string;
+ og_description?: string;
+ og_image?: string;
+ canonical_url?: string;
+ contact_info?: {
+ phone?: string;
+ email?: string;
+ address?: string;
+ };
+ map_url?: string;
+ social_links?: {
+ facebook?: string;
+ twitter?: string;
+ instagram?: string;
+ linkedin?: string;
+ youtube?: string;
+ };
+ footer_links?: {
+ quick_links?: Array<{ label: string; url: string }>;
+ support_links?: Array<{ label: string; url: string }>;
+ };
+ hero_title?: string;
+ hero_subtitle?: string;
+ hero_image?: string;
+ story_content?: string;
+ values?: Array<{ icon?: string; title: string; description: string }>;
+ features?: Array<{ icon?: string; title: string; description: string }>;
+ is_active?: boolean;
+ created_at?: string;
+ updated_at?: string;
+}
+
+export interface PageContentResponse {
+ status: string;
+ data: {
+ page_content?: PageContent | null;
+ page_contents?: PageContent[];
+ };
+ message?: string;
+}
+
+export interface UpdatePageContentData {
+ title?: string;
+ subtitle?: string;
+ description?: string;
+ content?: string;
+ meta_title?: string;
+ meta_description?: string;
+ meta_keywords?: string;
+ og_title?: string;
+ og_description?: string;
+ og_image?: string;
+ canonical_url?: string;
+ contact_info?: {
+ phone?: string;
+ email?: string;
+ address?: string;
+ };
+ map_url?: string;
+ social_links?: {
+ facebook?: string;
+ twitter?: string;
+ instagram?: string;
+ linkedin?: string;
+ youtube?: string;
+ };
+ footer_links?: {
+ quick_links?: Array<{ label: string; url: string }>;
+ support_links?: Array<{ label: string; url: string }>;
+ };
+ hero_title?: string;
+ hero_subtitle?: string;
+ hero_image?: string;
+ story_content?: string;
+ values?: Array<{ icon?: string; title: string; description: string }>;
+ features?: Array<{ icon?: string; title: string; description: string }>;
+ is_active?: boolean;
+}
+
+const pageContentService = {
+ /**
+ * Get all page contents
+ */
+ getAllPageContents: async (): Promise => {
+ const response = await apiClient.get('/page-content');
+ return response.data;
+ },
+
+ /**
+ * Get content for a specific page
+ */
+ getPageContent: async (pageType: PageType): Promise => {
+ const response = await apiClient.get(`/page-content/${pageType}`);
+ return response.data;
+ },
+
+ /**
+ * Update page content
+ */
+ updatePageContent: async (
+ pageType: PageType,
+ data: UpdatePageContentData
+ ): Promise => {
+ // Convert objects to JSON strings for fields that need it
+ const updateData: any = { ...data };
+
+ // Handle contact_info - ensure it has all required fields
+ if (data.contact_info) {
+ const contactInfo = {
+ phone: data.contact_info.phone || '',
+ email: data.contact_info.email || '',
+ address: data.contact_info.address || '',
+ };
+ updateData.contact_info = contactInfo; // Send as object, backend will convert to JSON
+ }
+
+ // Handle social_links - ensure it has all required fields
+ if (data.social_links) {
+ const socialLinks = {
+ facebook: data.social_links.facebook || '',
+ twitter: data.social_links.twitter || '',
+ instagram: data.social_links.instagram || '',
+ linkedin: data.social_links.linkedin || '',
+ youtube: data.social_links.youtube || '',
+ };
+ updateData.social_links = socialLinks; // Send as object, backend will convert to JSON
+ }
+
+ // Handle footer_links
+ if (data.footer_links) {
+ updateData.footer_links = {
+ quick_links: data.footer_links.quick_links || [],
+ support_links: data.footer_links.support_links || [],
+ };
+ }
+
+ // Handle values and features arrays
+ if (data.values) {
+ updateData.values = data.values; // Send as array, backend will convert to JSON
+ }
+ if (data.features) {
+ updateData.features = data.features; // Send as array, backend will convert to JSON
+ }
+
+ const response = await apiClient.put(
+ `/page-content/${pageType}`,
+ updateData
+ );
+ return response.data;
+ },
+};
+
+export default pageContentService;
+