updates
This commit is contained in:
@@ -1,16 +1,14 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Hotel,
|
||||
Award,
|
||||
Users,
|
||||
Heart,
|
||||
MapPin,
|
||||
Phone,
|
||||
Mail,
|
||||
Star,
|
||||
Shield,
|
||||
Clock
|
||||
Linkedin,
|
||||
Twitter
|
||||
} from 'lucide-react';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { pageContentService } from '../services/api';
|
||||
import type { PageContent } from '../services/api/pageContentService';
|
||||
@@ -23,7 +21,7 @@ const AboutPage: React.FC = () => {
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
try {
|
||||
const response = await pageContentService.getPageContent('about');
|
||||
const response = await pageContentService.getAboutContent();
|
||||
if (response.status === 'success' && response.data?.page_content) {
|
||||
setPageContent(response.data.page_content);
|
||||
|
||||
@@ -58,22 +56,22 @@ const AboutPage: React.FC = () => {
|
||||
// Default values
|
||||
const defaultValues = [
|
||||
{
|
||||
icon: Heart,
|
||||
icon: 'Heart',
|
||||
title: 'Passion',
|
||||
description: 'We are passionate about hospitality and dedicated to creating exceptional experiences for every guest.'
|
||||
},
|
||||
{
|
||||
icon: Award,
|
||||
icon: 'Award',
|
||||
title: 'Excellence',
|
||||
description: 'We strive for excellence in every aspect of our service, from the smallest detail to the grandest gesture.'
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
icon: 'Shield',
|
||||
title: 'Integrity',
|
||||
description: 'We conduct our business with honesty, transparency, and respect for our guests and community.'
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
icon: 'Users',
|
||||
title: 'Service',
|
||||
description: 'Our guests are at the heart of everything we do. Your comfort and satisfaction are our top priorities.'
|
||||
}
|
||||
@@ -81,17 +79,17 @@ const AboutPage: React.FC = () => {
|
||||
|
||||
const defaultFeatures = [
|
||||
{
|
||||
icon: Star,
|
||||
icon: 'Star',
|
||||
title: 'Premium Accommodations',
|
||||
description: 'Luxuriously appointed rooms and suites designed for ultimate comfort and relaxation.'
|
||||
},
|
||||
{
|
||||
icon: Clock,
|
||||
icon: 'Clock',
|
||||
title: '24/7 Service',
|
||||
description: 'Round-the-clock concierge and room service to attend to your needs at any time.'
|
||||
},
|
||||
{
|
||||
icon: Award,
|
||||
icon: 'Award',
|
||||
title: 'Award-Winning',
|
||||
description: 'Recognized for excellence in hospitality and guest satisfaction.'
|
||||
}
|
||||
@@ -99,7 +97,7 @@ const AboutPage: React.FC = () => {
|
||||
|
||||
const values = pageContent?.values && pageContent.values.length > 0
|
||||
? pageContent.values.map((v: any) => ({
|
||||
icon: defaultValues.find(d => d.title === v.title)?.icon || Heart,
|
||||
icon: v.icon || defaultValues.find(d => d.title === v.title)?.icon || 'Heart',
|
||||
title: v.title,
|
||||
description: v.description
|
||||
}))
|
||||
@@ -107,60 +105,122 @@ const AboutPage: React.FC = () => {
|
||||
|
||||
const features = pageContent?.features && pageContent.features.length > 0
|
||||
? pageContent.features.map((f: any) => ({
|
||||
icon: defaultFeatures.find(d => d.title === f.title)?.icon || Star,
|
||||
icon: f.icon || defaultFeatures.find(d => d.title === f.title)?.icon || 'Star',
|
||||
title: f.title,
|
||||
description: f.description
|
||||
}))
|
||||
: defaultFeatures;
|
||||
|
||||
// Parse JSON fields
|
||||
const team = pageContent?.team && typeof pageContent.team === 'string'
|
||||
? JSON.parse(pageContent.team)
|
||||
: (Array.isArray(pageContent?.team) ? pageContent.team : []);
|
||||
const timeline = pageContent?.timeline && typeof pageContent.timeline === 'string'
|
||||
? JSON.parse(pageContent.timeline)
|
||||
: (Array.isArray(pageContent?.timeline) ? pageContent.timeline : []);
|
||||
const achievements = pageContent?.achievements && typeof pageContent.achievements === 'string'
|
||||
? JSON.parse(pageContent.achievements)
|
||||
: (Array.isArray(pageContent?.achievements) ? pageContent.achievements : []);
|
||||
|
||||
// Helper to get icon component
|
||||
const getIconComponent = (iconName?: string) => {
|
||||
if (!iconName) return Heart;
|
||||
const IconComponent = (LucideIcons as any)[iconName] || Heart;
|
||||
return IconComponent;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-gray-50 to-white">
|
||||
<div className="min-h-screen bg-gradient-to-b from-slate-50 via-white to-slate-50">
|
||||
{/* Hero Section */}
|
||||
<div className="relative bg-gradient-to-b from-[#1a1a1a] to-[#0f0f0f] text-white py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="max-w-3xl mx-auto text-center animate-fade-in">
|
||||
<div className="flex justify-center mb-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#d4af37] to-[#f5d76e] rounded-full blur-2xl opacity-30"></div>
|
||||
<Hotel className="relative w-20 h-20 text-[#d4af37] drop-shadow-lg" />
|
||||
<div className={`relative ${pageContent?.about_hero_image ? '' : 'bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900'} text-white py-20 md:py-24 ${pageContent?.about_hero_image ? 'h-[400px] md:h-[450px] lg:h-[500px]' : ''} overflow-hidden`}>
|
||||
{pageContent?.about_hero_image && (
|
||||
<div className="absolute inset-0">
|
||||
<img
|
||||
src={pageContent.about_hero_image.startsWith('http') ? pageContent.about_hero_image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${pageContent.about_hero_image}`}
|
||||
alt="About Hero"
|
||||
className="w-full h-full object-cover scale-105 transition-transform duration-[20s] ease-out hover:scale-100"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/50 to-black/70"></div>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-[#d4af37]/10 via-transparent to-[#d4af37]/10"></div>
|
||||
</div>
|
||||
)}
|
||||
{!pageContent?.about_hero_image && (
|
||||
<div className="absolute inset-0">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_50%,rgba(212,175,55,0.1),transparent_70%)]"></div>
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[linear-gradient(45deg,transparent_30%,rgba(212,175,55,0.05)_50%,transparent_70%)]"></div>
|
||||
</div>
|
||||
)}
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
{!pageContent?.about_hero_image && (
|
||||
<div className="flex justify-center mb-8">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#d4af37] via-[#f5d76e] to-[#d4af37] rounded-full blur-3xl opacity-40 animate-pulse"></div>
|
||||
<div className="relative bg-gradient-to-br from-[#d4af37] to-[#c9a227] p-6 rounded-2xl shadow-2xl shadow-[#d4af37]/30">
|
||||
<Hotel className="w-12 h-12 md:w-16 md:h-16 text-white drop-shadow-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-6">
|
||||
<div className="inline-block mb-4">
|
||||
<div className="h-px w-20 bg-gradient-to-r from-transparent via-[#d4af37] to-transparent mx-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-5xl md:text-6xl font-serif font-bold mb-6 tracking-tight">
|
||||
{pageContent?.title || 'About Luxury Hotel'}
|
||||
<h1 className="text-5xl md:text-6xl lg:text-7xl font-serif font-light mb-6 tracking-[0.02em] leading-tight">
|
||||
<span className="bg-gradient-to-b from-white via-[#f5d76e] to-[#d4af37] bg-clip-text text-transparent drop-shadow-2xl">
|
||||
{pageContent?.title || 'About Luxury Hotel'}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-xl text-gray-300 font-light leading-relaxed">
|
||||
<p className="text-lg md:text-xl lg:text-2xl text-gray-200 font-light leading-relaxed max-w-2xl mx-auto tracking-wide">
|
||||
{pageContent?.subtitle || pageContent?.description || 'Where Excellence Meets Unforgettable Experiences'}
|
||||
</p>
|
||||
<div className="mt-8">
|
||||
<div className="inline-block">
|
||||
<div className="h-px w-20 bg-gradient-to-r from-transparent via-[#d4af37] to-transparent mx-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Our Story Section */}
|
||||
<section className="py-16 bg-white">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="text-center mb-12 animate-fade-in">
|
||||
<h2 className="text-4xl font-serif font-bold text-gray-900 mb-4">
|
||||
<section className="py-20 md:py-28 bg-white relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-[#d4af37]/30 to-transparent"></div>
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-block mb-4">
|
||||
<span className="text-sm font-semibold text-[#d4af37] tracking-[0.2em] uppercase">Our Heritage</span>
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl lg:text-6xl font-serif font-light text-gray-900 mb-6 tracking-tight">
|
||||
Our Story
|
||||
</h2>
|
||||
<div className="w-24 h-1 bg-gradient-to-r from-[#d4af37] to-[#f5d76e] mx-auto"></div>
|
||||
<div className="flex items-center justify-center gap-4 mb-6">
|
||||
<div className="h-px w-12 bg-gradient-to-r from-transparent to-[#d4af37]"></div>
|
||||
<div className="w-2 h-2 bg-[#d4af37] rounded-full"></div>
|
||||
<div className="h-px w-12 bg-gradient-to-l from-transparent to-[#d4af37]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="prose prose-lg max-w-none text-gray-700 leading-relaxed space-y-6 animate-slide-up">
|
||||
<div className="prose prose-lg md:prose-xl max-w-none text-gray-700 leading-relaxed space-y-8">
|
||||
{pageContent?.story_content ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: pageContent.story_content.replace(/\n/g, '<br />') }} />
|
||||
<div
|
||||
className="text-lg md:text-xl leading-relaxed font-light tracking-wide"
|
||||
dangerouslySetInnerHTML={{ __html: pageContent.story_content.replace(/\n/g, '<br />') }}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
<p className="text-lg md:text-xl leading-relaxed font-light tracking-wide first-letter:text-5xl first-letter:font-serif first-letter:text-[#d4af37] first-letter:float-left first-letter:mr-2 first-letter:leading-none">
|
||||
Welcome to Luxury Hotel, where timeless elegance meets modern sophistication.
|
||||
Since our founding, we have been dedicated to providing exceptional hospitality
|
||||
and creating unforgettable memories for our guests.
|
||||
</p>
|
||||
<p>
|
||||
<p className="text-lg md:text-xl leading-relaxed font-light tracking-wide">
|
||||
Nestled in the heart of the city, our hotel combines classic architecture with
|
||||
contemporary amenities, offering a perfect blend of comfort and luxury. Every
|
||||
detail has been carefully curated to ensure your stay exceeds expectations.
|
||||
</p>
|
||||
<p>
|
||||
<p className="text-lg md:text-xl leading-relaxed font-light tracking-wide">
|
||||
Our commitment to excellence extends beyond our beautiful rooms and facilities.
|
||||
We believe in creating meaningful connections with our guests, understanding
|
||||
their needs, and delivering personalized service that makes each visit special.
|
||||
@@ -170,34 +230,49 @@ const AboutPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-[#d4af37]/30 to-transparent"></div>
|
||||
</section>
|
||||
|
||||
{/* Values Section */}
|
||||
<section className="py-16 bg-gray-50">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-12 animate-fade-in">
|
||||
<h2 className="text-4xl font-serif font-bold text-gray-900 mb-4">
|
||||
<section className="py-20 md:py-28 bg-gradient-to-b from-slate-50 via-white to-slate-50 relative">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_30%_50%,rgba(212,175,55,0.03),transparent_50%)]"></div>
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-block mb-4">
|
||||
<span className="text-sm font-semibold text-[#d4af37] tracking-[0.2em] uppercase">Core Principles</span>
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl lg:text-6xl font-serif font-light text-gray-900 mb-6 tracking-tight">
|
||||
Our Values
|
||||
</h2>
|
||||
<div className="w-24 h-1 bg-gradient-to-r from-[#d4af37] to-[#f5d76e] mx-auto"></div>
|
||||
<div className="flex items-center justify-center gap-4 mb-6">
|
||||
<div className="h-px w-12 bg-gradient-to-r from-transparent to-[#d4af37]"></div>
|
||||
<div className="w-2 h-2 bg-[#d4af37] rounded-full"></div>
|
||||
<div className="h-px w-12 bg-gradient-to-l from-transparent to-[#d4af37]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
|
||||
{values.map((value, index) => (
|
||||
<div
|
||||
key={value.title}
|
||||
className="bg-white p-6 rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 animate-slide-up"
|
||||
className="group relative bg-white/80 backdrop-blur-sm p-8 rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[#d4af37]/30 hover:-translate-y-2"
|
||||
style={{ animationDelay: `${index * 0.1}s` }}
|
||||
>
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-[#d4af37] to-[#c9a227] rounded-lg flex items-center justify-center mb-4">
|
||||
<value.icon className="w-6 h-6 text-white" />
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-[#d4af37]/5 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div className="relative">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-[#d4af37] via-[#f5d76e] to-[#d4af37] rounded-xl flex items-center justify-center mb-6 shadow-lg shadow-[#d4af37]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
|
||||
{(() => {
|
||||
const ValueIcon = getIconComponent(value.icon);
|
||||
return <ValueIcon className="w-8 h-8 text-white drop-shadow-md" />;
|
||||
})()}
|
||||
</div>
|
||||
<h3 className="text-xl md:text-2xl font-serif font-semibold text-gray-900 mb-3 group-hover:text-[#d4af37] transition-colors duration-300">
|
||||
{value.title}
|
||||
</h3>
|
||||
<p className="text-gray-600 leading-relaxed font-light text-sm md:text-base">
|
||||
{value.description}
|
||||
</p>
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
{value.title}
|
||||
</h3>
|
||||
<p className="text-gray-600 leading-relaxed">
|
||||
{value.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -206,60 +281,303 @@ const AboutPage: React.FC = () => {
|
||||
</section>
|
||||
|
||||
{/* Features Section */}
|
||||
<section className="py-16 bg-white">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-12 animate-fade-in">
|
||||
<h2 className="text-4xl font-serif font-bold text-gray-900 mb-4">
|
||||
<section className="py-20 md:py-28 bg-white relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-[linear-gradient(135deg,transparent_0%,rgba(212,175,55,0.02)_50%,transparent_100%)]"></div>
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-block mb-4">
|
||||
<span className="text-sm font-semibold text-[#d4af37] tracking-[0.2em] uppercase">Excellence Defined</span>
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl lg:text-6xl font-serif font-light text-gray-900 mb-6 tracking-tight">
|
||||
Why Choose Us
|
||||
</h2>
|
||||
<div className="w-24 h-1 bg-gradient-to-r from-[#d4af37] to-[#f5d76e] mx-auto"></div>
|
||||
<div className="flex items-center justify-center gap-4 mb-6">
|
||||
<div className="h-px w-12 bg-gradient-to-r from-transparent to-[#d4af37]"></div>
|
||||
<div className="w-2 h-2 bg-[#d4af37] rounded-full"></div>
|
||||
<div className="h-px w-12 bg-gradient-to-l from-transparent to-[#d4af37]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{features.map((feature, index) => (
|
||||
<div
|
||||
key={feature.title}
|
||||
className="text-center p-6 animate-slide-up"
|
||||
style={{ animationDelay: `${index * 0.1}s` }}
|
||||
>
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-[#d4af37] to-[#c9a227] rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<feature.icon className="w-8 h-8 text-white" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 lg:gap-12">
|
||||
{features.map((feature, index) => {
|
||||
const FeatureIcon = getIconComponent(feature.icon);
|
||||
return (
|
||||
<div
|
||||
key={feature.title || index}
|
||||
className="group text-center p-8 relative"
|
||||
style={{ animationDelay: `${index * 0.1}s` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#d4af37]/5 to-transparent rounded-3xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div className="relative">
|
||||
<div className="w-20 h-20 bg-gradient-to-br from-[#d4af37] via-[#f5d76e] to-[#d4af37] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-xl shadow-[#d4af37]/30 group-hover:scale-110 group-hover:shadow-2xl group-hover:shadow-[#d4af37]/40 transition-all duration-500">
|
||||
<FeatureIcon className="w-10 h-10 text-white drop-shadow-lg" />
|
||||
</div>
|
||||
<h3 className="text-xl md:text-2xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[#d4af37] transition-colors duration-300">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-gray-600 leading-relaxed font-light text-sm md:text-base">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-gray-600 leading-relaxed">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Mission & Vision Section */}
|
||||
{(pageContent?.mission || pageContent?.vision) && (
|
||||
<section className="py-20 md:py-28 bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 relative overflow-hidden">
|
||||
<div className="absolute inset-0">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_50%,rgba(212,175,55,0.1),transparent_70%)]"></div>
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[linear-gradient(45deg,transparent_30%,rgba(212,175,55,0.05)_50%,transparent_70%)]"></div>
|
||||
</div>
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-8 lg:gap-12">
|
||||
{pageContent.mission && (
|
||||
<div className="group relative bg-white/95 backdrop-blur-sm p-10 md:p-12 rounded-2xl shadow-2xl border border-[#d4af37]/20 hover:border-[#d4af37]/40 transition-all duration-500 hover:shadow-[#d4af37]/20 hover:-translate-y-1">
|
||||
<div className="absolute top-0 right-0 w-40 h-40 bg-gradient-to-br from-[#d4af37]/10 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div className="relative">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-1 h-12 bg-gradient-to-b from-[#d4af37] to-[#f5d76e] rounded-full"></div>
|
||||
<h2 className="text-3xl md:text-4xl font-serif font-light text-gray-900">Our Mission</h2>
|
||||
</div>
|
||||
<p className="text-gray-700 leading-relaxed text-base md:text-lg font-light tracking-wide">{pageContent.mission}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{pageContent.vision && (
|
||||
<div className="group relative bg-white/95 backdrop-blur-sm p-10 md:p-12 rounded-2xl shadow-2xl border border-[#d4af37]/20 hover:border-[#d4af37]/40 transition-all duration-500 hover:shadow-[#d4af37]/20 hover:-translate-y-1">
|
||||
<div className="absolute top-0 right-0 w-40 h-40 bg-gradient-to-br from-[#d4af37]/10 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div className="relative">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-1 h-12 bg-gradient-to-b from-[#d4af37] to-[#f5d76e] rounded-full"></div>
|
||||
<h2 className="text-3xl md:text-4xl font-serif font-light text-gray-900">Our Vision</h2>
|
||||
</div>
|
||||
<p className="text-gray-700 leading-relaxed text-base md:text-lg font-light tracking-wide">{pageContent.vision}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Team Section */}
|
||||
{team && team.length > 0 && (
|
||||
<section className="py-20 md:py-28 bg-gradient-to-b from-white via-slate-50 to-white relative">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-block mb-4">
|
||||
<span className="text-sm font-semibold text-[#d4af37] tracking-[0.2em] uppercase">Meet The Experts</span>
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl lg:text-6xl font-serif font-light text-gray-900 mb-6 tracking-tight">
|
||||
Our Team
|
||||
</h2>
|
||||
<div className="flex items-center justify-center gap-4 mb-6">
|
||||
<div className="h-px w-12 bg-gradient-to-r from-transparent to-[#d4af37]"></div>
|
||||
<div className="w-2 h-2 bg-[#d4af37] rounded-full"></div>
|
||||
<div className="h-px w-12 bg-gradient-to-l from-transparent to-[#d4af37]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-10">
|
||||
{team.map((member: any, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="group relative bg-white rounded-2xl shadow-xl overflow-hidden hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[#d4af37]/30 hover:-translate-y-2"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#d4af37]/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 z-10"></div>
|
||||
{member.image && (
|
||||
<div className="relative overflow-hidden h-72">
|
||||
<img
|
||||
src={member.image.startsWith('http') ? member.image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${member.image}`}
|
||||
alt={member.name}
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-8 relative z-10">
|
||||
<h3 className="text-2xl font-serif font-semibold text-gray-900 mb-2 group-hover:text-[#d4af37] transition-colors duration-300">{member.name}</h3>
|
||||
<p className="text-[#d4af37] font-medium mb-4 text-sm tracking-wide uppercase">{member.role}</p>
|
||||
{member.bio && <p className="text-gray-600 text-sm mb-6 leading-relaxed font-light">{member.bio}</p>}
|
||||
{member.social_links && (
|
||||
<div className="flex gap-4 pt-4 border-t border-gray-100">
|
||||
{member.social_links.linkedin && (
|
||||
<a
|
||||
href={member.social_links.linkedin}
|
||||
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-[#d4af37] hover:text-white transition-all duration-300 group-hover:scale-110"
|
||||
>
|
||||
<Linkedin className="w-5 h-5" />
|
||||
</a>
|
||||
)}
|
||||
{member.social_links.twitter && (
|
||||
<a
|
||||
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-[#d4af37] hover:text-white transition-all duration-300 group-hover:scale-110"
|
||||
>
|
||||
<Twitter className="w-5 h-5" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Timeline Section */}
|
||||
{timeline && timeline.length > 0 && (
|
||||
<section className="py-20 md:py-28 bg-gradient-to-b from-slate-50 via-white to-slate-50 relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_70%_50%,rgba(212,175,55,0.03),transparent_50%)]"></div>
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-block mb-4">
|
||||
<span className="text-sm font-semibold text-[#d4af37] tracking-[0.2em] uppercase">Our Journey</span>
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl lg:text-6xl font-serif font-light text-gray-900 mb-6 tracking-tight">
|
||||
Our History
|
||||
</h2>
|
||||
<div className="flex items-center justify-center gap-4 mb-6">
|
||||
<div className="h-px w-12 bg-gradient-to-r from-transparent to-[#d4af37]"></div>
|
||||
<div className="w-2 h-2 bg-[#d4af37] rounded-full"></div>
|
||||
<div className="h-px w-12 bg-gradient-to-l from-transparent to-[#d4af37]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="absolute left-8 md:left-1/2 transform md:-translate-x-1/2 w-1 h-full bg-gradient-to-b from-[#d4af37] via-[#f5d76e] to-[#d4af37] shadow-lg"></div>
|
||||
<div className="space-y-12 md:space-y-16">
|
||||
{timeline.map((event: any, index: number) => (
|
||||
<div key={index} className={`relative flex items-center ${index % 2 === 0 ? 'md:flex-row' : 'md:flex-row-reverse'}`}>
|
||||
<div className="absolute left-6 md:left-1/2 transform md:-translate-x-1/2 w-6 h-6 bg-gradient-to-br from-[#d4af37] to-[#c9a227] rounded-full border-4 border-white shadow-xl z-10 group-hover:scale-125 transition-transform duration-300"></div>
|
||||
<div className={`ml-20 md:ml-0 md:w-5/12 ${index % 2 === 0 ? 'md:mr-auto md:pr-8' : 'md:ml-auto md:pl-8'}`}>
|
||||
<div className="group bg-white/90 backdrop-blur-sm p-8 rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[#d4af37]/30">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="text-[#d4af37] font-bold text-2xl md:text-3xl font-serif">{event.year}</div>
|
||||
<div className="h-px flex-1 bg-gradient-to-r from-[#d4af37] to-transparent"></div>
|
||||
</div>
|
||||
<h3 className="text-2xl md:text-3xl font-serif font-semibold text-gray-900 mb-3 group-hover:text-[#d4af37] transition-colors duration-300">{event.title}</h3>
|
||||
<p className="text-gray-600 leading-relaxed font-light mb-4">{event.description}</p>
|
||||
{event.image && (
|
||||
<div className="mt-6 overflow-hidden rounded-xl">
|
||||
<img
|
||||
src={event.image.startsWith('http') ? event.image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${event.image}`}
|
||||
alt={event.title}
|
||||
className="w-full h-56 md:h-64 object-cover group-hover:scale-110 transition-transform duration-700"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Achievements Section */}
|
||||
{achievements && achievements.length > 0 && (
|
||||
<section className="py-20 md:py-28 bg-white relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-[linear-gradient(135deg,transparent_0%,rgba(212,175,55,0.02)_50%,transparent_100%)]"></div>
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-block mb-4">
|
||||
<span className="text-sm font-semibold text-[#d4af37] tracking-[0.2em] uppercase">Recognition</span>
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl lg:text-6xl font-serif font-light text-gray-900 mb-6 tracking-tight">
|
||||
Achievements & Awards
|
||||
</h2>
|
||||
<div className="flex items-center justify-center gap-4 mb-6">
|
||||
<div className="h-px w-12 bg-gradient-to-r from-transparent to-[#d4af37]"></div>
|
||||
<div className="w-2 h-2 bg-[#d4af37] rounded-full"></div>
|
||||
<div className="h-px w-12 bg-gradient-to-l from-transparent to-[#d4af37]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-10">
|
||||
{achievements.map((achievement: any, index: number) => {
|
||||
const AchievementIcon = getIconComponent(achievement.icon);
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="group relative bg-gradient-to-br from-white to-slate-50 p-8 rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[#d4af37]/40 hover:-translate-y-2"
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-[#d4af37]/10 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div className="relative">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-[#d4af37] via-[#f5d76e] to-[#d4af37] rounded-xl flex items-center justify-center shadow-lg shadow-[#d4af37]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
|
||||
<AchievementIcon className="w-8 h-8 text-white drop-shadow-md" />
|
||||
</div>
|
||||
{achievement.year && (
|
||||
<div className="text-[#d4af37] font-bold text-2xl font-serif">{achievement.year}</div>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-xl md:text-2xl font-serif font-semibold text-gray-900 mb-3 group-hover:text-[#d4af37] transition-colors duration-300">{achievement.title}</h3>
|
||||
<p className="text-gray-600 text-sm md:text-base leading-relaxed font-light mb-4">{achievement.description}</p>
|
||||
{achievement.image && (
|
||||
<div className="mt-6 overflow-hidden rounded-xl">
|
||||
<img
|
||||
src={achievement.image.startsWith('http') ? achievement.image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${achievement.image}`}
|
||||
alt={achievement.title}
|
||||
className="w-full h-40 object-cover group-hover:scale-110 transition-transform duration-700"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Contact Section */}
|
||||
<section className="py-16 bg-gradient-to-b from-gray-50 to-white">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="text-center mb-12 animate-fade-in">
|
||||
<h2 className="text-4xl font-serif font-bold text-gray-900 mb-4">
|
||||
<section className="py-20 md:py-28 bg-gradient-to-br from-slate-50 via-white to-slate-50 relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_50%,rgba(212,175,55,0.03),transparent_70%)]"></div>
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-block mb-4">
|
||||
<span className="text-sm font-semibold text-[#d4af37] tracking-[0.2em] uppercase">Connect With Us</span>
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl lg:text-6xl font-serif font-light text-gray-900 mb-6 tracking-tight">
|
||||
Get In Touch
|
||||
</h2>
|
||||
<div className="w-24 h-1 bg-gradient-to-r from-[#d4af37] to-[#f5d76e] mx-auto"></div>
|
||||
<p className="text-gray-600 mt-4">
|
||||
<div className="flex items-center justify-center gap-4 mb-6">
|
||||
<div className="h-px w-12 bg-gradient-to-r from-transparent to-[#d4af37]"></div>
|
||||
<div className="w-2 h-2 bg-[#d4af37] rounded-full"></div>
|
||||
<div className="h-px w-12 bg-gradient-to-l from-transparent to-[#d4af37]"></div>
|
||||
</div>
|
||||
<p className="text-gray-600 mt-6 text-lg font-light max-w-2xl mx-auto">
|
||||
We'd love to hear from you. Contact us for reservations or inquiries.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div className="text-center p-6 bg-white rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 animate-slide-up">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-[#d4af37] to-[#c9a227] rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<MapPin className="w-6 h-6 text-white" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 lg:gap-10 mb-16">
|
||||
<div className="group text-center p-8 bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[#d4af37]/40 hover:-translate-y-2">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-[#d4af37] via-[#f5d76e] to-[#d4af37] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-[#d4af37]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
|
||||
<MapPin className="w-8 h-8 text-white drop-shadow-md" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[#d4af37] transition-colors duration-300">
|
||||
Address
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
<p className="text-gray-600 leading-relaxed font-light">
|
||||
{displayAddress
|
||||
.split('\n').map((line, i) => (
|
||||
<React.Fragment key={i}>
|
||||
@@ -269,40 +587,41 @@ const AboutPage: React.FC = () => {
|
||||
))}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center p-6 bg-white rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 animate-slide-up" style={{ animationDelay: '0.1s' }}>
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-[#d4af37] to-[#c9a227] rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Phone className="w-6 h-6 text-white" />
|
||||
<div className="group text-center p-8 bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[#d4af37]/40 hover:-translate-y-2" style={{ animationDelay: '0.1s' }}>
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-[#d4af37] via-[#f5d76e] to-[#d4af37] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-[#d4af37]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
|
||||
<Phone className="w-8 h-8 text-white drop-shadow-md" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[#d4af37] transition-colors duration-300">
|
||||
Phone
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
<a href={`tel:${displayPhone.replace(/\s+/g, '').replace(/[()]/g, '')}`} className="hover:text-[#d4af37] transition-colors">
|
||||
<p className="text-gray-600 font-light">
|
||||
<a href={`tel:${displayPhone.replace(/\s+/g, '').replace(/[()]/g, '')}`} className="hover:text-[#d4af37] transition-colors duration-300">
|
||||
{displayPhone}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center p-6 bg-white rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 animate-slide-up" style={{ animationDelay: '0.2s' }}>
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-[#d4af37] to-[#c9a227] rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Mail className="w-6 h-6 text-white" />
|
||||
<div className="group text-center p-8 bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[#d4af37]/40 hover:-translate-y-2" style={{ animationDelay: '0.2s' }}>
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-[#d4af37] via-[#f5d76e] to-[#d4af37] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-[#d4af37]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
|
||||
<Mail className="w-8 h-8 text-white drop-shadow-md" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[#d4af37] transition-colors duration-300">
|
||||
Email
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
<a href={`mailto:${displayEmail}`} className="hover:text-[#d4af37] transition-colors">
|
||||
<p className="text-gray-600 font-light">
|
||||
<a href={`mailto:${displayEmail}`} className="hover:text-[#d4af37] transition-colors duration-300">
|
||||
{displayEmail}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center mt-12 animate-fade-in">
|
||||
<div className="text-center">
|
||||
<Link
|
||||
to="/rooms"
|
||||
className="inline-flex items-center space-x-2 px-8 py-3 bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-white rounded-lg hover:from-[#f5d76e] hover:to-[#d4af37] transition-all duration-300 font-medium shadow-lg shadow-[#d4af37]/20 hover:shadow-[#d4af37]/30"
|
||||
className="group inline-flex items-center space-x-3 px-10 py-4 bg-gradient-to-r from-[#d4af37] via-[#f5d76e] to-[#d4af37] text-white rounded-xl hover:shadow-2xl hover:shadow-[#d4af37]/40 transition-all duration-500 font-medium text-lg tracking-wide relative overflow-hidden"
|
||||
>
|
||||
<span>Explore Our Rooms</span>
|
||||
<Hotel className="w-5 h-5" />
|
||||
<span className="relative z-10">Explore Our Rooms</span>
|
||||
<Hotel className="w-5 h-5 relative z-10 group-hover:translate-x-1 transition-transform duration-300" />
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-[#f5d76e] to-[#d4af37] opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -99,7 +99,7 @@ const ContactPage: React.FC = () => {
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
try {
|
||||
const response = await pageContentService.getPageContent('contact');
|
||||
const response = await pageContentService.getContactContent();
|
||||
if (response.status === 'success' && response.data?.page_content) {
|
||||
setPageContent(response.data.page_content);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,23 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Search, Eye, XCircle, CheckCircle, Loader2 } from 'lucide-react';
|
||||
import { bookingService, Booking } from '../../services/api';
|
||||
import { Search, Eye, XCircle, CheckCircle, Loader2, FileText } from 'lucide-react';
|
||||
import { bookingService, Booking, invoiceService } from '../../services/api';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../utils/format';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const BookingManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
const navigate = useNavigate();
|
||||
const [bookings, setBookings] = useState<Booking[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedBooking, setSelectedBooking] = useState<Booking | null>(null);
|
||||
const [showDetailModal, setShowDetailModal] = useState(false);
|
||||
const [updatingBookingId, setUpdatingBookingId] = useState<number | null>(null);
|
||||
const [cancellingBookingId, setCancellingBookingId] = useState<number | null>(null);
|
||||
const [creatingInvoice, setCreatingInvoice] = useState(false);
|
||||
const [filters, setFilters] = useState({
|
||||
search: '',
|
||||
status: '',
|
||||
@@ -80,6 +83,32 @@ const BookingManagementPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateInvoice = async (bookingId: number) => {
|
||||
try {
|
||||
setCreatingInvoice(true);
|
||||
// Ensure bookingId is a number
|
||||
const invoiceData = {
|
||||
booking_id: Number(bookingId),
|
||||
};
|
||||
|
||||
const response = await invoiceService.createInvoice(invoiceData);
|
||||
|
||||
if (response.status === 'success' && response.data?.invoice) {
|
||||
toast.success('Invoice created successfully!');
|
||||
setShowDetailModal(false);
|
||||
navigate(`/admin/invoices/${response.data.invoice.id}`);
|
||||
} else {
|
||||
throw new Error('Failed to create invoice');
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.response?.data?.detail || error.response?.data?.message || error.message || 'Unable to create invoice';
|
||||
toast.error(errorMessage);
|
||||
console.error('Invoice creation error:', error);
|
||||
} finally {
|
||||
setCreatingInvoice(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const badges: Record<string, { bg: string; text: string; label: string; border: string }> = {
|
||||
pending: {
|
||||
@@ -622,7 +651,24 @@ const BookingManagementPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className="mt-8 pt-6 border-t border-slate-200 flex justify-end">
|
||||
<div className="mt-8 pt-6 border-t border-slate-200 flex justify-between items-center">
|
||||
<button
|
||||
onClick={() => handleCreateInvoice(selectedBooking.id)}
|
||||
disabled={creatingInvoice}
|
||||
className="flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-xl font-semibold hover:from-amber-600 hover:to-amber-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-lg hover:shadow-xl"
|
||||
>
|
||||
{creatingInvoice ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
Creating Invoice...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileText className="w-5 h-5" />
|
||||
Create Invoice
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowDetailModal(false)}
|
||||
className="px-8 py-3 bg-gradient-to-r from-slate-700 to-slate-800 text-white rounded-xl font-semibold hover:from-slate-800 hover:to-slate-900 transition-all duration-200 shadow-lg hover:shadow-xl border border-slate-600"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Search, User, Hotel, CheckCircle, AlertCircle } from 'lucide-react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Search, User, Hotel, CheckCircle, AlertCircle, Calendar, LogIn, LogOut } from 'lucide-react';
|
||||
import { bookingService, Booking } from '../../services/api';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
@@ -14,7 +14,14 @@ interface GuestInfo {
|
||||
|
||||
const CheckInPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
const [bookingNumber, setBookingNumber] = useState('');
|
||||
const [selectedDate, setSelectedDate] = useState<string>(() => {
|
||||
const today = new Date();
|
||||
return today.toISOString().split('T')[0];
|
||||
});
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [checkInBookings, setCheckInBookings] = useState<Booking[]>([]);
|
||||
const [checkOutBookings, setCheckOutBookings] = useState<Booking[]>([]);
|
||||
const [loadingBookings, setLoadingBookings] = useState(false);
|
||||
const [booking, setBooking] = useState<Booking | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searching, setSearching] = useState(false);
|
||||
@@ -24,19 +31,77 @@ const CheckInPage: React.FC = () => {
|
||||
const [children, setChildren] = useState(0);
|
||||
const [additionalFee, setAdditionalFee] = useState(0);
|
||||
|
||||
const handleSearch = async () => {
|
||||
if (!bookingNumber.trim()) {
|
||||
// Fetch bookings for the selected date
|
||||
useEffect(() => {
|
||||
fetchBookingsForDate();
|
||||
}, [selectedDate, searchQuery]);
|
||||
|
||||
const fetchBookingsForDate = async () => {
|
||||
if (!selectedDate) return;
|
||||
|
||||
try {
|
||||
setLoadingBookings(true);
|
||||
const date = new Date(selectedDate);
|
||||
date.setHours(0, 0, 0, 0);
|
||||
const nextDay = new Date(date);
|
||||
nextDay.setDate(nextDay.getDate() + 1);
|
||||
|
||||
// Fetch all bookings (we'll filter on client side for accuracy)
|
||||
const params: any = {
|
||||
limit: 200, // Fetch more to ensure we get all bookings for the date range
|
||||
};
|
||||
if (searchQuery) {
|
||||
params.search = searchQuery;
|
||||
}
|
||||
|
||||
// Fetch bookings around the selected date (3 days before and after to be safe)
|
||||
const startDate = new Date(date);
|
||||
startDate.setDate(startDate.getDate() - 3);
|
||||
const endDate = new Date(date);
|
||||
endDate.setDate(endDate.getDate() + 3);
|
||||
|
||||
params.startDate = startDate.toISOString();
|
||||
params.endDate = endDate.toISOString();
|
||||
|
||||
const response = await bookingService.getAllBookings(params);
|
||||
const allBookings = response.data.bookings || [];
|
||||
|
||||
// Filter check-ins: confirmed bookings with check_in_date matching selected date
|
||||
const filteredCheckIns = allBookings.filter((booking) => {
|
||||
if (!booking.check_in_date || booking.status !== 'confirmed') return false;
|
||||
const checkInDate = new Date(booking.check_in_date);
|
||||
checkInDate.setHours(0, 0, 0, 0);
|
||||
return checkInDate.getTime() === date.getTime();
|
||||
});
|
||||
setCheckInBookings(filteredCheckIns);
|
||||
|
||||
// Filter check-outs: checked_in bookings with check_out_date matching selected date
|
||||
const filteredCheckOuts = allBookings.filter((booking) => {
|
||||
if (!booking.check_out_date || booking.status !== 'checked_in') return false;
|
||||
const checkOutDate = new Date(booking.check_out_date);
|
||||
checkOutDate.setHours(0, 0, 0, 0);
|
||||
return checkOutDate.getTime() === date.getTime();
|
||||
});
|
||||
setCheckOutBookings(filteredCheckOuts);
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.message || 'Unable to load bookings');
|
||||
} finally {
|
||||
setLoadingBookings(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearchByNumber = async () => {
|
||||
if (!searchQuery.trim()) {
|
||||
toast.error('Please enter booking number');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setSearching(true);
|
||||
const response = await bookingService.checkBookingByNumber(bookingNumber);
|
||||
const response = await bookingService.checkBookingByNumber(searchQuery);
|
||||
setBooking(response.data.booking);
|
||||
setActualRoomNumber(response.data.booking.room?.room_number || '');
|
||||
|
||||
// Show warning if there's remaining balance
|
||||
if ((response as any).warning) {
|
||||
const warning = (response as any).warning;
|
||||
toast.warning(
|
||||
@@ -54,6 +119,30 @@ const CheckInPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectBooking = async (bookingNumber: string) => {
|
||||
try {
|
||||
setSearching(true);
|
||||
const response = await bookingService.checkBookingByNumber(bookingNumber);
|
||||
setBooking(response.data.booking);
|
||||
setActualRoomNumber(response.data.booking.room?.room_number || '');
|
||||
setSearchQuery(bookingNumber);
|
||||
|
||||
if ((response as any).warning) {
|
||||
const warning = (response as any).warning;
|
||||
toast.warning(
|
||||
`⚠️ Payment Reminder: Guest has remaining balance of ${formatCurrency(warning.remaining_balance)} (${warning.payment_percentage.toFixed(1)}% paid)`,
|
||||
{ autoClose: 8000 }
|
||||
);
|
||||
} else {
|
||||
toast.success('Booking selected');
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.message || 'Unable to load booking');
|
||||
} finally {
|
||||
setSearching(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddGuest = () => {
|
||||
setGuests([...guests, { name: '', id_number: '', phone: '' }]);
|
||||
};
|
||||
@@ -71,9 +160,8 @@ const CheckInPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const calculateAdditionalFee = () => {
|
||||
// Logic to calculate additional fees: children and extra person
|
||||
const extraPersonFee = extraPersons * 200000; // 200k/person
|
||||
const childrenFee = children * 100000; // 100k/child
|
||||
const extraPersonFee = extraPersons * 200000;
|
||||
const childrenFee = children * 100000;
|
||||
const total = extraPersonFee + childrenFee;
|
||||
setAdditionalFee(total);
|
||||
return total;
|
||||
@@ -82,7 +170,6 @@ const CheckInPage: React.FC = () => {
|
||||
const handleCheckIn = async () => {
|
||||
if (!booking) return;
|
||||
|
||||
// Validate
|
||||
if (!actualRoomNumber.trim()) {
|
||||
toast.error('Please enter actual room number');
|
||||
return;
|
||||
@@ -96,15 +183,12 @@ const CheckInPage: React.FC = () => {
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
// Calculate additional fee
|
||||
calculateAdditionalFee();
|
||||
|
||||
const response = await bookingService.updateBooking(booking.id, {
|
||||
status: 'checked_in',
|
||||
// Can send additional data about guests, room_number, additional_fee
|
||||
} as any);
|
||||
|
||||
// Show warning if there's remaining balance
|
||||
if ((response as any).warning) {
|
||||
const warning = (response as any).warning;
|
||||
toast.warning(
|
||||
@@ -115,14 +199,15 @@ const CheckInPage: React.FC = () => {
|
||||
toast.success('Check-in successful');
|
||||
}
|
||||
|
||||
// Reset form
|
||||
// Reset form and refresh bookings
|
||||
setBooking(null);
|
||||
setBookingNumber('');
|
||||
setSearchQuery('');
|
||||
setActualRoomNumber('');
|
||||
setGuests([{ name: '', id_number: '', phone: '' }]);
|
||||
setExtraPersons(0);
|
||||
setChildren(0);
|
||||
setAdditionalFee(0);
|
||||
await fetchBookingsForDate();
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.message || 'An error occurred during check-in');
|
||||
} finally {
|
||||
@@ -130,6 +215,15 @@ const CheckInPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
@@ -144,39 +238,187 @@ const CheckInPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Booking */}
|
||||
{/* Date and Search Filters */}
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm">
|
||||
<h2 className="text-lg font-semibold mb-4">1. Search booking</h2>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
<Calendar className="w-4 h-4 inline mr-2" />
|
||||
Select Date
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={bookingNumber}
|
||||
onChange={(e) => setBookingNumber(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
|
||||
placeholder="Enter booking number"
|
||||
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
type="date"
|
||||
value={selectedDate}
|
||||
onChange={(e) => setSelectedDate(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
disabled={searching}
|
||||
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 flex items-center gap-2"
|
||||
>
|
||||
{searching ? 'Searching...' : 'Search'}
|
||||
</button>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
<Search className="w-4 h-4 inline mr-2" />
|
||||
Search by Booking Number
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleSearchByNumber()}
|
||||
placeholder="Enter booking number"
|
||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSearchByNumber}
|
||||
disabled={searching}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
|
||||
>
|
||||
{searching ? '...' : 'Search'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<button
|
||||
onClick={() => {
|
||||
const today = new Date();
|
||||
setSelectedDate(today.toISOString().split('T')[0]);
|
||||
setSearchQuery('');
|
||||
}}
|
||||
className="w-full px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200"
|
||||
>
|
||||
Reset to Today
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Booking Info */}
|
||||
{/* Check-ins and Check-outs Lists */}
|
||||
{!booking && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Check-ins for Today */}
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold flex items-center gap-2">
|
||||
<LogIn className="w-5 h-5 text-green-600" />
|
||||
Check-ins for {formatDate(selectedDate)}
|
||||
</h2>
|
||||
<span className="px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm font-medium">
|
||||
{checkInBookings.length}
|
||||
</span>
|
||||
</div>
|
||||
{loadingBookings ? (
|
||||
<div className="text-center py-8">
|
||||
<Loading />
|
||||
</div>
|
||||
) : checkInBookings.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<LogIn className="w-12 h-12 mx-auto mb-2 text-gray-300" />
|
||||
<p>No check-ins scheduled for this date</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{checkInBookings.map((b) => (
|
||||
<div
|
||||
key={b.id}
|
||||
onClick={() => handleSelectBooking(b.booking_number)}
|
||||
className="p-4 border border-gray-200 rounded-lg hover:border-blue-500 hover:shadow-md cursor-pointer transition-all"
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="font-semibold text-gray-900">{b.booking_number}</div>
|
||||
<div className="text-sm text-gray-600 mt-1">{b.user?.full_name}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
{b.room?.room_type?.name} • {formatCurrency(b.total_price)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
b.status === 'confirmed' ? 'bg-green-100 text-green-800' :
|
||||
b.status === 'checked_in' ? 'bg-blue-100 text-blue-800' :
|
||||
'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{b.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Check-outs for Today */}
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold flex items-center gap-2">
|
||||
<LogOut className="w-5 h-5 text-orange-600" />
|
||||
Check-outs for {formatDate(selectedDate)}
|
||||
</h2>
|
||||
<span className="px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-sm font-medium">
|
||||
{checkOutBookings.length}
|
||||
</span>
|
||||
</div>
|
||||
{loadingBookings ? (
|
||||
<div className="text-center py-8">
|
||||
<Loading />
|
||||
</div>
|
||||
) : checkOutBookings.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<LogOut className="w-12 h-12 mx-auto mb-2 text-gray-300" />
|
||||
<p>No check-outs scheduled for this date</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{checkOutBookings.map((b) => (
|
||||
<div
|
||||
key={b.id}
|
||||
onClick={() => handleSelectBooking(b.booking_number)}
|
||||
className="p-4 border border-gray-200 rounded-lg hover:border-orange-500 hover:shadow-md cursor-pointer transition-all"
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="font-semibold text-gray-900">{b.booking_number}</div>
|
||||
<div className="text-sm text-gray-600 mt-1">{b.user?.full_name}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
{b.room?.room_type?.name} • {formatCurrency(b.total_price)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
b.status === 'checked_in' ? 'bg-blue-100 text-blue-800' :
|
||||
b.status === 'checked_out' ? 'bg-gray-100 text-gray-800' :
|
||||
'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{b.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Booking Info and Check-in Form */}
|
||||
{booking && (
|
||||
<>
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm">
|
||||
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||
2. Booking Information
|
||||
</h2>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||
2. Booking Information
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => {
|
||||
setBooking(null);
|
||||
setSearchQuery('');
|
||||
}}
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<div className="space-y-3">
|
||||
@@ -299,7 +541,6 @@ const CheckInPage: React.FC = () => {
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
{(() => {
|
||||
// Use payment_balance from API if available, otherwise calculate from payments
|
||||
const paymentBalance = booking.payment_balance || (() => {
|
||||
const completedPayments = booking.payments?.filter(
|
||||
(p) => p.payment_status === 'completed'
|
||||
@@ -545,19 +786,6 @@ const CheckInPage: React.FC = () => {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{!booking && !searching && (
|
||||
<div className="bg-gray-50 rounded-lg p-12 text-center">
|
||||
<Search className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
No booking selected
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
Please enter booking number above to start check-in process
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -47,7 +47,8 @@ const InvoiceManagementPage: React.FC = () => {
|
||||
invoiceList = invoiceList.filter((inv) =>
|
||||
inv.invoice_number.toLowerCase().includes(filters.search.toLowerCase()) ||
|
||||
inv.customer_name.toLowerCase().includes(filters.search.toLowerCase()) ||
|
||||
inv.customer_email.toLowerCase().includes(filters.search.toLowerCase())
|
||||
inv.customer_email.toLowerCase().includes(filters.search.toLowerCase()) ||
|
||||
(inv.promotion_code && inv.promotion_code.toLowerCase().includes(filters.search.toLowerCase()))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -130,11 +131,11 @@ const InvoiceManagementPage: React.FC = () => {
|
||||
<p className="text-slate-600 mt-3 text-lg font-light">Manage and track all invoices</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate('/admin/invoices/create')}
|
||||
onClick={() => navigate('/admin/bookings')}
|
||||
className="flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-xl font-semibold hover:from-amber-600 hover:to-amber-700 transition-all duration-200 shadow-lg hover:shadow-xl"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
Create Invoice
|
||||
Create Invoice from Booking
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -190,6 +191,9 @@ const InvoiceManagementPage: React.FC = () => {
|
||||
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">
|
||||
Amount
|
||||
</th>
|
||||
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">
|
||||
Promotion
|
||||
</th>
|
||||
<th className="px-8 py-5 text-left text-xs font-bold text-amber-100 uppercase tracking-wider border-b border-slate-700">
|
||||
Status
|
||||
</th>
|
||||
@@ -235,6 +239,25 @@ const InvoiceManagementPage: React.FC = () => {
|
||||
Due: {formatCurrency(invoice.balance_due)}
|
||||
</div>
|
||||
)}
|
||||
{invoice.discount_amount > 0 && (
|
||||
<div className="text-xs text-green-600 font-medium mt-1">
|
||||
Discount: -{formatCurrency(invoice.discount_amount)}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-8 py-5 whitespace-nowrap">
|
||||
{invoice.promotion_code ? (
|
||||
<span className="px-3 py-1 text-xs font-semibold rounded-full bg-gradient-to-r from-purple-50 to-pink-50 text-purple-700 border border-purple-200">
|
||||
{invoice.promotion_code}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs text-slate-400">—</span>
|
||||
)}
|
||||
{invoice.is_proforma && (
|
||||
<div className="text-xs text-blue-600 font-medium mt-1">
|
||||
Proforma
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-8 py-5 whitespace-nowrap">
|
||||
<span className={`px-4 py-1.5 text-xs font-semibold rounded-full border shadow-sm ${statusBadge.bg} ${statusBadge.text} ${statusBadge.border || ''}`}>
|
||||
@@ -274,7 +297,7 @@ const InvoiceManagementPage: React.FC = () => {
|
||||
})
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-8 py-12 text-center">
|
||||
<td colSpan={8} className="px-8 py-12 text-center">
|
||||
<div className="text-slate-500">
|
||||
<FileText className="w-16 h-16 mx-auto mb-4 text-slate-300" />
|
||||
<p className="text-lg font-semibold">No invoices found</p>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -125,8 +125,17 @@ const InvoicePage: React.FC = () => {
|
||||
{/* Invoice Header */}
|
||||
<div className="flex justify-between items-start mb-8 pb-8 border-b-2 border-gray-200">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Invoice</h1>
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
{invoice.is_proforma ? 'Proforma Invoice' : 'Invoice'}
|
||||
</h1>
|
||||
<p className="text-gray-600">#{invoice.invoice_number}</p>
|
||||
{invoice.promotion_code && (
|
||||
<div className="mt-2">
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold bg-gradient-to-r from-purple-100 to-pink-100 text-purple-700 border border-purple-200">
|
||||
Promotion Code: {invoice.promotion_code}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`flex items-center gap-2 px-4 py-2 rounded-lg border ${getStatusColor(invoice.status)}`}>
|
||||
{getStatusIcon(invoice.status)}
|
||||
@@ -252,8 +261,10 @@ const InvoicePage: React.FC = () => {
|
||||
<span>{formatCurrency(invoice.subtotal)}</span>
|
||||
</div>
|
||||
{invoice.discount_amount > 0 && (
|
||||
<div className="flex justify-between text-gray-600">
|
||||
<span>Discount:</span>
|
||||
<div className="flex justify-between text-green-600 font-medium">
|
||||
<span>
|
||||
Discount{invoice.promotion_code ? ` (${invoice.promotion_code})` : ''}:
|
||||
</span>
|
||||
<span>-{formatCurrency(invoice.discount_amount)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user