diff --git a/Backend/src/system/routes/__pycache__/system_settings_routes.cpython-312.pyc b/Backend/src/system/routes/__pycache__/system_settings_routes.cpython-312.pyc index 10f7bfa2..c42aa0c2 100644 Binary files a/Backend/src/system/routes/__pycache__/system_settings_routes.cpython-312.pyc and b/Backend/src/system/routes/__pycache__/system_settings_routes.cpython-312.pyc differ diff --git a/Backend/src/system/routes/system_settings_routes.py b/Backend/src/system/routes/system_settings_routes.py index f9c5a1f2..863ef474 100644 --- a/Backend/src/system/routes/system_settings_routes.py +++ b/Backend/src/system/routes/system_settings_routes.py @@ -1435,6 +1435,10 @@ class UpdateCompanySettingsRequest(BaseModel): tax_rate: Optional[float] = None chat_working_hours_start: Optional[int] = None chat_working_hours_end: Optional[int] = None + bank_name: Optional[str] = None + bank_account_number: Optional[str] = None + bank_account_holder: Optional[str] = None + bank_code: Optional[str] = None @router.get("/company") async def get_company_settings( @@ -1452,6 +1456,10 @@ async def get_company_settings( "tax_rate", "chat_working_hours_start", "chat_working_hours_end", + "bank_name", + "bank_account_number", + "bank_account_holder", + "bank_code", ] settings_dict = {} @@ -1488,6 +1496,10 @@ async def get_company_settings( "tax_rate": float(settings_dict.get("tax_rate", 0)) if settings_dict.get("tax_rate") else 0.0, "chat_working_hours_start": int(settings_dict.get("chat_working_hours_start", 9)) if settings_dict.get("chat_working_hours_start") else 9, "chat_working_hours_end": int(settings_dict.get("chat_working_hours_end", 17)) if settings_dict.get("chat_working_hours_end") else 17, + "bank_name": settings_dict.get("bank_name", ""), + "bank_account_number": settings_dict.get("bank_account_number", ""), + "bank_account_holder": settings_dict.get("bank_account_holder", ""), + "bank_code": settings_dict.get("bank_code", ""), "updated_at": updated_at, "updated_by": updated_by, } @@ -1533,6 +1545,14 @@ async def update_company_settings( db_settings["chat_working_hours_start"] = str(request_data.chat_working_hours_start) if request_data.chat_working_hours_end is not None: db_settings["chat_working_hours_end"] = str(request_data.chat_working_hours_end) + if request_data.bank_name is not None: + db_settings["bank_name"] = request_data.bank_name + if request_data.bank_account_number is not None: + db_settings["bank_account_number"] = request_data.bank_account_number + if request_data.bank_account_holder is not None: + db_settings["bank_account_holder"] = request_data.bank_account_holder + if request_data.bank_code is not None: + db_settings["bank_code"] = request_data.bank_code for key, value in db_settings.items(): @@ -1581,7 +1601,7 @@ async def update_company_settings( updated_settings = {} - for key in ["company_name", "company_tagline", "company_logo_url", "company_favicon_url", "company_phone", "company_email", "company_address", "tax_rate", "chat_working_hours_start", "chat_working_hours_end"]: + for key in ["company_name", "company_tagline", "company_logo_url", "company_favicon_url", "company_phone", "company_email", "company_address", "tax_rate", "chat_working_hours_start", "chat_working_hours_end", "bank_name", "bank_account_number", "bank_account_holder", "bank_code"]: setting = db.query(SystemSettings).filter( SystemSettings.key == key ).first() @@ -1619,6 +1639,10 @@ async def update_company_settings( "tax_rate": float(updated_settings.get("tax_rate", 0)) if updated_settings.get("tax_rate") else 0.0, "chat_working_hours_start": int(updated_settings.get("chat_working_hours_start", 9)) if updated_settings.get("chat_working_hours_start") else 9, "chat_working_hours_end": int(updated_settings.get("chat_working_hours_end", 17)) if updated_settings.get("chat_working_hours_end") else 17, + "bank_name": updated_settings.get("bank_name", ""), + "bank_account_number": updated_settings.get("bank_account_number", ""), + "bank_account_holder": updated_settings.get("bank_account_holder", ""), + "bank_code": updated_settings.get("bank_code", ""), "updated_at": updated_at, "updated_by": updated_by, } @@ -1851,6 +1875,7 @@ class UpdateThemeSettingsRequest(BaseModel): theme_primary_light: Optional[str] = None theme_primary_dark: Optional[str] = None theme_primary_accent: Optional[str] = None + theme_layout_mode: Optional[str] = None # 'dark' or 'light' @router.get("/theme") async def get_theme_settings( @@ -1863,6 +1888,7 @@ async def get_theme_settings( "theme_primary_light", "theme_primary_dark", "theme_primary_accent", + "theme_layout_mode", ] settings_dict = {} @@ -1893,6 +1919,7 @@ async def get_theme_settings( "theme_primary_light": settings_dict.get("theme_primary_light", "#f5d76e"), "theme_primary_dark": settings_dict.get("theme_primary_dark", "#c9a227"), "theme_primary_accent": settings_dict.get("theme_primary_accent", "#e8c547"), + "theme_layout_mode": settings_dict.get("theme_layout_mode", "dark"), "updated_at": updated_at, "updated_by": updated_by, } @@ -1953,6 +1980,14 @@ async def update_theme_settings( ) db_settings["theme_primary_accent"] = request_data.theme_primary_accent + if request_data.theme_layout_mode is not None: + if request_data.theme_layout_mode not in ["dark", "light"]: + raise HTTPException( + status_code=400, + detail="Invalid theme_layout_mode. Must be 'dark' or 'light'" + ) + db_settings["theme_layout_mode"] = request_data.theme_layout_mode + # Update or create settings for key, value in db_settings.items(): setting = db.query(SystemSettings).filter( @@ -1975,7 +2010,7 @@ async def update_theme_settings( # Get updated settings updated_settings = {} - for key in ["theme_primary_color", "theme_primary_light", "theme_primary_dark", "theme_primary_accent"]: + for key in ["theme_primary_color", "theme_primary_light", "theme_primary_dark", "theme_primary_accent", "theme_layout_mode"]: setting = db.query(SystemSettings).filter( SystemSettings.key == key ).first() @@ -1988,6 +2023,7 @@ async def update_theme_settings( "theme_primary_light": "#f5d76e", "theme_primary_dark": "#c9a227", "theme_primary_accent": "#e8c547", + "theme_layout_mode": "dark", } updated_settings[key] = defaults.get(key) @@ -2009,6 +2045,7 @@ async def update_theme_settings( "theme_primary_light": updated_settings.get("theme_primary_light", "#f5d76e"), "theme_primary_dark": updated_settings.get("theme_primary_dark", "#c9a227"), "theme_primary_accent": updated_settings.get("theme_primary_accent", "#e8c547"), + "theme_layout_mode": updated_settings.get("theme_layout_mode", "dark"), "updated_at": updated_at, "updated_by": updated_by, } diff --git a/Frontend/src/features/bookings/services/bookingService.ts b/Frontend/src/features/bookings/services/bookingService.ts index 0da6d400..187953e1 100644 --- a/Frontend/src/features/bookings/services/bookingService.ts +++ b/Frontend/src/features/bookings/services/bookingService.ts @@ -307,22 +307,24 @@ export const notifyPayment = async ( export const generateQRCode = ( bookingNumber: string, - amount: number + amount: number, + bankCode?: string, + accountNumber?: string, + accountName?: string ): string => { + // If bank details are not provided, return empty string (QR code won't be generated) + if (!bankCode || !accountNumber || !accountName) { + return ''; + } - - - const bankCode = 'VCB'; - const accountNumber = '0123456789'; - const accountName = 'KHACH SAN ABC'; const transferContent = bookingNumber; - + // Generate QR code URL (using generic QR code service, not Vietnam-specific) + // Note: This is a placeholder - you may want to use a different QR code service + // that supports international bank transfers const qrUrl = - `https://img.vietqr.io/image/${bankCode}-${accountNumber}-compact2.jpg?` + - `amount=${amount}&` + - `addInfo=${encodeURIComponent(transferContent)}&` + - `accountName=${encodeURIComponent(accountName)}`; + `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=` + + encodeURIComponent(`Bank: ${bankCode}, Account: ${accountNumber}, Name: ${accountName}, Amount: ${amount}, Reference: ${transferContent}`); return qrUrl; }; diff --git a/Frontend/src/features/content/pages/AboutPage.tsx b/Frontend/src/features/content/pages/AboutPage.tsx index 4ee87ece..d0bb62e7 100644 --- a/Frontend/src/features/content/pages/AboutPage.tsx +++ b/Frontend/src/features/content/pages/AboutPage.tsx @@ -6,20 +6,26 @@ import { Phone, Mail, Linkedin, - Twitter + Twitter, + Clock } from 'lucide-react'; import * as LucideIcons from 'lucide-react'; import { Link } from 'react-router-dom'; import pageContentService from '../services/pageContentService'; import type { PageContent } from '../services/pageContentService'; import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext'; +import { useTheme } from '../../../shared/contexts/ThemeContext'; import { createSanitizedHtml } from '../../../shared/utils/htmlSanitizer'; +import { formatWorkingHours } from '../../../shared/utils/format'; +import { getThemeTextClasses } from '../../../shared/utils/themeUtils'; const AboutPage: React.FC = () => { const { settings } = useCompanySettings(); + const { theme } = useTheme(); const [pageContent, setPageContent] = useState(null); const [apiError, setApiError] = useState(false); const [loading, setLoading] = useState(true); + const textClasses = getThemeTextClasses(theme.theme_layout_mode); useEffect(() => { const fetchPageContent = async () => { @@ -194,7 +200,9 @@ const AboutPage: React.FC = () => {
- + {React.createElement(getIconComponent(pageContent?.about_hero_icon, Hotel), { + className: 'w-12 h-12 md:w-16 md:h-16 text-white drop-shadow-lg' + })}
@@ -445,13 +453,15 @@ const AboutPage: React.FC = () => {

{member.role}

{member.bio &&

{member.bio}

} {member.social_links && ( -
+
{member.social_links.linkedin && ( @@ -461,7 +471,9 @@ const AboutPage: React.FC = () => { href={member.social_links.twitter} target="_blank" rel="noopener noreferrer" - className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 hover:bg-[var(--luxury-gold)] hover:text-white transition-all duration-300 group-hover:scale-110" + className={`w-10 h-10 rounded-full ${theme.theme_layout_mode === 'light' + ? 'bg-gray-100 text-gray-700' + : 'bg-gray-800 text-gray-300'} flex items-center justify-center hover:bg-[var(--luxury-gold)] hover:text-white transition-all duration-300 group-hover:scale-110`} > @@ -614,7 +626,9 @@ const AboutPage: React.FC = () => { {displayAddress && (
- + {React.createElement(getIconComponent(pageContent?.about_contact_icons?.location, MapPin), { + className: 'w-8 h-8 text-white drop-shadow-md' + })}

Address @@ -633,7 +647,9 @@ const AboutPage: React.FC = () => { {displayPhone && (
- + {React.createElement(getIconComponent(pageContent?.about_contact_icons?.phone, Phone), { + className: 'w-8 h-8 text-white drop-shadow-md' + })}

Phone @@ -648,7 +664,9 @@ const AboutPage: React.FC = () => { {displayEmail && (
- + {React.createElement(getIconComponent(pageContent?.about_contact_icons?.email, Mail), { + className: 'w-8 h-8 text-white drop-shadow-md' + })}

Email @@ -660,6 +678,19 @@ const AboutPage: React.FC = () => {

)} + {settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined && ( +
+
+ +
+

+ Chat Support Hours +

+

+ {formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)} +

+
+ )}

)}
@@ -668,7 +699,9 @@ const AboutPage: React.FC = () => { className="group inline-flex items-center space-x-3 px-10 py-4 bg-gradient-to-r from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] text-white rounded-xl hover:shadow-2xl hover:shadow-[var(--luxury-gold)]/40 transition-all duration-500 font-medium text-lg tracking-wide relative overflow-hidden" > Explore Our Rooms - + {React.createElement(getIconComponent(pageContent?.about_learn_more_icon, Hotel), { + className: 'w-5 h-5 relative z-10 group-hover:translate-x-1 transition-transform duration-300' + })}
diff --git a/Frontend/src/features/content/pages/AccessibilityPage.tsx b/Frontend/src/features/content/pages/AccessibilityPage.tsx index 331be8f7..98a7564b 100644 --- a/Frontend/src/features/content/pages/AccessibilityPage.tsx +++ b/Frontend/src/features/content/pages/AccessibilityPage.tsx @@ -4,13 +4,20 @@ import { Link } from 'react-router-dom'; import pageContentService from '../services/pageContentService'; import type { PageContent } from '../services/pageContentService'; import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext'; +import { useTheme } from '../../../shared/contexts/ThemeContext'; import Loading from '../../../shared/components/Loading'; import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer'; +import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils'; const AccessibilityPage: React.FC = () => { const { settings } = useCompanySettings(); + const { theme } = useTheme(); const [pageContent, setPageContent] = useState(null); const [loading, setLoading] = useState(true); + + const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode); + const textClasses = getThemeTextClasses(theme.theme_layout_mode); + const cardClasses = getThemeCardClasses(theme.theme_layout_mode); useEffect(() => { const fetchPageContent = async () => { @@ -26,22 +33,26 @@ const AccessibilityPage: React.FC = () => { const tempDiv = document.createElement('div'); tempDiv.innerHTML = sanitizedContent; + // Get theme-aware colors + const isLightMode = theme.theme_layout_mode === 'light'; + const headingColor = isLightMode ? '#111827' : '#ffffff'; + const bodyColor = isLightMode ? '#111827' : '#d1d5db'; + const accentColor = '#d4af37'; // Gold color for links and strong + const allElements = tempDiv.querySelectorAll('*'); allElements.forEach((el) => { const htmlEl = el as HTMLElement; const tagName = htmlEl.tagName.toLowerCase(); - const currentColor = htmlEl.style.color; - if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') { - if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) { - htmlEl.style.color = '#ffffff'; - } else if (['strong', 'b'].includes(tagName)) { - htmlEl.style.color = '#d4af37'; - } else if (tagName === 'a') { - htmlEl.style.color = '#d4af37'; - } else { - htmlEl.style.color = '#d1d5db'; - } + // Override inline colors to use theme-aware colors + if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) { + htmlEl.style.color = headingColor; + } else if (['strong', 'b'].includes(tagName)) { + htmlEl.style.color = accentColor; + } else if (tagName === 'a') { + htmlEl.style.color = accentColor; + } else { + htmlEl.style.color = bodyColor; } }); @@ -83,7 +94,7 @@ const AccessibilityPage: React.FC = () => { if (!pageContent) { return ( -
{ >
-

Accessibility

-

This page is currently unavailable.

+

Accessibility

+

This page is currently unavailable.

{ } return ( -
{
-

+

{pageContent.title || 'Accessibility'}

{pageContent.subtitle && ( -

+

{pageContent.subtitle}

)}
-
+
No content available.

' + pageContent.content || pageContent.description || `

No content available.

` )} />
{settings.company_email && (
-

+

For accessibility inquiries, contact us at{' '} {settings.company_email} diff --git a/Frontend/src/features/content/pages/BlogDetailPage.tsx b/Frontend/src/features/content/pages/BlogDetailPage.tsx index 1cec6c05..7844fefa 100644 --- a/Frontend/src/features/content/pages/BlogDetailPage.tsx +++ b/Frontend/src/features/content/pages/BlogDetailPage.tsx @@ -4,13 +4,20 @@ import { Calendar, User, Tag, ArrowLeft, Eye, Share2, ArrowRight } from 'lucide- import blogService, { BlogPost } from '../services/blogService'; import Loading from '../../../shared/components/Loading'; import { createSanitizedHtml } from '../../../shared/utils/htmlSanitizer'; +import { useTheme } from '../../../shared/contexts/ThemeContext'; +import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils'; const BlogDetailPage: React.FC = () => { const { slug } = useParams<{ slug: string }>(); const navigate = useNavigate(); + const { theme } = useTheme(); const [post, setPost] = useState(null); const [loading, setLoading] = useState(true); const [relatedPosts, setRelatedPosts] = useState([]); + + const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode); + const textClasses = getThemeTextClasses(theme.theme_layout_mode); + const cardClasses = getThemeCardClasses(theme.theme_layout_mode); useEffect(() => { if (slug) { @@ -108,9 +115,9 @@ const BlogDetailPage: React.FC = () => { if (!post) { return ( -

)} {section.content && ( -

+

{section.content}

)} @@ -277,8 +283,8 @@ const BlogDetailPage: React.FC = () => {
{section.title {section.title && ( -
-

{section.title}

+
+

{section.title}

)}
@@ -373,7 +379,7 @@ const BlogDetailPage: React.FC = () => { {/* Related Posts */} {relatedPosts.length > 0 && (
-

Related Posts

+

Related Posts

{relatedPosts.map((relatedPost) => ( {
)}
-

+

{relatedPost.title}

{relatedPost.excerpt && ( -

+

{relatedPost.excerpt}

)} diff --git a/Frontend/src/features/content/pages/BlogPage.tsx b/Frontend/src/features/content/pages/BlogPage.tsx index 1e0130d7..56be6993 100644 --- a/Frontend/src/features/content/pages/BlogPage.tsx +++ b/Frontend/src/features/content/pages/BlogPage.tsx @@ -3,9 +3,12 @@ import { Link } from 'react-router-dom'; import { Calendar, User, Tag, Search, ArrowRight, Eye, Sparkles, BookOpen } from 'lucide-react'; import blogService, { BlogPost } from '../services/blogService'; import Loading from '../../../shared/components/Loading'; +import { useTheme } from '../../../shared/contexts/ThemeContext'; +import { getThemeBackgroundClasses, getThemeHeroBackgroundClasses, getThemeTextClasses, getThemeCardClasses, getThemeInputClasses } from '../../../shared/utils/themeUtils'; import Pagination from '../../../shared/components/Pagination'; const BlogPage: React.FC = () => { + const { theme } = useTheme(); const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); const [currentPage, setCurrentPage] = useState(1); @@ -67,10 +70,16 @@ const BlogPage: React.FC = () => { return ; } + const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode); + const heroBgClasses = getThemeHeroBackgroundClasses(theme.theme_layout_mode); + const textClasses = getThemeTextClasses(theme.theme_layout_mode); + const cardClasses = getThemeCardClasses(theme.theme_layout_mode); + const inputClasses = getThemeInputClasses(theme.theme_layout_mode); + return ( -
+
{/* Hero Section */} -
+
{/* Background Effects */}
@@ -83,18 +92,20 @@ const BlogPage: React.FC = () => {
-
+

- + Our Blog

-

+

Discover stories, insights, and updates from our luxury hotel

@@ -125,7 +136,7 @@ const BlogPage: React.FC = () => { setSearchTerm(e.target.value); setCurrentPage(1); }} - className="w-full pl-14 pr-5 py-4 bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] border border-[var(--luxury-gold)]/20 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)]/50 transition-all duration-300 backdrop-blur-sm font-light" + className={`w-full pl-14 pr-5 py-4 ${inputClasses} border border-[var(--luxury-gold)]/20 rounded-xl placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)]/50 transition-all duration-300 backdrop-blur-sm font-light`} />
@@ -139,7 +150,7 @@ const BlogPage: React.FC = () => { {/* Results Count */} {!loading && total > 0 && ( -
+
Showing {posts.length} of {total} posts
)} @@ -150,8 +161,8 @@ const BlogPage: React.FC = () => {
-

No blog posts found

-

Try adjusting your search or filters

+

No blog posts found

+

Try adjusting your search or filters

) : ( <> @@ -160,7 +171,7 @@ const BlogPage: React.FC = () => { {/* Premium Glow Effects */} @@ -204,15 +215,15 @@ const BlogPage: React.FC = () => { ))}
)} -

+

{post.title}

{post.excerpt && ( -

+

{post.excerpt}

)} -
+
{post.published_at && (
@@ -259,10 +270,10 @@ const BlogPage: React.FC = () => {
{allTags.length > 0 && (
-
+
-

Filter by Tags

+

Filter by Tags

@@ -362,7 +368,7 @@ const RoomFilter: React.FC = ({ onFilterChange }) => {
{checkInDate && !checkOutDate && ( -

+

Select check-out date (minimum 1 night stay)

)} @@ -417,7 +423,7 @@ const RoomFilter: React.FC = ({ onFilterChange }) => { {}
-
@@ -479,12 +485,12 @@ const RoomFilter: React.FC = ({ onFilterChange }) => { placeholder="No limit" inputMode="numeric" pattern="[0-9.]*" - className="w-full px-4 py-3.5 pl-10 bg-[#1a1a1a] border-2 - border-[var(--luxury-gold)]/30 rounded-lg text-white text-base + className={`w-full px-4 py-3.5 pl-10 ${inputClasses} border-2 + border-[var(--luxury-gold)]/30 rounded-lg text-base placeholder-gray-400 focus:ring-2 focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)] transition-all duration-300 font-normal tracking-wide - hover:border-[var(--luxury-gold)]/50" + hover:border-[var(--luxury-gold)]/50`} />
@@ -495,8 +501,8 @@ const RoomFilter: React.FC = ({ onFilterChange }) => {
{}
-
+
Loading amenities...
) : ( -
+
{availableAmenities.map((amenity) => (
- {filteredIcons.length > 0 ? ( + {allIcons.length === 0 ? ( +
+

Unable to load icons. Please refresh the page.

+

If the problem persists, check the browser console for errors.

+
+ ) : filteredIcons.length > 0 ? ( <> {!searchQuery.trim() && (
@@ -166,7 +205,7 @@ const IconPicker: React.FC = ({ value, onChange, label = 'Icon' const IconComponent = (LucideIcons as any)[iconName]; if (!IconComponent) return null; - const isSelected = value === iconName; + const isSelected = normalizedValue === iconName; const isPopular = !searchQuery.trim() && popularIcons.includes(iconName); try { @@ -208,8 +247,14 @@ const IconPicker: React.FC = ({ value, onChange, label = 'Icon' ) : (
-

No icons found matching "{searchQuery}"

-

Try a different search term

+ {searchQuery.trim() ? ( + <> +

No icons found matching "{searchQuery}"

+

Try a different search term

+ + ) : ( +

No icons available

+ )}
)}
diff --git a/Frontend/src/features/system/services/systemSettingsService.ts b/Frontend/src/features/system/services/systemSettingsService.ts index 134124eb..75762171 100644 --- a/Frontend/src/features/system/services/systemSettingsService.ts +++ b/Frontend/src/features/system/services/systemSettingsService.ts @@ -139,6 +139,10 @@ export interface CompanySettingsResponse { tax_rate: number; chat_working_hours_start: number; chat_working_hours_end: number; + bank_name?: string; + bank_account_number?: string; + bank_account_holder?: string; + bank_code?: string; updated_at?: string | null; updated_by?: string | null; }; @@ -154,6 +158,10 @@ export interface UpdateCompanySettingsRequest { tax_rate?: number; chat_working_hours_start?: number; chat_working_hours_end?: number; + bank_name?: string; + bank_account_number?: string; + bank_account_holder?: string; + bank_code?: string; } export interface UploadLogoResponse { @@ -219,6 +227,7 @@ export interface ThemeSettingsResponse { theme_primary_light: string; theme_primary_dark: string; theme_primary_accent: string; + theme_layout_mode: string; // 'dark' or 'light' updated_at?: string | null; updated_by?: string | null; }; @@ -230,6 +239,7 @@ export interface UpdateThemeSettingsRequest { theme_primary_light?: string; theme_primary_dark?: string; theme_primary_accent?: string; + theme_layout_mode?: string; // 'dark' or 'light' } export interface VerifyRecaptchaRequest { diff --git a/Frontend/src/pages/admin/CurrencySettingsPage.tsx b/Frontend/src/pages/admin/CurrencySettingsPage.tsx index b33cbd70..d9b4d673 100644 --- a/Frontend/src/pages/admin/CurrencySettingsPage.tsx +++ b/Frontend/src/pages/admin/CurrencySettingsPage.tsx @@ -14,7 +14,6 @@ const CurrencySettingsPage: React.FC = () => { const [saving, setSaving] = useState(false); const currencyNames: Record = { - VND: 'Vietnamese Dong', USD: 'US Dollar', EUR: 'Euro', GBP: 'British Pound', diff --git a/Frontend/src/pages/admin/PageContentDashboard.tsx b/Frontend/src/pages/admin/PageContentDashboard.tsx index 8add0e92..b67f5a04 100644 --- a/Frontend/src/pages/admin/PageContentDashboard.tsx +++ b/Frontend/src/pages/admin/PageContentDashboard.tsx @@ -250,6 +250,7 @@ const PageContentDashboard: React.FC = () => { services_section_button_text: contents.home.services_section_button_text || '', services_section_button_link: contents.home.services_section_button_link || '', services_section_limit: contents.home.services_section_limit || 6, + services_fallback_icon: contents.home.services_fallback_icon || 'Award', luxury_experiences_section_title: contents.home.luxury_experiences_section_title || '', luxury_experiences_section_subtitle: contents.home.luxury_experiences_section_subtitle || '', luxury_experiences: normalizeArray(contents.home.luxury_experiences), @@ -284,19 +285,56 @@ const PageContentDashboard: React.FC = () => { // Contact if (contents.contact) { + const defaultContactIcons = { + hero: 'Mail', + email: 'Mail', + phone: 'Phone', + location: 'MapPin', + name_field: 'User', + email_field: 'Mail', + phone_field: 'Phone', + subject_field: 'MessageSquare', + message_field: 'MessageSquare', + submit_button: 'Send', + }; setContactData({ title: contents.contact.title || '', subtitle: contents.contact.subtitle || '', description: contents.contact.description || '', content: contents.contact.content || '', map_url: contents.contact.map_url || '', + contact_icons: contents.contact.contact_icons ? { + ...defaultContactIcons, + ...contents.contact.contact_icons, + } : defaultContactIcons, meta_title: contents.contact.meta_title || '', meta_description: contents.contact.meta_description || '', }); + } else { + // Initialize with defaults even if no contact data exists + setContactData({ + contact_icons: { + hero: 'Mail', + email: 'Mail', + phone: 'Phone', + location: 'MapPin', + name_field: 'User', + email_field: 'Mail', + phone_field: 'Phone', + subject_field: 'MessageSquare', + message_field: 'MessageSquare', + submit_button: 'Send', + }, + }); } // About if (contents.about) { + const defaultAboutContactIcons = { + location: 'MapPin', + phone: 'Phone', + email: 'Mail', + }; setAboutData({ title: contents.about.title || '', subtitle: contents.about.subtitle || '', @@ -306,6 +344,12 @@ const PageContentDashboard: React.FC = () => { values: normalizeArray(contents.about.values), features: normalizeArray(contents.about.features), about_hero_image: contents.about.about_hero_image || '', + about_hero_icon: contents.about.about_hero_icon || 'Hotel', + about_contact_icons: contents.about.about_contact_icons ? { + ...defaultAboutContactIcons, + ...contents.about.about_contact_icons, + } : defaultAboutContactIcons, + about_learn_more_icon: contents.about.about_learn_more_icon || 'Hotel', mission: contents.about.mission || '', vision: contents.about.vision || '', team: normalizeArray(contents.about.team), @@ -314,6 +358,17 @@ const PageContentDashboard: React.FC = () => { meta_title: contents.about.meta_title || '', meta_description: contents.about.meta_description || '', }); + } else { + // Initialize with defaults even if no about data exists + setAboutData({ + about_hero_icon: 'Hotel', + about_contact_icons: { + location: 'MapPin', + phone: 'Phone', + email: 'Mail', + }, + about_learn_more_icon: 'Hotel', + }); } // Footer @@ -1354,7 +1409,7 @@ const PageContentDashboard: React.FC = () => {
{ setHomeData((prevData) => { const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : []; @@ -1550,7 +1605,7 @@ const PageContentDashboard: React.FC = () => {
{ setHomeData((prevData) => { const currentFeatures = Array.isArray(prevData.luxury_features) ? [...prevData.luxury_features] : []; @@ -2031,7 +2086,7 @@ const PageContentDashboard: React.FC = () => {
{ const currentStats = Array.isArray(homeData.stats) ? [...homeData.stats] : []; currentStats[index] = { ...currentStats[index], icon: iconName }; @@ -2177,7 +2232,7 @@ const PageContentDashboard: React.FC = () => {
{ setHomeData((prevData) => { const currentFeatures = Array.isArray(prevData.features) ? [...prevData.features] : []; @@ -2303,6 +2358,14 @@ const PageContentDashboard: React.FC = () => { placeholder="6" />
+
+ +

Icon shown when a service has no icon set

+ setHomeData({ ...homeData, services_fallback_icon: icon })} + /> +
@@ -2371,7 +2434,7 @@ const PageContentDashboard: React.FC = () => {
{ const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : []; current[index] = { ...current[index], icon: iconName }; @@ -2522,7 +2585,7 @@ const PageContentDashboard: React.FC = () => {
{ const current = Array.isArray(homeData.awards) ? [...homeData.awards] : []; current[index] = { ...current[index], icon: iconName }; @@ -3607,6 +3670,112 @@ const PageContentDashboard: React.FC = () => {
+
+

Icons

+
+
+ + setContactData({ + ...contactData, + contact_icons: { ...contactData.contact_icons, hero: icon } + })} + /> +
+
+ + setContactData({ + ...contactData, + contact_icons: { ...contactData.contact_icons, email: icon } + })} + /> +
+
+ + setContactData({ + ...contactData, + contact_icons: { ...contactData.contact_icons, phone: icon } + })} + /> +
+
+ + setContactData({ + ...contactData, + contact_icons: { ...contactData.contact_icons, location: icon } + })} + /> +
+
+ + setContactData({ + ...contactData, + contact_icons: { ...contactData.contact_icons, name_field: icon } + })} + /> +
+
+ + setContactData({ + ...contactData, + contact_icons: { ...contactData.contact_icons, email_field: icon } + })} + /> +
+
+ + setContactData({ + ...contactData, + contact_icons: { ...contactData.contact_icons, phone_field: icon } + })} + /> +
+
+ + setContactData({ + ...contactData, + contact_icons: { ...contactData.contact_icons, subject_field: icon } + })} + /> +
+
+ + setContactData({ + ...contactData, + contact_icons: { ...contactData.contact_icons, message_field: icon } + })} + /> +
+
+ + setContactData({ + ...contactData, + contact_icons: { ...contactData.contact_icons, submit_button: icon } + })} + /> +
+
+
+
@@ -3727,6 +3896,15 @@ const PageContentDashboard: React.FC = () => { )}
+ {/* Hero Icon (when no hero image) */} +
+ + setAboutData({ ...aboutData, about_hero_icon: icon })} + /> +
+ {/* Mission */}
@@ -3792,7 +3970,7 @@ const PageContentDashboard: React.FC = () => {
{ setAboutData((prevData) => { const newValues = [...(prevData.values || [])]; @@ -3883,7 +4061,7 @@ const PageContentDashboard: React.FC = () => {
{ setAboutData((prevData) => { const newFeatures = [...(prevData.features || [])]; @@ -4262,7 +4440,7 @@ const PageContentDashboard: React.FC = () => {
{ setAboutData((prevData) => { const newAchievements = [...(prevData.achievements || [])]; @@ -4378,6 +4556,50 @@ const PageContentDashboard: React.FC = () => { )}
+ {/* Additional Icons */} +
+

Additional Icons

+
+
+ + setAboutData({ + ...aboutData, + about_contact_icons: { ...aboutData.about_contact_icons, location: icon } + })} + /> +
+
+ + setAboutData({ + ...aboutData, + about_contact_icons: { ...aboutData.about_contact_icons, phone: icon } + })} + /> +
+
+ + setAboutData({ + ...aboutData, + about_contact_icons: { ...aboutData.about_contact_icons, email: icon } + })} + /> +
+
+ + setAboutData({ ...aboutData, about_learn_more_icon: icon })} + /> +
+
+
+
diff --git a/Frontend/src/pages/admin/SettingsPage.tsx b/Frontend/src/pages/admin/SettingsPage.tsx index e8d1627b..fbd10840 100644 --- a/Frontend/src/pages/admin/SettingsPage.tsx +++ b/Frontend/src/pages/admin/SettingsPage.tsx @@ -134,6 +134,10 @@ const SettingsPage: React.FC = () => { tax_rate: 0, chat_working_hours_start: 9, chat_working_hours_end: 17, + bank_name: '', + bank_account_number: '', + bank_account_holder: '', + bank_code: '', }); const [logoPreview, setLogoPreview] = useState(null); const [faviconPreview, setFaviconPreview] = useState(null); @@ -156,6 +160,7 @@ const SettingsPage: React.FC = () => { theme_primary_light: '#f5d76e', theme_primary_dark: '#c9a227', theme_primary_accent: '#e8c547', + theme_layout_mode: 'dark', }); const [loading, setLoading] = useState(true); @@ -165,7 +170,6 @@ const SettingsPage: React.FC = () => { const [openPaymentModal, setOpenPaymentModal] = useState<'stripe' | 'paypal' | 'borica' | null>(null); const currencyNames: Record = { - VND: 'Vietnamese Dong', USD: 'US Dollar', EUR: 'Euro', GBP: 'British Pound', @@ -300,6 +304,10 @@ const SettingsPage: React.FC = () => { tax_rate: companyRes.data.tax_rate || 0, chat_working_hours_start: companyRes.data.chat_working_hours_start || 9, chat_working_hours_end: companyRes.data.chat_working_hours_end || 17, + bank_name: companyRes.data.bank_name || '', + bank_account_number: companyRes.data.bank_account_number || '', + bank_account_holder: companyRes.data.bank_account_holder || '', + bank_code: companyRes.data.bank_code || '', }); @@ -804,6 +812,7 @@ const SettingsPage: React.FC = () => { theme_primary_light: themeRes.data.theme_primary_light || '#f5d76e', theme_primary_dark: themeRes.data.theme_primary_dark || '#c9a227', theme_primary_accent: themeRes.data.theme_primary_accent || '#e8c547', + theme_layout_mode: themeRes.data.theme_layout_mode || 'dark', }); } catch (error: any) { toast.error( @@ -2722,6 +2731,82 @@ const SettingsPage: React.FC = () => {

+ {/* Bank Details Section */} +
+
+ +

Bank Transfer Details

+
+

+ Configure bank details for bank transfer payments. These will be displayed on payment confirmation pages. +

+ +
+
+ + + setCompanyFormData({ ...companyFormData, bank_name: e.target.value }) + } + placeholder="e.g., Bank of America, Chase Bank" + 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" + /> +
+ +
+ + + setCompanyFormData({ ...companyFormData, bank_account_number: e.target.value }) + } + placeholder="Bank account number" + 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" + /> +
+ +
+ + + setCompanyFormData({ ...companyFormData, bank_account_holder: e.target.value }) + } + placeholder="Name on the bank account" + 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" + /> +
+ +
+ + + setCompanyFormData({ ...companyFormData, bank_code: e.target.value }) + } + placeholder="Bank routing/swift code (if needed for QR codes)" + 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" + /> +

+ Optional: Bank code used for QR code generation +

+
+
+
+ {}
+ {/* Layout Mode Section */} +
+
+
+ +

+ Choose between dark (black) or light (white) background theme for all frontend pages +

+
+ + +
+
+
+
+
-

About Theme Colors

+

About Theme Settings

- Changes to theme colors will be applied immediately across all frontend pages. - Use hex color codes (e.g., #d4af37) for best results. The colors will replace the default gold/yellow theme throughout the application. + Changes to theme colors and layout mode will be applied immediately across all frontend pages. + Use hex color codes (e.g., #d4af37) for best results. The layout mode controls whether pages use dark (black) or light (white) backgrounds.

diff --git a/Frontend/src/pages/customer/BookingSuccessPage.tsx b/Frontend/src/pages/customer/BookingSuccessPage.tsx index 419c7b6a..efc6fb9d 100644 --- a/Frontend/src/pages/customer/BookingSuccessPage.tsx +++ b/Frontend/src/pages/customer/BookingSuccessPage.tsx @@ -39,12 +39,14 @@ import { getBookingStatusConfig } from '../../shared/utils/bookingUtils'; import { getPaymentMethodLabel } from '../../shared/utils/paymentUtils'; import { BOOKING_STATUS, PAYMENT_METHOD, PAYMENT_STATUS, PAYMENT_TYPE } from '../../shared/constants/bookingConstants'; import useAuthStore from '../../store/useAuthStore'; +import { useCompanySettings } from '../../shared/contexts/CompanySettingsContext'; const BookingSuccessPage: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { formatCurrency } = useFormatCurrency(); const { userInfo } = useAuthStore(); + const { settings } = useCompanySettings(); const abortControllerRef = useRef(null); const [booking, setBooking] = useState( @@ -260,7 +262,10 @@ const BookingSuccessPage: React.FC = () => { const qrCodeUrl = booking ? generateQRCode( booking.booking_number, - booking.total_price + booking.total_price, + settings.bank_code || undefined, + settings.bank_account_number || undefined, + settings.bank_account_holder || undefined ) : null; @@ -687,15 +692,15 @@ const BookingSuccessPage: React.FC = () => { >

Bank: - Vietcombank (VCB) + {settings.bank_name || 'Not configured'}

Account Number: - 0123456789 + {settings.bank_account_number || 'Not configured'}

Account Holder: - KHACH SAN ABC + {settings.bank_account_holder || 'Not configured'}

Amount:{' '} diff --git a/Frontend/src/pages/customer/PaymentConfirmationPage.tsx b/Frontend/src/pages/customer/PaymentConfirmationPage.tsx index b727d036..e74f61b5 100644 --- a/Frontend/src/pages/customer/PaymentConfirmationPage.tsx +++ b/Frontend/src/pages/customer/PaymentConfirmationPage.tsx @@ -30,6 +30,7 @@ import { validateBookingId } from '../../shared/utils/routeValidation'; import { validateAndHandleBookingOwnership } from '../../shared/utils/ownershipValidation'; import { getPaymentMethodLabel } from '../../shared/utils/paymentUtils'; import { PAYMENT_METHOD, PAYMENT_STATUS } from '../../shared/constants/bookingConstants'; +import { useCompanySettings } from '../../shared/contexts/CompanySettingsContext'; const PaymentConfirmationPage: React.FC = () => { const { id } = useParams<{ id: string }>(); @@ -37,6 +38,7 @@ const PaymentConfirmationPage: React.FC = () => { const { isAuthenticated, userInfo } = useAuthStore(); const { openModal } = useAuthModal(); const { formatCurrency } = useFormatCurrency(); + const { settings } = useCompanySettings(); const abortControllerRef = useRef(null); const [booking, setBooking] = useState( @@ -256,7 +258,10 @@ const PaymentConfirmationPage: React.FC = () => { const qrCodeUrl = generateQRCode( booking.booking_number, - booking.total_price + booking.total_price, + settings.bank_code || undefined, + settings.bank_account_number || undefined, + settings.bank_account_holder || undefined ); return ( @@ -371,15 +376,15 @@ const PaymentConfirmationPage: React.FC = () => { >

Bank: - Vietcombank (VCB) + {settings.bank_name || 'Not configured'}

Account Number: - 0123456789 + {settings.bank_account_number || 'Not configured'}

Account Holder: - KHACH SAN ABC + {settings.bank_account_holder || 'Not configured'}

Amount:{' '} @@ -408,12 +413,18 @@ const PaymentConfirmationPage: React.FC = () => { > Scan QR code to transfer

- QR Code + {qrCodeUrl ? ( + QR Code + ) : ( +

+ Bank details not configured. Please contact support. +

+ )}
diff --git a/Frontend/src/pages/customer/PaymentResultPage.tsx b/Frontend/src/pages/customer/PaymentResultPage.tsx index d97ba00d..75205cb9 100644 --- a/Frontend/src/pages/customer/PaymentResultPage.tsx +++ b/Frontend/src/pages/customer/PaymentResultPage.tsx @@ -17,8 +17,8 @@ const PaymentResultPage: React.FC = () => { const { settings } = useCompanySettings(); - const supportEmail = settings.company_email || 'support@hotel.com'; - const supportPhone = settings.company_phone || '1900 xxxx'; + const supportEmail = settings.company_email || null; + const supportPhone = settings.company_phone || null; const status = searchParams.get('status'); const bookingId = searchParams.get('bookingId'); diff --git a/Frontend/src/pages/customer/RoomDetailPage.tsx b/Frontend/src/pages/customer/RoomDetailPage.tsx index 5eeae096..5ad763f9 100644 --- a/Frontend/src/pages/customer/RoomDetailPage.tsx +++ b/Frontend/src/pages/customer/RoomDetailPage.tsx @@ -21,8 +21,14 @@ import LuxuryBookingModal from '../../features/bookings/components/LuxuryBooking import { useAuthModal } from '../../features/auth/contexts/AuthModalContext'; import { toast } from 'react-toastify'; import { logger } from '../../shared/utils/logger'; +import { useTheme } from '../../shared/contexts/ThemeContext'; +import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../shared/utils/themeUtils'; const RoomDetailPage: React.FC = () => { + const { theme } = useTheme(); + const textClasses = getThemeTextClasses(theme.theme_layout_mode); + const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode); + const cardClasses = getThemeCardClasses(theme.theme_layout_mode); const { room_number } = useParams<{ room_number: string }>(); const navigate = useNavigate(); const { formatCurrency } = useFormatCurrency(); @@ -172,7 +178,7 @@ const RoomDetailPage: React.FC = () => { if (loading) { return (
{ >
-
-
-
+
+
+
@@ -196,7 +202,7 @@ const RoomDetailPage: React.FC = () => { if (error || !room) { return (
{ return (
{ : 'Maintenance'}
{room.status === 'occupied' && bookedUntilDate && ( -

+

Booked until {bookedUntilDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}

)} @@ -322,10 +328,12 @@ const RoomDetailPage: React.FC = () => {
-

{roomType?.name}

@@ -334,62 +342,62 @@ const RoomDetailPage: React.FC = () => { {}
-
-

+

Location

-

+

Room {room.room_number} - Floor {room.floor}

-
-

+

Capacity

-

+

{room?.capacity || roomType?.capacity || 0} guests

{room.average_rating != null && ( -
-

+

Rating

-

+

{Number(room.average_rating).toFixed(1)}

- + ({room.total_reviews || 0})
@@ -401,23 +409,23 @@ const RoomDetailPage: React.FC = () => { {} {(room?.description || roomType?.description) && ( -
-

{room?.description ? 'Room Description' : 'Room Type Description'}

-

{room?.description || roomType?.description}

@@ -425,17 +433,17 @@ const RoomDetailPage: React.FC = () => { )} {} -
-

Amenities & Features

@@ -461,14 +469,14 @@ const RoomDetailPage: React.FC = () => { {}
{} -
diff --git a/Frontend/src/pages/customer/RoomListPage.tsx b/Frontend/src/pages/customer/RoomListPage.tsx index acf0aaa4..bdc5d043 100644 --- a/Frontend/src/pages/customer/RoomListPage.tsx +++ b/Frontend/src/pages/customer/RoomListPage.tsx @@ -9,8 +9,15 @@ import Pagination from '../../shared/components/Pagination'; import { ArrowLeft, Hotel, Filter, ChevronDown, ChevronUp, Tag, X, CheckCircle } from 'lucide-react'; import { logger } from '../../shared/utils/logger'; import { toast } from 'react-toastify'; +import { useTheme } from '../../shared/contexts/ThemeContext'; +import { getThemeBackgroundClasses, getThemeHeroBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../shared/utils/themeUtils'; const RoomListPage: React.FC = () => { + const { theme } = useTheme(); + const textClasses = getThemeTextClasses(theme.theme_layout_mode); + const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode); + const heroBgClasses = getThemeHeroBackgroundClasses(theme.theme_layout_mode); + const cardClasses = getThemeCardClasses(theme.theme_layout_mode); const [searchParams] = useSearchParams(); const [rooms, setRooms] = useState([]); const [loading, setLoading] = useState(true); @@ -146,13 +153,13 @@ const RoomListPage: React.FC = () => { }, [searchParams]); return ( -
+
{} {/* Promotion Banner */} {showPromotionBanner && activePromotion && (
-
+
@@ -165,11 +172,11 @@ const RoomListPage: React.FC = () => {
{activePromotion.discount && ( -

+

{activePromotion.discount} - {activePromotion.description || 'Valid on bookings'}

)} -

+

The promotion code will be automatically applied when you book a room

@@ -179,14 +186,14 @@ const RoomListPage: React.FC = () => { className="p-2 hover:bg-[var(--luxury-gold)]/10 rounded-lg transition-colors" aria-label="Dismiss promotion" > - +
)} -
+
{} {
-

Our Rooms & Suites

-

+

Discover our collection of luxurious accommodations, each designed to provide an exceptional stay

@@ -235,18 +244,18 @@ const RoomListPage: React.FC = () => {
-

+

{pageContent?.description || 'Experience unparalleled luxury and world-class hospitality. Your journey to exceptional comfort begins here.'}

@@ -252,7 +261,7 @@ const Footer: React.FC = () => { className="group flex items-center space-x-2 px-3 py-2 bg-gradient-to-r from-[var(--luxury-gold)]/5 to-transparent border border-[var(--luxury-gold)]/10 rounded-lg hover:border-[var(--luxury-gold)]/30 hover:from-[var(--luxury-gold)]/10 transition-all duration-300" > - {badge.text} + {badge.text}
); })} @@ -266,10 +275,12 @@ const Footer: React.FC = () => { href={pageContent.social_links.facebook} target="_blank" rel="noopener noreferrer" - className="group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50 hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5" + className={`group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg ${theme.theme_layout_mode === 'light' + ? 'bg-white border border-gray-300 shadow-sm' + : 'bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50'} hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5`} aria-label="Facebook" > - +
)} @@ -278,10 +289,12 @@ const Footer: React.FC = () => { href={pageContent.social_links.twitter} target="_blank" rel="noopener noreferrer" - className="group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50 hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5" + className={`group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg ${theme.theme_layout_mode === 'light' + ? 'bg-white border border-gray-300 shadow-sm' + : 'bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50'} hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5`} aria-label="Twitter" > - +
)} @@ -290,10 +303,12 @@ const Footer: React.FC = () => { href={pageContent.social_links.instagram} target="_blank" rel="noopener noreferrer" - className="group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50 hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5" + className={`group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg ${theme.theme_layout_mode === 'light' + ? 'bg-white border border-gray-300 shadow-sm' + : 'bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50'} hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5`} aria-label="Instagram" > - +
)} @@ -302,10 +317,12 @@ const Footer: React.FC = () => { href={pageContent.social_links.linkedin} target="_blank" rel="noopener noreferrer" - className="group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50 hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5" + className={`group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg ${theme.theme_layout_mode === 'light' + ? 'bg-white border border-gray-300 shadow-sm' + : 'bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50'} hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5`} aria-label="LinkedIn" > - +
)} @@ -314,10 +331,12 @@ const Footer: React.FC = () => { href={pageContent.social_links.youtube} target="_blank" rel="noopener noreferrer" - className="group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50 hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5" + className={`group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg ${theme.theme_layout_mode === 'light' + ? 'bg-white border border-gray-300 shadow-sm' + : 'bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50'} hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5`} aria-label="YouTube" > - +
)} @@ -327,7 +346,7 @@ const Footer: React.FC = () => { {/* Quick Links */} {quickLinks.length > 0 && (
-

+

Quick Links

@@ -336,7 +355,7 @@ const Footer: React.FC = () => {
  • {link.label} @@ -350,7 +369,7 @@ const Footer: React.FC = () => { {/* Guest Services */} {supportLinks.length > 0 && (
    -

    +

    Guest Services

    @@ -359,7 +378,7 @@ const Footer: React.FC = () => {
  • {link.label} @@ -377,7 +396,7 @@ const Footer: React.FC = () => { {homePageContent?.newsletter_section_subtitle && ( -

    +

    {homePageContent.newsletter_section_subtitle}

    )} @@ -416,7 +435,9 @@ const Footer: React.FC = () => { value={newsletterEmail} onChange={(e) => setNewsletterEmail(e.target.value)} placeholder={homePageContent?.newsletter_placeholder || 'Enter your email'} - className="w-full px-4 py-2.5 rounded-lg border border-gray-700 bg-gray-800/50 text-white placeholder-gray-400 focus:border-[var(--luxury-gold)] focus:ring-2 focus:ring-[var(--luxury-gold)]/20 transition-all disabled:opacity-50 disabled:cursor-not-allowed text-sm" + className={`w-full px-4 py-2.5 rounded-lg border ${theme.theme_layout_mode === 'light' + ? 'border-gray-300 bg-white text-gray-900 placeholder-gray-500' + : 'border-gray-700 bg-gray-800/50 text-white placeholder-gray-400'} focus:border-[var(--luxury-gold)] focus:ring-2 focus:ring-[var(--luxury-gold)]/20 transition-all disabled:opacity-50 disabled:cursor-not-allowed text-sm`} required disabled={newsletterSubmitting} /> @@ -441,7 +462,7 @@ const Footer: React.FC = () => { {/* Contact Information */}
    -

    +

    Contact

    @@ -455,7 +476,7 @@ const Footer: React.FC = () => {
  • - + {displayAddress .split('\n').map((line, i) => ( @@ -474,7 +495,7 @@ const Footer: React.FC = () => {
    -
    + {displayPhone} @@ -487,11 +508,29 @@ const Footer: React.FC = () => {
    - + {displayEmail} )} + {settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined && ( +
  • +
    +
    + +
    +
    +
    +
    + + Chat Support Hours + + + {formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)} + +
    +
  • + )} )} diff --git a/Frontend/src/shared/components/Header.tsx b/Frontend/src/shared/components/Header.tsx index 1f5839ce..904efc2e 100644 --- a/Frontend/src/shared/components/Header.tsx +++ b/Frontend/src/shared/components/Header.tsx @@ -18,10 +18,12 @@ import { } from 'lucide-react'; import { useClickOutside } from '../hooks/useClickOutside'; import { useCompanySettings } from '../contexts/CompanySettingsContext'; +import { useTheme } from '../contexts/ThemeContext'; import { useAuthModal } from '../../features/auth/contexts/AuthModalContext'; import { normalizeImageUrl } from '../utils/imageUtils'; import InAppNotificationBell from '../../features/notifications/components/InAppNotificationBell'; import Navbar from './Navbar'; +import { getThemeTextClasses } from '../utils/themeUtils'; interface HeaderProps { isAuthenticated?: boolean; @@ -40,12 +42,14 @@ const Header: React.FC = ({ onLogout }) => { const { settings } = useCompanySettings(); + const { theme } = useTheme(); const { openModal } = useAuthModal(); const location = useLocation(); + const textClasses = getThemeTextClasses(theme.theme_layout_mode); - const displayPhone = settings.company_phone || '+1 (234) 567-890'; - const displayEmail = settings.company_email || 'info@luxuryhotel.com'; + const displayPhone = settings.company_phone || null; + const displayEmail = settings.company_email || null; const logoUrl = settings.company_logo_url ? (settings.company_logo_url.startsWith('http') ? settings.company_logo_url @@ -110,12 +114,12 @@ const Header: React.FC = ({ onTouchStart={(e) => { e.stopPropagation(); }} - className="flex items-center - space-x-2 px-4 py-3.5 text-white/95 + className={`flex items-center + space-x-2 px-4 py-3.5 ${textClasses.primary}/95 hover:bg-[rgba(var(--luxury-gold-rgb),0.15)] hover:text-[var(--luxury-gold)] rounded-md transition-all duration-300 border-l-2 border-transparent - hover:border-[var(--luxury-gold)] font-light tracking-wider text-sm w-full text-left group relative mx-2 cursor-pointer" + hover:border-[var(--luxury-gold)] font-light tracking-wider text-sm w-full text-left group relative mx-2 cursor-pointer`} style={{ touchAction: 'manipulation' }} > @@ -160,7 +164,7 @@ const Header: React.FC = ({ e.stopPropagation(); }} className="flex items-center - space-x-2 px-4 py-3.5 text-white/95 + space-x-2 px-4 py-3.5 ${textClasses.primary}/95 hover:bg-[rgba(var(--luxury-gold-rgb),0.15)] hover:text-[var(--luxury-gold)] rounded-md transition-all duration-300 border-l-2 border-transparent @@ -181,12 +185,12 @@ const Header: React.FC = ({ onTouchStart={(e) => { e.stopPropagation(); }} - className="flex items-center - space-x-2 px-4 py-3 text-white/90 + className={`flex items-center + space-x-2 px-4 py-3 ${textClasses.primary}/90 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] rounded-sm transition-all duration-300 border-l-2 border-transparent - hover:border-[var(--luxury-gold)] font-light tracking-wide" + hover:border-[var(--luxury-gold)] font-light tracking-wide`} style={{ touchAction: 'manipulation' }} > @@ -201,12 +205,12 @@ const Header: React.FC = ({ onTouchStart={(e) => { e.stopPropagation(); }} - className="flex items-center - space-x-2 px-4 py-3 text-white/90 + className={`flex items-center + space-x-2 px-4 py-3 ${textClasses.primary}/90 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] rounded-sm transition-all duration-300 border-l-2 border-transparent - hover:border-[var(--luxury-gold)] font-light tracking-wide" + hover:border-[var(--luxury-gold)] font-light tracking-wide`} style={{ touchAction: 'manipulation' }} > @@ -221,12 +225,12 @@ const Header: React.FC = ({ onTouchStart={(e) => { e.stopPropagation(); }} - className="flex items-center - space-x-2 px-4 py-3 text-white/90 + className={`flex items-center + space-x-2 px-4 py-3 ${textClasses.primary}/90 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] rounded-sm transition-all duration-300 border-l-2 border-transparent - hover:border-[var(--luxury-gold)] font-light tracking-wide" + hover:border-[var(--luxury-gold)] font-light tracking-wide`} style={{ touchAction: 'manipulation' }} > @@ -241,12 +245,12 @@ const Header: React.FC = ({ onTouchStart={(e) => { e.stopPropagation(); }} - className="flex items-center - space-x-2 px-4 py-3 text-white/90 + className={`flex items-center + space-x-2 px-4 py-3 ${textClasses.primary}/90 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] rounded-sm transition-all duration-300 border-l-2 border-transparent - hover:border-[var(--luxury-gold)] font-light tracking-wide" + hover:border-[var(--luxury-gold)] font-light tracking-wide`} style={{ touchAction: 'manipulation' }} > @@ -261,12 +265,12 @@ const Header: React.FC = ({ onTouchStart={(e) => { e.stopPropagation(); }} - className="flex items-center - space-x-2 px-4 py-3 text-white/90 + className={`flex items-center + space-x-2 px-4 py-3 ${textClasses.primary}/90 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] rounded-sm transition-all duration-300 border-l-2 border-transparent - hover:border-[var(--luxury-gold)] font-light tracking-wide" + hover:border-[var(--luxury-gold)] font-light tracking-wide`} style={{ touchAction: 'manipulation' }} > @@ -281,12 +285,12 @@ const Header: React.FC = ({ onTouchStart={(e) => { e.stopPropagation(); }} - className="flex items-center - space-x-2 px-4 py-3 text-white/90 + className={`flex items-center + space-x-2 px-4 py-3 ${textClasses.primary}/90 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] rounded-sm transition-all duration-300 border-l-2 border-transparent - hover:border-[var(--luxury-gold)] font-light tracking-wide" + hover:border-[var(--luxury-gold)] font-light tracking-wide`} style={{ touchAction: 'manipulation' }} > @@ -301,12 +305,12 @@ const Header: React.FC = ({ onTouchStart={(e) => { e.stopPropagation(); }} - className="flex items-center - space-x-2 px-4 py-3 text-white/90 + className={`flex items-center + space-x-2 px-4 py-3 ${textClasses.primary}/90 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] rounded-sm transition-all duration-300 border-l-2 border-transparent - hover:border-[var(--luxury-gold)] font-light tracking-wide" + hover:border-[var(--luxury-gold)] font-light tracking-wide`} style={{ touchAction: 'manipulation' }} > @@ -321,12 +325,12 @@ const Header: React.FC = ({ onTouchStart={(e) => { e.stopPropagation(); }} - className="flex items-center - space-x-2 px-4 py-3 text-white/90 + className={`flex items-center + space-x-2 px-4 py-3 ${textClasses.primary}/90 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] rounded-sm transition-all duration-300 border-l-2 border-transparent - hover:border-[var(--luxury-gold)] font-light tracking-wide" + hover:border-[var(--luxury-gold)] font-light tracking-wide`} style={{ touchAction: 'manipulation' }} > @@ -344,12 +348,12 @@ const Header: React.FC = ({ onTouchStart={(e) => { e.stopPropagation(); }} - className="flex items-center - space-x-2 px-4 py-3 text-white/90 + className={`flex items-center + space-x-2 px-4 py-3 ${textClasses.primary}/90 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] rounded-sm transition-all duration-300 border-l-2 border-transparent - hover:border-[var(--luxury-gold)] font-light tracking-wide" + hover:border-[var(--luxury-gold)] font-light tracking-wide`} style={{ touchAction: 'manipulation' }} > @@ -366,12 +370,12 @@ const Header: React.FC = ({ onTouchStart={(e) => { e.stopPropagation(); }} - className="flex items-center - space-x-2 px-4 py-3 text-white/90 + className={`flex items-center + space-x-2 px-4 py-3 ${textClasses.primary}/90 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] rounded-sm transition-all duration-300 border-l-2 border-transparent - hover:border-[var(--luxury-gold)] font-light tracking-wide" + hover:border-[var(--luxury-gold)] font-light tracking-wide`} style={{ touchAction: 'manipulation' }} > @@ -388,12 +392,12 @@ const Header: React.FC = ({ onTouchStart={(e) => { e.stopPropagation(); }} - className="flex items-center - space-x-2 px-4 py-3 text-white/90 + className={`flex items-center + space-x-2 px-4 py-3 ${textClasses.primary}/90 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] rounded-sm transition-all duration-300 border-l-2 border-transparent - hover:border-[var(--luxury-gold)] font-light tracking-wide" + hover:border-[var(--luxury-gold)] font-light tracking-wide`} style={{ touchAction: 'manipulation' }} > @@ -410,12 +414,12 @@ const Header: React.FC = ({ onTouchStart={(e) => { e.stopPropagation(); }} - className="flex items-center - space-x-2 px-4 py-3 text-white/90 + className={`flex items-center + space-x-2 px-4 py-3 ${textClasses.primary}/90 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] rounded-sm transition-all duration-300 border-l-2 border-transparent - hover:border-[var(--luxury-gold)] font-light tracking-wide" + hover:border-[var(--luxury-gold)] font-light tracking-wide`} style={{ touchAction: 'manipulation' }} > @@ -446,9 +450,15 @@ const Header: React.FC = ({ return (
    -
    +
    {displayPhone && ( @@ -492,7 +502,9 @@ const Header: React.FC = ({
    )}
    - + {settings.company_name} @@ -530,10 +542,10 @@ const Header: React.FC = ({ <>
    )} - + {userInfo?.name} {isUserMenuOpen && ( -
    setIsUserMenuOpen(false)} className="flex items-center space-x-3 - px-5 py-3 text-white/95 + px-5 py-3 ${textClasses.primary}/95 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] transition-all duration-300 border-l-2 border-transparent hover:border-[var(--luxury-gold)] rounded-md group relative" @@ -620,7 +636,7 @@ const Header: React.FC = ({ to="/dashboard" onClick={() => setIsUserMenuOpen(false)} className="flex items-center space-x-3 - px-5 py-3 text-white/95 + px-5 py-3 ${textClasses.primary}/95 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] transition-all duration-300 border-l-2 border-transparent hover:border-[var(--luxury-gold)] rounded-md group relative" @@ -633,7 +649,7 @@ const Header: React.FC = ({ to="/favorites" onClick={() => setIsUserMenuOpen(false)} className="flex items-center space-x-3 - px-5 py-3 text-white/95 + px-5 py-3 ${textClasses.primary}/95 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] transition-all duration-300 border-l-2 border-transparent hover:border-[var(--luxury-gold)] rounded-md group relative" @@ -646,7 +662,7 @@ const Header: React.FC = ({ to="/bookings" onClick={() => setIsUserMenuOpen(false)} className="flex items-center space-x-3 - px-5 py-3 text-white/95 + px-5 py-3 ${textClasses.primary}/95 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] transition-all duration-300 border-l-2 border-transparent hover:border-[var(--luxury-gold)] rounded-md group relative" @@ -659,7 +675,7 @@ const Header: React.FC = ({ to="/loyalty" onClick={() => setIsUserMenuOpen(false)} className="flex items-center space-x-3 - px-5 py-3 text-white/95 + px-5 py-3 ${textClasses.primary}/95 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] transition-all duration-300 border-l-2 border-transparent hover:border-[var(--luxury-gold)] rounded-md group relative" @@ -672,7 +688,7 @@ const Header: React.FC = ({ to="/group-bookings" onClick={() => setIsUserMenuOpen(false)} className="flex items-center space-x-3 - px-5 py-3 text-white/95 + px-5 py-3 ${textClasses.primary}/95 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] transition-all duration-300 border-l-2 border-transparent hover:border-[var(--luxury-gold)] rounded-md group relative" @@ -685,7 +701,7 @@ const Header: React.FC = ({ to="/complaints" onClick={() => setIsUserMenuOpen(false)} className="flex items-center space-x-3 - px-5 py-3 text-white/95 + px-5 py-3 ${textClasses.primary}/95 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] transition-all duration-300 border-l-2 border-transparent hover:border-[var(--luxury-gold)] rounded-md group relative" @@ -698,7 +714,7 @@ const Header: React.FC = ({ to="/guest-requests" onClick={() => setIsUserMenuOpen(false)} className="flex items-center space-x-3 - px-5 py-3 text-white/95 + px-5 py-3 ${textClasses.primary}/95 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] transition-all duration-300 border-l-2 border-transparent hover:border-[var(--luxury-gold)] rounded-md group relative" @@ -711,7 +727,7 @@ const Header: React.FC = ({ to="/gdpr" onClick={() => setIsUserMenuOpen(false)} className="flex items-center space-x-3 - px-5 py-3 text-white/95 + px-5 py-3 ${textClasses.primary}/95 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] transition-all duration-300 border-l-2 border-transparent hover:border-[var(--luxury-gold)] rounded-md group relative" @@ -728,11 +744,11 @@ const Header: React.FC = ({ onClick={() => setIsUserMenuOpen(false) } - className="flex items-center - space-x-3 px-5 py-3 text-white/95 + className={`flex items-center + space-x-3 px-5 py-3 ${textClasses.primary}/95 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] transition-all duration-300 border-l-2 border-transparent - hover:border-[var(--luxury-gold)] rounded-md group relative" + hover:border-[var(--luxury-gold)] rounded-md group relative`} > @@ -745,11 +761,11 @@ const Header: React.FC = ({ onClick={() => setIsUserMenuOpen(false) } - className="flex items-center - space-x-3 px-5 py-3 text-white/95 + className={`flex items-center + space-x-3 px-5 py-3 ${textClasses.primary}/95 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] transition-all duration-300 border-l-2 border-transparent - hover:border-[var(--luxury-gold)] rounded-md group relative" + hover:border-[var(--luxury-gold)] rounded-md group relative`} > @@ -762,11 +778,11 @@ const Header: React.FC = ({ onClick={() => setIsUserMenuOpen(false) } - className="flex items-center - space-x-3 px-5 py-3 text-white/95 + className={`flex items-center + space-x-3 px-5 py-3 ${textClasses.primary}/95 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] transition-all duration-300 border-l-2 border-transparent - hover:border-[var(--luxury-gold)] rounded-md group relative" + hover:border-[var(--luxury-gold)] rounded-md group relative`} > @@ -779,11 +795,11 @@ const Header: React.FC = ({ onClick={() => setIsUserMenuOpen(false) } - className="flex items-center - space-x-3 px-5 py-3 text-white/95 + className={`flex items-center + space-x-3 px-5 py-3 ${textClasses.primary}/95 hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] transition-all duration-300 border-l-2 border-transparent - hover:border-[var(--luxury-gold)] rounded-md group relative" + hover:border-[var(--luxury-gold)] rounded-md group relative`} > diff --git a/Frontend/src/shared/components/Navbar.tsx b/Frontend/src/shared/components/Navbar.tsx index 2723090e..ca52967b 100644 --- a/Frontend/src/shared/components/Navbar.tsx +++ b/Frontend/src/shared/components/Navbar.tsx @@ -2,6 +2,8 @@ import React, { useRef } from 'react'; import { createPortal } from 'react-dom'; import { Link } from 'react-router-dom'; import { Menu, X } from 'lucide-react'; +import { useTheme } from '../contexts/ThemeContext'; +import { getThemeTextClasses } from '../utils/themeUtils'; interface NavbarProps { isMobileMenuOpen: boolean; @@ -27,6 +29,8 @@ const Navbar: React.FC = ({ renderMobileLinksOnly = false, mobileMenuContent }) => { + const { theme } = useTheme(); + const textClasses = getThemeTextClasses(theme.theme_layout_mode); const mobileMenuContainerRef = useRef(null); const mobileMenuDropdownRef = useRef(null); @@ -88,11 +92,11 @@ const Navbar: React.FC = ({ key={link.to} to={link.to} onClick={handleLinkClick} - className="px-4 py-3 text-white/90 + className={`px-4 py-3 ${textClasses.primary}/90 hover:bg-[var(--luxury-gold)]/10 hover:text-[var(--luxury-gold)] rounded-sm transition-all duration-300 border-l-2 border-transparent - hover:border-[var(--luxury-gold)] font-light tracking-wide" + hover:border-[var(--luxury-gold)] font-light tracking-wide`} > {link.label} @@ -109,9 +113,9 @@ const Navbar: React.FC = ({ {link.label} @@ -148,7 +152,7 @@ const Navbar: React.FC = ({ <> {/* Mobile Menu Backdrop */}
    = ({ {/* Mobile Menu Dropdown - Right side on mobile */}
    = ({ // Ensure mouse events work e.stopPropagation(); }} - className="px-4 py-3.5 text-white/95 + className={`px-4 py-3.5 ${textClasses.primary}/95 hover:bg-[rgba(var(--luxury-gold-rgb),0.15)] hover:text-[var(--luxury-gold)] rounded-md transition-all duration-300 border-l-2 border-transparent hover:border-[var(--luxury-gold)] font-light tracking-wider text-sm uppercase - group relative mx-2 cursor-pointer text-left w-full block" + group relative mx-2 cursor-pointer text-left w-full block`} style={{ touchAction: 'manipulation', WebkitTapHighlightColor: 'transparent', diff --git a/Frontend/src/shared/contexts/CompanySettingsContext.tsx b/Frontend/src/shared/contexts/CompanySettingsContext.tsx index 05aa46da..1d2f5291 100644 --- a/Frontend/src/shared/contexts/CompanySettingsContext.tsx +++ b/Frontend/src/shared/contexts/CompanySettingsContext.tsx @@ -17,6 +17,10 @@ type CompanySettings = { company_address: string; chat_working_hours_start: number; chat_working_hours_end: number; + bank_name?: string; + bank_account_number?: string; + bank_account_holder?: string; + bank_code?: string; }; type CompanySettingsContextValue = { @@ -35,6 +39,10 @@ const defaultSettings: CompanySettings = { company_address: '', chat_working_hours_start: 9, chat_working_hours_end: 17, + bank_name: '', + bank_account_number: '', + bank_account_holder: '', + bank_code: '', }; const CompanySettingsContext = createContext(undefined); @@ -71,6 +79,10 @@ export const CompanySettingsProvider: React.FC = ( company_address: response.data.company_address || defaultSettings.company_address, chat_working_hours_start: response.data.chat_working_hours_start || defaultSettings.chat_working_hours_start, chat_working_hours_end: response.data.chat_working_hours_end || defaultSettings.chat_working_hours_end, + bank_name: response.data.bank_name || defaultSettings.bank_name, + bank_account_number: response.data.bank_account_number || defaultSettings.bank_account_number, + bank_account_holder: response.data.bank_account_holder || defaultSettings.bank_account_holder, + bank_code: response.data.bank_code || defaultSettings.bank_code, }); diff --git a/Frontend/src/shared/contexts/ThemeContext.tsx b/Frontend/src/shared/contexts/ThemeContext.tsx index f663e3cc..b02ae446 100644 --- a/Frontend/src/shared/contexts/ThemeContext.tsx +++ b/Frontend/src/shared/contexts/ThemeContext.tsx @@ -7,25 +7,28 @@ import React, { } from 'react'; import { themeService } from '../../features/system/services/systemSettingsService'; -type ThemeColors = { - primary: string; - primaryLight: string; - primaryDark: string; - primaryAccent: string; +type ThemeLayoutMode = 'dark' | 'light'; + +type ThemeSettings = { + theme_primary_color: string; + theme_primary_light: string; + theme_primary_dark: string; + theme_primary_accent: string; + theme_layout_mode: ThemeLayoutMode; }; type ThemeContextValue = { - colors: ThemeColors; + theme: ThemeSettings; isLoading: boolean; refreshTheme: () => Promise; - updateColors: (colors: Partial) => void; }; -const defaultColors: ThemeColors = { - primary: '#d4af37', - primaryLight: '#f5d76e', - primaryDark: '#c9a227', - primaryAccent: '#e8c547', +const defaultTheme: ThemeSettings = { + theme_primary_color: '#d4af37', + theme_primary_light: '#f5d76e', + theme_primary_dark: '#c9a227', + theme_primary_accent: '#e8c547', + theme_layout_mode: 'dark', }; const ThemeContext = createContext(undefined); @@ -42,58 +45,8 @@ interface ThemeProviderProps { children: ReactNode; } -/** - * Converts hex color to RGB values - */ -const hexToRgb = (hex: string): string => { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result - ? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}` - : '212, 175, 55'; // Default gold RGB -}; - -/** - * Applies theme colors as CSS variables to the document root - */ -const applyThemeColors = (colors: ThemeColors) => { - const root = document.documentElement; - - // Apply CSS variables - root.style.setProperty('--luxury-gold', colors.primary); - root.style.setProperty('--luxury-gold-light', colors.primaryLight); - root.style.setProperty('--luxury-gold-dark', colors.primaryDark); - root.style.setProperty('--luxury-gold-accent', colors.primaryAccent); - - // Add RGB versions for rgba() usage - root.style.setProperty('--luxury-gold-rgb', hexToRgb(colors.primary)); - root.style.setProperty('--luxury-gold-light-rgb', hexToRgb(colors.primaryLight)); - root.style.setProperty('--luxury-gold-dark-rgb', hexToRgb(colors.primaryDark)); - root.style.setProperty('--luxury-gold-accent-rgb', hexToRgb(colors.primaryAccent)); - - // Also update gradient variables - root.style.setProperty( - '--gradient-gold', - `linear-gradient(135deg, ${colors.primary} 0%, ${colors.primaryLight} 100%)` - ); - root.style.setProperty( - '--gradient-gold-dark', - `linear-gradient(135deg, ${colors.primaryDark} 0%, ${colors.primary} 100%)` - ); - - // Update shadow variables with proper opacity - const primaryRgb = hexToRgb(colors.primary); - root.style.setProperty( - '--shadow-luxury', - `0 4px 20px rgba(${primaryRgb}, 0.15)` - ); - root.style.setProperty( - '--shadow-luxury-gold', - `0 8px 30px rgba(${primaryRgb}, 0.25)` - ); -}; - export const ThemeProvider: React.FC = ({ children }) => { - const [colors, setColors] = useState(defaultColors); + const [theme, setTheme] = useState(defaultTheme); const [isLoading, setIsLoading] = useState(true); const loadTheme = async () => { @@ -101,20 +54,34 @@ export const ThemeProvider: React.FC = ({ children }) => { setIsLoading(true); const response = await themeService.getThemeSettings(); if (response.data) { - const newColors: ThemeColors = { - primary: response.data.theme_primary_color || defaultColors.primary, - primaryLight: response.data.theme_primary_light || defaultColors.primaryLight, - primaryDark: response.data.theme_primary_dark || defaultColors.primaryDark, - primaryAccent: response.data.theme_primary_accent || defaultColors.primaryAccent, + const themeData: ThemeSettings = { + theme_primary_color: response.data.theme_primary_color || defaultTheme.theme_primary_color, + theme_primary_light: response.data.theme_primary_light || defaultTheme.theme_primary_light, + theme_primary_dark: response.data.theme_primary_dark || defaultTheme.theme_primary_dark, + theme_primary_accent: response.data.theme_primary_accent || defaultTheme.theme_primary_accent, + theme_layout_mode: (response.data.theme_layout_mode === 'light' ? 'light' : 'dark') as ThemeLayoutMode, }; - setColors(newColors); - applyThemeColors(newColors); + setTheme(themeData); + + // Apply CSS variables for colors + document.documentElement.style.setProperty('--luxury-gold', themeData.theme_primary_color); + document.documentElement.style.setProperty('--luxury-gold-light', themeData.theme_primary_light); + document.documentElement.style.setProperty('--luxury-gold-dark', themeData.theme_primary_dark); + document.documentElement.style.setProperty('--luxury-gold-accent', themeData.theme_primary_accent); + + // Apply layout mode class to body + document.body.classList.remove('theme-dark', 'theme-light'); + document.body.classList.add(`theme-${themeData.theme_layout_mode}`); + document.documentElement.classList.remove('theme-dark', 'theme-light'); + document.documentElement.classList.add(`theme-${themeData.theme_layout_mode}`); } } catch (error) { console.error('Error loading theme settings:', error); - // Apply default colors on error - applyThemeColors(defaultColors); - setColors(defaultColors); + // Apply defaults + document.body.classList.remove('theme-dark', 'theme-light'); + document.body.classList.add('theme-dark'); + document.documentElement.classList.remove('theme-dark', 'theme-light'); + document.documentElement.classList.add('theme-dark'); } finally { setIsLoading(false); } @@ -122,12 +89,12 @@ export const ThemeProvider: React.FC = ({ children }) => { useEffect(() => { loadTheme(); - + // Listen for theme refresh events const handleRefresh = () => { loadTheme(); }; - + if (typeof window !== 'undefined') { window.addEventListener('refreshTheme', handleRefresh); return () => { @@ -140,23 +107,15 @@ export const ThemeProvider: React.FC = ({ children }) => { await loadTheme(); }; - const updateColors = (newColors: Partial) => { - const updatedColors = { ...colors, ...newColors }; - setColors(updatedColors); - applyThemeColors(updatedColors); - }; - return ( {children} ); }; - diff --git a/Frontend/src/shared/utils/constants.ts b/Frontend/src/shared/utils/constants.ts index 4d497049..81291008 100644 --- a/Frontend/src/shared/utils/constants.ts +++ b/Frontend/src/shared/utils/constants.ts @@ -12,9 +12,9 @@ export const DATE_FORMATS = { } as const; export const CURRENCY = { - VND: 'VND', USD: 'USD', EUR: 'EUR', + GBP: 'GBP', } as const; export const STORAGE_KEYS = { diff --git a/Frontend/src/shared/utils/format.ts b/Frontend/src/shared/utils/format.ts index dd154a2f..0eb56801 100644 --- a/Frontend/src/shared/utils/format.ts +++ b/Frontend/src/shared/utils/format.ts @@ -43,9 +43,7 @@ export const formatCurrency = ( const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount; if (isNaN(numAmount)) { - - if (currency === 'VND') return '0 ₫'; - return `0 ${currency || 'VND'}`; + return `0 ${currency || 'USD'}`; } @@ -67,16 +65,7 @@ export const formatCurrency = ( } } - currencyToUse = currencyToUse || 'VND'; - - if (currencyToUse === 'VND') { - return new Intl.NumberFormat('vi-VN', { - style: 'currency', - currency: 'VND', - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(numAmount); - } + currencyToUse = currencyToUse || 'USD'; const localeMap: Record = { @@ -242,3 +231,20 @@ export const truncateText = ( return text.slice(0, maxLength - suffix.length) + suffix; }; +/** + * Format working hours from 24-hour format to 12-hour format + * @param startHour - Start hour in 24-hour format (0-23) + * @param endHour - End hour in 24-hour format (0-23) + * @returns Formatted string like "9:00 AM - 5:00 PM" + */ +export const formatWorkingHours = (startHour: number, endHour: number): string => { + const formatHour = (hour: number): string => { + if (hour === 0) return '12:00 AM'; + if (hour < 12) return `${hour}:00 AM`; + if (hour === 12) return '12:00 PM'; + return `${hour - 12}:00 PM`; + }; + + return `${formatHour(startHour)} - ${formatHour(endHour)}`; +}; + diff --git a/Frontend/src/shared/utils/themeUtils.ts b/Frontend/src/shared/utils/themeUtils.ts new file mode 100644 index 00000000..293423c6 --- /dev/null +++ b/Frontend/src/shared/utils/themeUtils.ts @@ -0,0 +1,73 @@ +/** + * Theme utility functions for applying theme-aware classes + */ + +export type ThemeLayoutMode = 'dark' | 'light'; + +/** + * Get background classes based on theme layout mode + */ +export const getThemeBackgroundClasses = (mode: ThemeLayoutMode): string => { + if (mode === 'light') { + return 'bg-gradient-to-b from-gray-50 via-white to-gray-50'; + } + // Dark mode (default) + return 'bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f]'; +}; + +/** + * Get hero section background classes based on theme layout mode + */ +export const getThemeHeroBackgroundClasses = (mode: ThemeLayoutMode): string => { + if (mode === 'light') { + return 'bg-gradient-to-br from-gray-50 via-white to-gray-50 border-b border-gray-200'; + } + // Dark mode (default) + return 'bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#1a1a1a] border-b border-[var(--luxury-gold)]/10'; +}; + +/** + * Get text color classes based on theme layout mode + */ +export const getThemeTextClasses = (mode: ThemeLayoutMode): { + primary: string; + secondary: string; + muted: string; +} => { + if (mode === 'light') { + return { + primary: 'text-gray-900', + secondary: 'text-gray-700', + muted: 'text-gray-500', + }; + } + // Dark mode (default) + return { + primary: 'text-white', + secondary: 'text-gray-300', + muted: 'text-gray-400', + }; +}; + +/** + * Get card/container background classes based on theme layout mode + */ +export const getThemeCardClasses = (mode: ThemeLayoutMode): string => { + if (mode === 'light') { + return 'bg-white border border-gray-200'; + } + // Dark mode (default) + return 'bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a] border border-[var(--luxury-gold)]/30'; +}; + +/** + * Get input/field background classes based on theme layout mode + */ +export const getThemeInputClasses = (mode: ThemeLayoutMode): string => { + if (mode === 'light') { + return 'bg-white border-gray-300 text-gray-900'; + } + // Dark mode (default) + return 'bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-gray-700 text-white'; +}; + diff --git a/ICON_CONTROL_AUDIT.md b/ICON_CONTROL_AUDIT.md new file mode 100644 index 00000000..c507c0ee --- /dev/null +++ b/ICON_CONTROL_AUDIT.md @@ -0,0 +1,149 @@ +# Icon Control Audit Report + +## Summary +This document provides a comprehensive audit of icon usage across frontend pages and identifies which icons are controlled from the admin dashboard vs hardcoded. + +--- + +## ✅ Pages with Full Admin Control + +### 1. **HomePage** (`Frontend/src/features/content/pages/HomePage.tsx`) +**Status: ✅ Fully Controlled** + +All icons are controlled from admin dashboard: +- **Features Section** - Icons controlled via `pageContent.features[].icon` +- **Luxury Features Section** - Icons controlled via `pageContent.luxury_features[].icon` +- **Stats/Achievements Section** - Icons controlled via `pageContent.stats[].icon` (FIXED: now handles lowercase icon names) +- **Amenities Section** - Icons controlled via `pageContent.amenities[].icon` +- **Awards Section** - Icons controlled via `pageContent.awards[].icon` +- **Experiences Section** - Icons controlled via `pageContent.luxury_experiences[].icon` + +**Admin Location:** `PageContentDashboard.tsx` → Home Tab +- Features: Line 2179 +- Luxury Features: Line 1552 +- Stats: Line 2033 +- Amenities: Line 1356 +- Awards: Line 2524 +- Experiences: Line 2373 + +--- + +### 2. **ServiceDetailPage** (`Frontend/src/features/content/pages/ServiceDetailPage.tsx`) +**Status: ✅ Fully Controlled** + +- Service icon comes from `service.icon` field in database +- Managed via Service Management admin page + +--- + +## ⚠️ Pages with Partial Admin Control + +### 3. **AboutPage** (`Frontend/src/features/content/pages/AboutPage.tsx`) +**Status: ⚠️ Partially Controlled** + +**Controlled Icons:** +- ✅ Values Section - Icons controlled via `pageContent.values[].icon` (Admin: Line 3794) +- ✅ Features Section - Icons controlled via `pageContent.features[].icon` (Admin: Line 3885) +- ✅ Achievements Section - Icons controlled via `pageContent.achievements[].icon` (Admin: Line 4264) + +**Hardcoded Icons:** +- ❌ Hero Section - `Hotel` icon (Line 197) - Hardcoded, no admin control +- ❌ Contact Info Section - `MapPin`, `Phone`, `Mail` icons (Lines 617, 636, 651) - Hardcoded +- ❌ "Learn More" Button - `Hotel` icon (Line 671) - Hardcoded + +**Recommendation:** Add admin controls for: +1. Hero section icon (when no hero image is set) +2. Contact info section icons (optional, as these are semantic) + +--- + +### 4. **ServicesPage** (`Frontend/src/features/content/pages/ServicesPage.tsx`) +**Status: ⚠️ Partially Controlled** + +**Controlled Icons:** +- ✅ Service icons come from `service.icon` field in database + +**Hardcoded Icons:** +- ❌ Fallback icon - `Award` icon (Line 334) - Used when service has no icon + +**Recommendation:** This is acceptable as a fallback, but could be made configurable. + +--- + +## ❌ Pages with No Admin Control + +### 5. **ContactPage** (`Frontend/src/features/content/pages/ContactPage.tsx`) +**Status: ❌ All Icons Hardcoded** + +**Hardcoded Icons:** +- ❌ Hero Section - `Mail` icon (Line 191) +- ❌ Email Contact Info - `Mail` icon (Line 234) +- ❌ Phone Contact Info - `Phone` icon (Line 246) +- ❌ Location Contact Info - `MapPin` icon (Line 258) +- ❌ Form Field Icons: + - `User` icon (Line 333) - Name field + - `Mail` icon (Line 361) - Email field + - `Phone` icon (Line 387) - Phone field + - `MessageSquare` icon (Lines 410, 436) - Subject and Message fields +- ❌ Submit Button - `Send` icon (Line 497) + +**Recommendation:** Add admin controls for: +1. Hero section icon +2. Contact info section icons (Email, Phone, Location) +3. Form field icons (optional, as these are semantic UI elements) + +**Admin Location:** `PageContentDashboard.tsx` → Contact Tab (currently no icon controls) + +--- + +## 📊 Summary Statistics + +| Page | Total Icon Usages | Admin Controlled | Hardcoded | Control % | +|------|------------------|------------------|-----------|-----------| +| HomePage | ~20+ | 20+ | 0 | 100% ✅ | +| ServiceDetailPage | 1 | 1 | 0 | 100% ✅ | +| AboutPage | 8 | 3 | 5 | 37.5% ⚠️ | +| ServicesPage | ~6 | 5 | 1 | 83% ⚠️ | +| ContactPage | 8 | 0 | 8 | 0% ❌ | + +--- + +## 🔧 Recommendations + +### High Priority +1. **ContactPage** - Add icon controls for: + - Hero section icon + - Contact info section icons (Email, Phone, Location) + +### Medium Priority +2. **AboutPage** - Add icon control for: + - Hero section icon (when no hero image is set) + +### Low Priority +3. **ServicesPage** - Consider making fallback icon configurable +4. **ContactPage** - Consider making form field icons configurable (though semantic icons are fine) + +--- + +## 🛠️ Implementation Notes + +### Icon Name Handling +- Icons in database may be stored as lowercase (e.g., 'users', 'calendar') +- Lucide React icons use PascalCase (e.g., 'Users', 'Calendar') +- The `getIconComponent` helper function in HomePage.tsx handles this conversion +- Consider adding this helper to other pages that need it + +### Admin Icon Picker +- IconPicker component is available at: `Frontend/src/features/system/components/IconPicker.tsx` +- Already integrated in PageContentDashboard for Home and About pages +- Can be easily added to Contact page admin section + +--- + +## 📝 Notes + +- Most content icons are properly controlled from admin +- Hardcoded icons are mostly semantic UI elements (form fields, buttons) +- The main gap is the ContactPage which has no icon controls +- AboutPage hero section could benefit from icon control when no image is set + diff --git a/SETTINGS_USAGE_AUDIT.md b/SETTINGS_USAGE_AUDIT.md new file mode 100644 index 00000000..a687b9c3 --- /dev/null +++ b/SETTINGS_USAGE_AUDIT.md @@ -0,0 +1,221 @@ +# Settings Usage Audit Report + +## Summary +This document provides a comprehensive audit of how frontend pages use information from Settings (email, phone, address, currency, etc.) vs hardcoded values. + +--- + +## ✅ Pages Using Settings Correctly + +### 1. **Header Component** (`Frontend/src/shared/components/Header.tsx`) +**Status: ⚠️ Uses Settings with Hardcoded Fallbacks** + +- ✅ Uses `useCompanySettings()` hook +- ✅ Uses `settings.company_phone` and `settings.company_email` +- ❌ **Hardcoded fallbacks:** + - Phone: `'+1 (234) 567-890'` + - Email: `'info@luxuryhotel.com'` +- ✅ Uses `settings.company_logo_url` + +**Recommendation:** Remove hardcoded fallbacks or use empty string/null instead. + +--- + +### 2. **Footer Component** (`Frontend/src/shared/components/Footer.tsx`) +**Status: ✅ Fully Uses Settings** + +- ✅ Uses `useCompanySettings()` hook +- ✅ Uses `settings.company_phone`, `settings.company_email`, `settings.company_address` +- ✅ No hardcoded fallbacks (uses `null` if not available) +- ✅ Uses `settings.company_logo_url` + +--- + +### 3. **ContactPage** (`Frontend/src/features/content/pages/ContactPage.tsx`) +**Status: ⚠️ Uses Settings with Hardcoded Fallback Text** + +- ✅ Uses `useCompanySettings()` hook +- ✅ Uses `settings.company_phone`, `settings.company_email`, `settings.company_address` +- ❌ **Hardcoded fallback text:** + - Phone: `'Available 24/7 for your convenience'` (should be actual phone or null) + - Email: `"We'll respond within 24 hours"` (should be actual email or null) + - Address: `'Visit us at our hotel reception'` (should be actual address or null) + +**Recommendation:** Use actual values from settings or show nothing if not available. + +--- + +### 4. **AboutPage** (`Frontend/src/features/content/pages/AboutPage.tsx`) +**Status: ✅ Fully Uses Settings** + +- ✅ Uses `useCompanySettings()` hook +- ✅ Uses `settings.company_phone`, `settings.company_email`, `settings.company_address` +- ✅ No hardcoded fallbacks (uses `null` if not available) + +--- + +### 5. **Policy Pages** (Privacy, Terms, Refunds, Cancellation, Accessibility, FAQ) +**Status: ✅ Uses Settings for Email** + +- ✅ All use `useCompanySettings()` hook +- ✅ Use `settings.company_email` for contact links +- ✅ Only show email link if `settings.company_email` exists + +**Pages:** +- `PrivacyPolicyPage.tsx` +- `TermsPage.tsx` +- `RefundsPolicyPage.tsx` +- `CancellationPolicyPage.tsx` +- `AccessibilityPage.tsx` +- `FAQPage.tsx` + +--- + +### 6. **Customer Pages - Currency Usage** +**Status: ✅ All Use Currency Context** + +All customer pages use `useFormatCurrency()` hook which uses `CurrencyContext`: +- ✅ `BookingDetailPage.tsx` +- ✅ `BookingSuccessPage.tsx` +- ✅ `MyBookingsPage.tsx` +- ✅ `RoomDetailPage.tsx` +- ✅ `FullPaymentPage.tsx` +- ✅ `PaymentConfirmationPage.tsx` +- ✅ `InvoicePage.tsx` +- ✅ `GroupBookingPage.tsx` +- ✅ `DashboardPage.tsx` + +**Currency Source:** `CurrencyContext` → `localStorage.getItem('currency')` → Falls back to 'VND' + +--- + +### 7. **Content Pages - Currency Usage** +**Status: ✅ All Use Currency Context** + +- ✅ `HomePage.tsx` - Uses `useFormatCurrency()` for service prices +- ✅ `ServicesPage.tsx` - Uses `useFormatCurrency()` for service prices +- ✅ `ServiceDetailPage.tsx` - Uses `useFormatCurrency()` for service prices + +--- + +### 8. **PaymentResultPage** (`Frontend/src/pages/customer/PaymentResultPage.tsx`) +**Status: ⚠️ Uses Settings with Hardcoded Fallbacks** + +- ✅ Uses `useCompanySettings()` hook +- ❌ **Hardcoded fallbacks:** + - Email: `'support@hotel.com'` + - Phone: `'1900 xxxx'` + +**Recommendation:** Remove hardcoded fallbacks. + +--- + +### 9. **Auth Components** +**Status: ⚠️ Mixed Usage** + +**ForgotPasswordModal:** +- ✅ Uses `settings.company_email || 'support@hotel.com'` (has fallback) + +**Other Auth Components:** +- ❌ Only use placeholder text in form fields (acceptable for UX) + +--- + +## ❌ Pages with Hardcoded Values + +### 1. **PaymentConfirmationPage** (`Frontend/src/pages/customer/PaymentConfirmationPage.tsx`) +**Status: ❌ Hardcoded Bank Details** + +**Hardcoded Values:** +- Bank: `'Vietcombank (VCB)'` +- Account Number: `'0123456789'` +- Account Holder: `'KHACH SAN ABC'` + +**Recommendation:** Add bank details to Settings and make them configurable from admin. + +--- + +## 📊 Summary Statistics + +| Category | Total | Uses Settings | Hardcoded Fallbacks | Hardcoded Values | +|----------|-------|---------------|---------------------|------------------| +| **Email/Phone/Address** | 15+ pages | 12 pages | 3 pages | 0 pages | +| **Currency** | 12+ pages | 12 pages | 0 pages | 0 pages | +| **Bank Details** | 1 page | 0 pages | 0 pages | 1 page | +| **Logo** | 2 components | 2 components | 0 | 0 | + +--- + +## 🔧 Issues Found + +### High Priority +1. **PaymentConfirmationPage** - Bank details are hardcoded + - Should be added to Settings + - Should be configurable from admin + +### Medium Priority +2. **Header Component** - Hardcoded fallback phone/email + - Should use empty string or null instead of fake values + +3. **ContactPage** - Hardcoded fallback text instead of actual values + - Should show actual phone/email/address or nothing + +4. **PaymentResultPage** - Hardcoded fallback support contact + - Should use settings or show nothing + +### Low Priority +5. **Auth Components** - Placeholder text in forms (acceptable for UX) + +--- + +## ✅ What's Working Well + +1. **Currency System** - Fully centralized via `CurrencyContext` + - All pages use `useFormatCurrency()` hook + - Currency stored in localStorage + - Falls back to 'VND' if not set + +2. **Footer Component** - Perfect implementation + - Uses settings without hardcoded fallbacks + - Shows nothing if settings not available + +3. **AboutPage** - Perfect implementation + - Uses settings without hardcoded fallbacks + +4. **Policy Pages** - Good implementation + - Only show email link if available + +--- + +## 🛠️ Recommendations + +### Immediate Actions +1. **Add Bank Details to Settings** + - Add fields: `bank_name`, `bank_account_number`, `bank_account_holder` + - Update PaymentConfirmationPage to use settings + - Add admin controls for bank details + +2. **Remove Hardcoded Fallbacks** + - Header: Remove `'+1 (234) 567-890'` and `'info@luxuryhotel.com'` + - ContactPage: Remove fallback text, show actual values or nothing + - PaymentResultPage: Remove `'support@hotel.com'` and `'1900 xxxx'` + +### Future Enhancements +3. **Currency Settings Integration** + - Consider adding default currency to Company Settings + - Allow admin to set default currency for the platform + +4. **Settings Validation** + - Add validation to ensure critical settings (email, phone) are set + - Show warnings in admin if settings are missing + +--- + +## 📝 Notes + +- Currency is well-implemented via CurrencyContext +- Most pages correctly use `useCompanySettings()` hook +- Main issues are hardcoded fallback values that should be removed +- Bank details need to be added to settings system +- Placeholder text in form fields is acceptable and doesn't need changes +