From a38ab4fa822471a8b984ee52c7b4a8c29d6275b5 Mon Sep 17 00:00:00 2001 From: Iliyan Angelov Date: Thu, 20 Nov 2025 21:06:30 +0200 Subject: [PATCH] updates --- ...dd_section_title_fields_to_page_content.py | 246 ++ ...9cc3_add_luxury_section_fields_to_page_.py | 38 + ...tle_fields_to_page_content.cpython-312.pyc | Bin 0 -> 22089 bytes ...ry_section_fields_to_page_.cpython-312.pyc | Bin 0 -> 2300 bytes .../add_about_page_fields.cpython-312.pyc | Bin 0 -> 2180 bytes ...right_text_to_page_content.cpython-312.pyc | Bin 0 -> 1056 bytes ...d_privacy_terms_page_types.cpython-312.pyc | Bin 0 -> 1200 bytes ...omotion_fields_to_bookings.cpython-312.pyc | Bin 1941 -> 1941 bytes ...ry_content_fields_to_page_.cpython-312.pyc | Bin 0 -> 4310 bytes ...y_sections_to_page_content.cpython-312.pyc | Bin 0 -> 2960 bytes .../alembic/versions/add_about_page_fields.py | 36 + .../add_copyright_text_to_page_content.py | 26 + ...b256_add_luxury_content_fields_to_page_.py | 53 + ...dd_more_luxury_sections_to_page_content.py | 43 + Backend/seed_about_page.py | 217 ++ Backend/seed_banners_company.py | 214 ++ Backend/seed_homepage_footer.py | 456 ++++ Backend/seed_luxury_content.py | 123 + Backend/seed_rooms.py | 317 +++ Backend/src/__pycache__/main.cpython-312.pyc | Bin 14253 -> 15132 bytes Backend/src/main.py | 11 +- .../__pycache__/auth.cpython-312.pyc | Bin 2819 -> 3169 bytes Backend/src/middleware/auth.py | 19 +- .../__pycache__/page_content.cpython-312.pyc | Bin 2640 -> 5351 bytes Backend/src/models/page_content.py | 52 + .../__pycache__/about_routes.cpython-312.pyc | Bin 0 -> 4054 bytes .../__pycache__/auth_routes.cpython-312.pyc | Bin 18782 -> 18804 bytes .../__pycache__/banner_routes.cpython-312.pyc | Bin 13226 -> 13669 bytes .../booking_routes.cpython-312.pyc | Bin 71587 -> 73241 bytes .../contact_content_routes.cpython-312.pyc | Bin 0 -> 3438 bytes .../__pycache__/footer_routes.cpython-312.pyc | Bin 0 -> 3093 bytes .../__pycache__/home_routes.cpython-312.pyc | Bin 0 -> 7178 bytes .../invoice_routes.cpython-312.pyc | Bin 9895 -> 10391 bytes .../page_content_routes.cpython-312.pyc | Bin 16989 -> 35501 bytes .../payment_routes.cpython-312.pyc | Bin 47991 -> 47991 bytes .../__pycache__/room_routes.cpython-312.pyc | Bin 37575 -> 37795 bytes .../system_settings_routes.cpython-312.pyc | Bin 56646 -> 56673 bytes Backend/src/routes/about_routes.py | 75 + Backend/src/routes/auth_routes.py | 1 + Backend/src/routes/banner_routes.py | 26 +- Backend/src/routes/booking_routes.py | 24 +- Backend/src/routes/contact_content_routes.py | 68 + Backend/src/routes/footer_routes.py | 63 + Backend/src/routes/home_routes.py | 110 + Backend/src/routes/invoice_routes.py | 38 +- Backend/src/routes/page_content_routes.py | 280 +- Backend/src/routes/room_routes.py | 11 +- Backend/src/routes/system_settings_routes.py | 2 + .../__pycache__/auth_service.cpython-312.pyc | Bin 21623 -> 22233 bytes .../invoice_service.cpython-312.pyc | Bin 20008 -> 20664 bytes .../paypal_service.cpython-312.pyc | Bin 24052 -> 25193 bytes .../stripe_service.cpython-312.pyc | Bin 25620 -> 25665 bytes Backend/src/services/auth_service.py | 12 + Backend/src/services/invoice_service.py | 21 +- Backend/src/services/paypal_service.py | 26 + Backend/src/services/stripe_service.py | 1 + Frontend/src/App.tsx | 14 + Frontend/src/components/admin/IconPicker.tsx | 225 ++ Frontend/src/components/layout/Footer.tsx | 9 +- .../src/components/rooms/BannerCarousel.tsx | 36 +- Frontend/src/data/luxuryContentSeed.ts | 65 + Frontend/src/pages/AboutPage.tsx | 529 +++- Frontend/src/pages/ContactPage.tsx | 2 +- Frontend/src/pages/HomePage.tsx | 1033 +++++++- .../src/pages/admin/BookingManagementPage.tsx | 52 +- Frontend/src/pages/admin/CheckInPage.tsx | 334 ++- .../src/pages/admin/InvoiceManagementPage.tsx | 31 +- .../src/pages/admin/PageContentDashboard.tsx | 2301 ++++++++++++++++- Frontend/src/pages/customer/InvoicePage.tsx | 17 +- Frontend/src/services/api/apiClient.ts | 5 + Frontend/src/services/api/authService.ts | 8 +- Frontend/src/services/api/bannerService.ts | 28 +- Frontend/src/services/api/bookingService.ts | 11 +- Frontend/src/services/api/invoiceService.ts | 2 + .../src/services/api/pageContentService.ts | 194 +- Frontend/src/services/api/paymentService.ts | 8 +- .../src/services/api/systemSettingsService.ts | 16 +- 77 files changed, 7169 insertions(+), 360 deletions(-) create mode 100644 Backend/alembic/versions/1444eb61188e_add_section_title_fields_to_page_content.py create mode 100644 Backend/alembic/versions/17efc6439cc3_add_luxury_section_fields_to_page_.py create mode 100644 Backend/alembic/versions/__pycache__/1444eb61188e_add_section_title_fields_to_page_content.cpython-312.pyc create mode 100644 Backend/alembic/versions/__pycache__/17efc6439cc3_add_luxury_section_fields_to_page_.cpython-312.pyc create mode 100644 Backend/alembic/versions/__pycache__/add_about_page_fields.cpython-312.pyc create mode 100644 Backend/alembic/versions/__pycache__/add_copyright_text_to_page_content.cpython-312.pyc create mode 100644 Backend/alembic/versions/__pycache__/add_privacy_terms_page_types.cpython-312.pyc create mode 100644 Backend/alembic/versions/__pycache__/bfa74be4b256_add_luxury_content_fields_to_page_.cpython-312.pyc create mode 100644 Backend/alembic/versions/__pycache__/ff515d77abbe_add_more_luxury_sections_to_page_content.cpython-312.pyc create mode 100644 Backend/alembic/versions/add_about_page_fields.py create mode 100644 Backend/alembic/versions/add_copyright_text_to_page_content.py create mode 100644 Backend/alembic/versions/bfa74be4b256_add_luxury_content_fields_to_page_.py create mode 100644 Backend/alembic/versions/ff515d77abbe_add_more_luxury_sections_to_page_content.py create mode 100644 Backend/seed_about_page.py create mode 100644 Backend/seed_banners_company.py create mode 100644 Backend/seed_homepage_footer.py create mode 100644 Backend/seed_luxury_content.py create mode 100644 Backend/seed_rooms.py create mode 100644 Backend/src/routes/__pycache__/about_routes.cpython-312.pyc create mode 100644 Backend/src/routes/__pycache__/contact_content_routes.cpython-312.pyc create mode 100644 Backend/src/routes/__pycache__/footer_routes.cpython-312.pyc create mode 100644 Backend/src/routes/__pycache__/home_routes.cpython-312.pyc create mode 100644 Backend/src/routes/about_routes.py create mode 100644 Backend/src/routes/contact_content_routes.py create mode 100644 Backend/src/routes/footer_routes.py create mode 100644 Backend/src/routes/home_routes.py create mode 100644 Frontend/src/components/admin/IconPicker.tsx create mode 100644 Frontend/src/data/luxuryContentSeed.ts diff --git a/Backend/alembic/versions/1444eb61188e_add_section_title_fields_to_page_content.py b/Backend/alembic/versions/1444eb61188e_add_section_title_fields_to_page_content.py new file mode 100644 index 00000000..3f5845c1 --- /dev/null +++ b/Backend/alembic/versions/1444eb61188e_add_section_title_fields_to_page_content.py @@ -0,0 +1,246 @@ +"""add_section_title_fields_to_page_content + +Revision ID: 1444eb61188e +Revises: ff515d77abbe +Create Date: 2025-11-20 15:51:29.671843 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '1444eb61188e' +down_revision = 'ff515d77abbe' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('bookings_room_id', table_name='bookings') + op.drop_index('bookings_status', table_name='bookings') + op.drop_index('bookings_user_id', table_name='bookings') + op.drop_index('ix_bookings_promotion_code', table_name='bookings') + op.create_index(op.f('ix_bookings_booking_number'), 'bookings', ['booking_number'], unique=True) + op.create_index(op.f('ix_bookings_id'), 'bookings', ['id'], unique=False) + op.drop_constraint('bookings_ibfk_1', 'bookings', type_='foreignkey') + op.drop_constraint('bookings_ibfk_2', 'bookings', type_='foreignkey') + op.create_foreign_key(None, 'bookings', 'rooms', ['room_id'], ['id']) + op.create_foreign_key(None, 'bookings', 'users', ['user_id'], ['id']) + op.drop_index('checkin_checkout_booking_id', table_name='checkin_checkout') + op.create_index(op.f('ix_checkin_checkout_id'), 'checkin_checkout', ['id'], unique=False) + op.create_unique_constraint(None, 'checkin_checkout', ['booking_id']) + op.drop_constraint('checkin_checkout_ibfk_1', 'checkin_checkout', type_='foreignkey') + op.drop_constraint('checkin_checkout_ibfk_2', 'checkin_checkout', type_='foreignkey') + op.drop_constraint('checkin_checkout_ibfk_3', 'checkin_checkout', type_='foreignkey') + op.create_foreign_key(None, 'checkin_checkout', 'users', ['checkin_by'], ['id']) + op.create_foreign_key(None, 'checkin_checkout', 'bookings', ['booking_id'], ['id']) + op.create_foreign_key(None, 'checkin_checkout', 'users', ['checkout_by'], ['id']) + op.drop_index('favorites_room_id', table_name='favorites') + op.drop_index('favorites_user_id', table_name='favorites') + op.drop_index('unique_user_room_favorite', table_name='favorites') + op.create_index(op.f('ix_favorites_id'), 'favorites', ['id'], unique=False) + op.drop_constraint('favorites_ibfk_1', 'favorites', type_='foreignkey') + op.drop_constraint('favorites_ibfk_2', 'favorites', type_='foreignkey') + op.create_foreign_key(None, 'favorites', 'rooms', ['room_id'], ['id']) + op.create_foreign_key(None, 'favorites', 'users', ['user_id'], ['id']) + op.add_column('page_contents', sa.Column('luxury_gallery_section_title', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('luxury_gallery_section_subtitle', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('luxury_testimonials_section_title', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('luxury_testimonials_section_subtitle', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('luxury_services_section_title', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('luxury_services_section_subtitle', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('luxury_experiences_section_title', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('luxury_experiences_section_subtitle', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('awards_section_title', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('awards_section_subtitle', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('partners_section_title', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('partners_section_subtitle', sa.Text(), nullable=True)) + op.alter_column('password_reset_tokens', 'used', + existing_type=mysql.TINYINT(display_width=1), + nullable=False, + existing_server_default=sa.text("'0'")) + op.drop_index('password_reset_tokens_token', table_name='password_reset_tokens') + op.drop_index('password_reset_tokens_user_id', table_name='password_reset_tokens') + op.drop_index('token', table_name='password_reset_tokens') + op.create_index(op.f('ix_password_reset_tokens_id'), 'password_reset_tokens', ['id'], unique=False) + op.create_index(op.f('ix_password_reset_tokens_token'), 'password_reset_tokens', ['token'], unique=True) + op.drop_constraint('password_reset_tokens_ibfk_1', 'password_reset_tokens', type_='foreignkey') + op.create_foreign_key(None, 'password_reset_tokens', 'users', ['user_id'], ['id']) + op.alter_column('payments', 'deposit_percentage', + existing_type=mysql.INTEGER(), + comment=None, + existing_comment='Percentage of deposit (e.g., 20, 30, 50)', + existing_nullable=True) + op.drop_index('payments_booking_id', table_name='payments') + op.drop_index('payments_payment_status', table_name='payments') + op.create_index(op.f('ix_payments_id'), 'payments', ['id'], unique=False) + op.drop_constraint('payments_ibfk_1', 'payments', type_='foreignkey') + op.drop_constraint('payments_related_payment_id_foreign_idx', 'payments', type_='foreignkey') + op.create_foreign_key(None, 'payments', 'payments', ['related_payment_id'], ['id']) + op.create_foreign_key(None, 'payments', 'bookings', ['booking_id'], ['id']) + op.drop_index('code', table_name='promotions') + op.drop_index('promotions_code', table_name='promotions') + op.drop_index('promotions_is_active', table_name='promotions') + op.create_index(op.f('ix_promotions_code'), 'promotions', ['code'], unique=True) + op.create_index(op.f('ix_promotions_id'), 'promotions', ['id'], unique=False) + op.drop_index('refresh_tokens_token', table_name='refresh_tokens') + op.drop_index('refresh_tokens_user_id', table_name='refresh_tokens') + op.drop_index('token', table_name='refresh_tokens') + op.create_index(op.f('ix_refresh_tokens_id'), 'refresh_tokens', ['id'], unique=False) + op.create_index(op.f('ix_refresh_tokens_token'), 'refresh_tokens', ['token'], unique=True) + op.drop_constraint('refresh_tokens_ibfk_1', 'refresh_tokens', type_='foreignkey') + op.create_foreign_key(None, 'refresh_tokens', 'users', ['user_id'], ['id']) + op.drop_index('reviews_room_id', table_name='reviews') + op.drop_index('reviews_status', table_name='reviews') + op.drop_index('reviews_user_id', table_name='reviews') + op.create_index(op.f('ix_reviews_id'), 'reviews', ['id'], unique=False) + op.drop_constraint('reviews_ibfk_2', 'reviews', type_='foreignkey') + op.drop_constraint('reviews_ibfk_1', 'reviews', type_='foreignkey') + op.create_foreign_key(None, 'reviews', 'users', ['user_id'], ['id']) + op.create_foreign_key(None, 'reviews', 'rooms', ['room_id'], ['id']) + op.drop_index('name', table_name='roles') + op.create_index(op.f('ix_roles_id'), 'roles', ['id'], unique=False) + op.create_index(op.f('ix_roles_name'), 'roles', ['name'], unique=True) + op.create_index(op.f('ix_room_types_id'), 'room_types', ['id'], unique=False) + op.drop_index('room_number', table_name='rooms') + op.drop_index('rooms_featured', table_name='rooms') + op.drop_index('rooms_room_type_id', table_name='rooms') + op.drop_index('rooms_status', table_name='rooms') + op.create_index(op.f('ix_rooms_id'), 'rooms', ['id'], unique=False) + op.create_index(op.f('ix_rooms_room_number'), 'rooms', ['room_number'], unique=True) + op.drop_constraint('rooms_ibfk_1', 'rooms', type_='foreignkey') + op.create_foreign_key(None, 'rooms', 'room_types', ['room_type_id'], ['id']) + op.drop_index('service_usages_booking_id', table_name='service_usages') + op.drop_index('service_usages_service_id', table_name='service_usages') + op.create_index(op.f('ix_service_usages_id'), 'service_usages', ['id'], unique=False) + op.drop_constraint('service_usages_ibfk_2', 'service_usages', type_='foreignkey') + op.drop_constraint('service_usages_ibfk_1', 'service_usages', type_='foreignkey') + op.create_foreign_key(None, 'service_usages', 'bookings', ['booking_id'], ['id']) + op.create_foreign_key(None, 'service_usages', 'services', ['service_id'], ['id']) + op.drop_index('services_category', table_name='services') + op.create_index(op.f('ix_services_id'), 'services', ['id'], unique=False) + op.drop_index('email', table_name='users') + op.drop_index('users_email', table_name='users') + op.drop_index('users_role_id', table_name='users') + op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) + op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False) + op.drop_constraint('users_ibfk_1', 'users', type_='foreignkey') + op.create_foreign_key(None, 'users', 'roles', ['role_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'users', type_='foreignkey') + op.create_foreign_key('users_ibfk_1', 'users', 'roles', ['role_id'], ['id'], onupdate='CASCADE', ondelete='RESTRICT') + op.drop_index(op.f('ix_users_id'), table_name='users') + op.drop_index(op.f('ix_users_email'), table_name='users') + op.create_index('users_role_id', 'users', ['role_id'], unique=False) + op.create_index('users_email', 'users', ['email'], unique=False) + op.create_index('email', 'users', ['email'], unique=False) + op.drop_index(op.f('ix_services_id'), table_name='services') + op.create_index('services_category', 'services', ['category'], unique=False) + op.drop_constraint(None, 'service_usages', type_='foreignkey') + op.drop_constraint(None, 'service_usages', type_='foreignkey') + op.create_foreign_key('service_usages_ibfk_1', 'service_usages', 'bookings', ['booking_id'], ['id'], onupdate='CASCADE', ondelete='RESTRICT') + op.create_foreign_key('service_usages_ibfk_2', 'service_usages', 'services', ['service_id'], ['id'], onupdate='CASCADE', ondelete='RESTRICT') + op.drop_index(op.f('ix_service_usages_id'), table_name='service_usages') + op.create_index('service_usages_service_id', 'service_usages', ['service_id'], unique=False) + op.create_index('service_usages_booking_id', 'service_usages', ['booking_id'], unique=False) + op.drop_constraint(None, 'rooms', type_='foreignkey') + op.create_foreign_key('rooms_ibfk_1', 'rooms', 'room_types', ['room_type_id'], ['id'], onupdate='CASCADE', ondelete='RESTRICT') + op.drop_index(op.f('ix_rooms_room_number'), table_name='rooms') + op.drop_index(op.f('ix_rooms_id'), table_name='rooms') + op.create_index('rooms_status', 'rooms', ['status'], unique=False) + op.create_index('rooms_room_type_id', 'rooms', ['room_type_id'], unique=False) + op.create_index('rooms_featured', 'rooms', ['featured'], unique=False) + op.create_index('room_number', 'rooms', ['room_number'], unique=False) + op.drop_index(op.f('ix_room_types_id'), table_name='room_types') + op.drop_index(op.f('ix_roles_name'), table_name='roles') + op.drop_index(op.f('ix_roles_id'), table_name='roles') + op.create_index('name', 'roles', ['name'], unique=False) + op.drop_constraint(None, 'reviews', type_='foreignkey') + op.drop_constraint(None, 'reviews', type_='foreignkey') + op.create_foreign_key('reviews_ibfk_1', 'reviews', 'users', ['user_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + op.create_foreign_key('reviews_ibfk_2', 'reviews', 'rooms', ['room_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + op.drop_index(op.f('ix_reviews_id'), table_name='reviews') + op.create_index('reviews_user_id', 'reviews', ['user_id'], unique=False) + op.create_index('reviews_status', 'reviews', ['status'], unique=False) + op.create_index('reviews_room_id', 'reviews', ['room_id'], unique=False) + op.drop_constraint(None, 'refresh_tokens', type_='foreignkey') + op.create_foreign_key('refresh_tokens_ibfk_1', 'refresh_tokens', 'users', ['user_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + op.drop_index(op.f('ix_refresh_tokens_token'), table_name='refresh_tokens') + op.drop_index(op.f('ix_refresh_tokens_id'), table_name='refresh_tokens') + op.create_index('token', 'refresh_tokens', ['token'], unique=False) + op.create_index('refresh_tokens_user_id', 'refresh_tokens', ['user_id'], unique=False) + op.create_index('refresh_tokens_token', 'refresh_tokens', ['token'], unique=False) + op.drop_index(op.f('ix_promotions_id'), table_name='promotions') + op.drop_index(op.f('ix_promotions_code'), table_name='promotions') + op.create_index('promotions_is_active', 'promotions', ['is_active'], unique=False) + op.create_index('promotions_code', 'promotions', ['code'], unique=False) + op.create_index('code', 'promotions', ['code'], unique=False) + op.drop_constraint(None, 'payments', type_='foreignkey') + op.drop_constraint(None, 'payments', type_='foreignkey') + op.create_foreign_key('payments_related_payment_id_foreign_idx', 'payments', 'payments', ['related_payment_id'], ['id'], onupdate='CASCADE', ondelete='SET NULL') + op.create_foreign_key('payments_ibfk_1', 'payments', 'bookings', ['booking_id'], ['id'], onupdate='CASCADE', ondelete='RESTRICT') + op.drop_index(op.f('ix_payments_id'), table_name='payments') + op.create_index('payments_payment_status', 'payments', ['payment_status'], unique=False) + op.create_index('payments_booking_id', 'payments', ['booking_id'], unique=False) + op.alter_column('payments', 'deposit_percentage', + existing_type=mysql.INTEGER(), + comment='Percentage of deposit (e.g., 20, 30, 50)', + existing_nullable=True) + op.drop_constraint(None, 'password_reset_tokens', type_='foreignkey') + op.create_foreign_key('password_reset_tokens_ibfk_1', 'password_reset_tokens', 'users', ['user_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + op.drop_index(op.f('ix_password_reset_tokens_token'), table_name='password_reset_tokens') + op.drop_index(op.f('ix_password_reset_tokens_id'), table_name='password_reset_tokens') + op.create_index('token', 'password_reset_tokens', ['token'], unique=False) + op.create_index('password_reset_tokens_user_id', 'password_reset_tokens', ['user_id'], unique=False) + op.create_index('password_reset_tokens_token', 'password_reset_tokens', ['token'], unique=False) + op.alter_column('password_reset_tokens', 'used', + existing_type=mysql.TINYINT(display_width=1), + nullable=True, + existing_server_default=sa.text("'0'")) + op.drop_column('page_contents', 'partners_section_subtitle') + op.drop_column('page_contents', 'partners_section_title') + op.drop_column('page_contents', 'awards_section_subtitle') + op.drop_column('page_contents', 'awards_section_title') + op.drop_column('page_contents', 'luxury_experiences_section_subtitle') + op.drop_column('page_contents', 'luxury_experiences_section_title') + op.drop_column('page_contents', 'luxury_services_section_subtitle') + op.drop_column('page_contents', 'luxury_services_section_title') + op.drop_column('page_contents', 'luxury_testimonials_section_subtitle') + op.drop_column('page_contents', 'luxury_testimonials_section_title') + op.drop_column('page_contents', 'luxury_gallery_section_subtitle') + op.drop_column('page_contents', 'luxury_gallery_section_title') + op.drop_constraint(None, 'favorites', type_='foreignkey') + op.drop_constraint(None, 'favorites', type_='foreignkey') + op.create_foreign_key('favorites_ibfk_2', 'favorites', 'rooms', ['room_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + op.create_foreign_key('favorites_ibfk_1', 'favorites', 'users', ['user_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') + op.drop_index(op.f('ix_favorites_id'), table_name='favorites') + op.create_index('unique_user_room_favorite', 'favorites', ['user_id', 'room_id'], unique=False) + op.create_index('favorites_user_id', 'favorites', ['user_id'], unique=False) + op.create_index('favorites_room_id', 'favorites', ['room_id'], unique=False) + op.drop_constraint(None, 'checkin_checkout', type_='foreignkey') + op.drop_constraint(None, 'checkin_checkout', type_='foreignkey') + op.drop_constraint(None, 'checkin_checkout', type_='foreignkey') + op.create_foreign_key('checkin_checkout_ibfk_3', 'checkin_checkout', 'users', ['checkout_by'], ['id'], onupdate='CASCADE', ondelete='SET NULL') + op.create_foreign_key('checkin_checkout_ibfk_2', 'checkin_checkout', 'users', ['checkin_by'], ['id'], onupdate='CASCADE', ondelete='SET NULL') + op.create_foreign_key('checkin_checkout_ibfk_1', 'checkin_checkout', 'bookings', ['booking_id'], ['id'], onupdate='CASCADE', ondelete='RESTRICT') + op.drop_constraint(None, 'checkin_checkout', type_='unique') + op.drop_index(op.f('ix_checkin_checkout_id'), table_name='checkin_checkout') + op.create_index('checkin_checkout_booking_id', 'checkin_checkout', ['booking_id'], unique=False) + op.drop_constraint(None, 'bookings', type_='foreignkey') + op.drop_constraint(None, 'bookings', type_='foreignkey') + op.create_foreign_key('bookings_ibfk_2', 'bookings', 'rooms', ['room_id'], ['id'], onupdate='CASCADE', ondelete='RESTRICT') + op.create_foreign_key('bookings_ibfk_1', 'bookings', 'users', ['user_id'], ['id'], onupdate='CASCADE', ondelete='RESTRICT') + op.drop_index(op.f('ix_bookings_id'), table_name='bookings') + op.drop_index(op.f('ix_bookings_booking_number'), table_name='bookings') + op.create_index('ix_bookings_promotion_code', 'bookings', ['promotion_code'], unique=False) + op.create_index('bookings_user_id', 'bookings', ['user_id'], unique=False) + op.create_index('bookings_status', 'bookings', ['status'], unique=False) + op.create_index('bookings_room_id', 'bookings', ['room_id'], unique=False) + # ### end Alembic commands ### + diff --git a/Backend/alembic/versions/17efc6439cc3_add_luxury_section_fields_to_page_.py b/Backend/alembic/versions/17efc6439cc3_add_luxury_section_fields_to_page_.py new file mode 100644 index 00000000..6c47ef81 --- /dev/null +++ b/Backend/alembic/versions/17efc6439cc3_add_luxury_section_fields_to_page_.py @@ -0,0 +1,38 @@ +"""add_luxury_section_fields_to_page_content + +Revision ID: 17efc6439cc3 +Revises: bfa74be4b256 +Create Date: 2025-11-20 13:37:20.015422 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '17efc6439cc3' +down_revision = 'bfa74be4b256' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Add luxury section fields to page_contents table + # Use TEXT instead of VARCHAR to avoid MySQL row size limits + op.add_column('page_contents', sa.Column('luxury_section_title', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('luxury_section_subtitle', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('luxury_section_image', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('luxury_features', sa.Text(), nullable=True)) # JSON array + op.add_column('page_contents', sa.Column('luxury_gallery', sa.Text(), nullable=True)) # JSON array of image URLs + op.add_column('page_contents', sa.Column('luxury_testimonials', sa.Text(), nullable=True)) # JSON array of testimonials + + +def downgrade() -> None: + # Remove luxury section fields + op.drop_column('page_contents', 'luxury_testimonials') + op.drop_column('page_contents', 'luxury_gallery') + op.drop_column('page_contents', 'luxury_features') + op.drop_column('page_contents', 'luxury_section_image') + op.drop_column('page_contents', 'luxury_section_subtitle') + op.drop_column('page_contents', 'luxury_section_title') + diff --git a/Backend/alembic/versions/__pycache__/1444eb61188e_add_section_title_fields_to_page_content.cpython-312.pyc b/Backend/alembic/versions/__pycache__/1444eb61188e_add_section_title_fields_to_page_content.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..de4db8954d890a121a892edabd81c0d4d574a04f GIT binary patch literal 22089 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 CywDH; literal 0 HcmV?d00001 diff --git a/Backend/alembic/versions/__pycache__/add_about_page_fields.cpython-312.pyc b/Backend/alembic/versions/__pycache__/add_about_page_fields.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc32c87c22815ae28a6890c0da9ff6cb0062e7c3 GIT binary patch literal 2180 zcmc&#O=ufO6y9Bpc2{dDxQ-LyQd&`!fGG0H@?T;?1Bod~A!@BUQM`dHtDUj6@%|{Y zD?7@`fkLmj<=R4_E$$^G5R46`v=BosK5QUNdTTF*91@&dl1pZ^Qsjt0Vsh}H`QCe) z$Gmy7?|XAUl~Oo5j^sYGUILE$ml>TQw3VIXRJqAPPU9dC1)FySP2iaxvtt^7K#Rk; zCc>B|(J4b2ia!7?L8U~c(v0O2*Xjm@x>5BSfnGNjkUnoA8~Tb;MoX4&d2Z&+^i*cP zXpC0J%yBq@CZiSPPi2geYSA2pV`zMSLOF$z5unU8g{e%jP#n*XjO2@j%*fkC5E~k`8szprG za|7fCKIxCxT0_{=Er)V;ma<898@6p!ZA2u;@)>c77!jC&02vOE1It0Sbp51UJq~2D8BmECNo@b5~)#Nzy?J4Fbyz3r+=Tq3ilrn ze>{8r><#so;;+YkKDM1d@%O;J^q=X!hPKbmg;^~G7yo(F<+r&FVN+U@))QOPVRj}2 zvP*P=fdpy2cUunX+CfCGTP0fUY4X??A^T#3h&>0U~CV*U=P0W zJ@|@y@Rhp0wASSkmgwc+0}N6Ok_`G7u-6c?W}OqFitU5gtG7!QA7rcSrtu*PBmuo; zH>#^4YJWnfU!f4yRq1-^#)+Tk_Z?-QFS5P~f!ejc4#AgQ>vRb6UF%E;hF@FTUramz zldZ@(fruCd4eXY3eMHiA=$X1sq&64YH1n_6hHci6)5QJkq%v;5vn0jNtar^SHe9o& zQ&J<_CkjM$$}C^^+!j?Z%d02t7xHn(gAE&fg+HgO?2YtCDLjesJpWV>dGP!I$EP20 l=_ewW{CGuv*#GHD>S6yNh15qY%8n@U7x-s`5`VOn(JL zf{=pXwYQ!HL8SfzqTnCkr3VXTJnF?;C3^AXn@vJLE`99#nzu7=-tRYG(`j7-VN+9$ zmMlp>L^2S_W9cq}+n2CpNf=?-LrvL|k%$#fu~e*D37oJrtXN6V6i#97m1+%vPlDIm zN_ObQO9x}yr9qoDYW2vD$PO542TqN^vL6vY()EXAs}VM+pLwuS&NyZ@@6O@^DXy1# z6%v*+>+40ci09^rA_Oj$jR7!&1@IU}i9x!d4Wd(z5d=5pW!jiC$2?nxq52InsXaJ8;0K-N_ zRWxN|VE7QG+kWrz6Od1WhGL4|NCQmeJ literal 0 HcmV?d00001 diff --git a/Backend/alembic/versions/__pycache__/add_privacy_terms_page_types.cpython-312.pyc b/Backend/alembic/versions/__pycache__/add_privacy_terms_page_types.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e861ab395e4863f401a2b472fe3add939655505 GIT binary patch literal 1200 zcmcgrUr*Ce6u)h^b}O5LXh@8azL>d0)-jz+7GnYo17wVZK%#wVy574n=s)l6kP#nd zH1Q+w8PJd77a%o0>5Ffe!3$5G+YK<_qw%EY?>+75`JH>dWV0#)`c#?jY$p)<&Yg1- zN5)_Yj01#`fv|u@M{q?$6u2!pl92$NaFVWU$Ph~!3YLu&mW(v8iqly6kTAwTr$A?V zQeNGEYhi2#w6km3Ju@WKWoBTti5d0+!c=vg>~>hk_q63&RlAltJSMEFwOZxbGOkpt zW|OE3lvp9rY9OjwsaPt{6lZ5jMQyfJEf(P^6k}4=pS-EO5Q%=!0F`um?6nh3T&ojh zS|#go^NC%?Pf5idK@lqnB9I^8NXe@jloeQXk@?vBEK-ESxSKnA% z*EZ(n>x){9fbDxB@j|9uq^z#hmS4Qm7S`&UtBqk2he=y(Y_2}cZTT+AJ<@XgJ-H;rj0q^7NrOwOf zZEsJn5w;WhfxZNvc4p>%f2ZTM^?A$QAs*H(hq%p-t?v@b=g0JGp#CxELePtpZqTL{ zCNv9mCV|-<5Pftaqone2<=x6LlEzb~NJ^&Z7@YjeDZIac_&-kJ3gR>cO;NspaeFia zR+Ps68*d29P4MplGmcFk{Yc1v0VOX+G9_V`dX0QGQcM&3wrNJnXd1(rs&L6I$KE1t zFA|v*O5XX`r2){&GNSxk4!qitZ^$k5ZmQiN9pE?@9-_IV6-v*JL R=&J)+7SboTlEUx?{06$JGBf}H literal 0 HcmV?d00001 diff --git a/Backend/alembic/versions/__pycache__/bd309b0742c1_add_promotion_fields_to_bookings.cpython-312.pyc b/Backend/alembic/versions/__pycache__/bd309b0742c1_add_promotion_fields_to_bookings.cpython-312.pyc index 8d28a5a73402679979215ffdac843cf179c2517a..6981abb185ef91aead659155d76f8e54e8f667e1 100644 GIT binary patch delta 19 ZcmbQrKb4>BG%qg~0}$B6ZRF}^2LLOj1StRj delta 19 ZcmbQrKb4>BG%qg~0}z~#-N@C=4gfC_1iJtL diff --git a/Backend/alembic/versions/__pycache__/bfa74be4b256_add_luxury_content_fields_to_page_.cpython-312.pyc b/Backend/alembic/versions/__pycache__/bfa74be4b256_add_luxury_content_fields_to_page_.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c11c964e9a1e9e319279c724bfe1652849c383bc GIT binary patch literal 4310 zcmcgvT}T{P6yDjr?w_ujXntuD$ymbDnq5~-&>C%xZ7OXxiJF>hTcChiX;%kKq3lG;ReEO=}Y_2zNMH#Nnd*J{$-p^F-y@4_sl)_ zd~?1zhr9QjU5P}(9yPrC# zQgyH{<%hmhfX*NcLjQG;3ejhPKErumG<5a42%(_nuH=loAnTfiG)uUoAQhT|r3+ax zjmq2M@F=>hn2N5ojSTm-NteXq-4g1S5Wuaa%cFYfkX?n4O7wA zmWU^MIyyT$67jaquD-DcISPkgZo8!-`dDq9PBkmp7%sslO*D-#U|M!W{{>>3Ni%~ z$ri23f~8n0Ql8Q}HBM`3P|K;RD5;3-uZEgAsfhlaRx%Q*0LXp|nU<2#HAPfip2>k1 z+RCm-gVRiQrA1Xmw5izeb!4x5yYhNt*`j1<(_xYpQPOjkkTq!2fo6-rk=+|ND=+Wb zxL;~y+&&7PNu6nlmKpUMtca5^>wjcj&1E#=HAUh-SzPGn5d$Rq{y+$$Y2eV8FThD!sad4 z(QKXsa@n*YLS!7G)Gc(H9@=iqd42=_&re@FeIxc&;_Kcod+m;QR=sPX<&Zskex=bC z5#}#pkp9_?8d*)O^)C0?=P#}d+DO6tGzK#+)U6Ni9=>;E;mDonLx}k?48~mmyQAXf z!peXRr!cQzFkOXue^pvjmKFQLM=QELrD8sVf#yQpn!G!DZ(?EM&PDs&dCY%+!GsH8 zcMRW`9w>{7eRga~vjrLR5Cc?&I<>lUZQt@fdu(FmkS(V%KZSu(g*tK9xHr2ndnZqE zn7@F*#VW|q{h9hY%H~aGRVtPrjQvQcV(uC`(I~Dd<|E!Wb;0u7p1e_GfgMQ_+C&8nv>2z-AW}PkbE5{ps~{;IBpx qx92C%o+m+1!~VJ87fK6y9Brch_s11o~q@Rcm1=^1*EE#BpsQszMHw3abRS0n>K7-kBsz*1KkQ zoy44iiqvavy{1y7Ql*!wNL7?b5dw)wz4)+5Xo`AiFTGKvy>RMGY$wK2l~^1+(tI;- z-?#H-W_P|fzeb}9N6YVTykovAaoiuw=oF#d**;4<-*AvCbC8FE#aD&0z_Wh93X}l^ z$kj@pdBWLotTz)*QD1Wi_BzYn@wIYdhk4z%nBO2{>?=&*S0Il0! z0=5Yr-*MjTY)|I_Z6WUkJLXNX)7xvI(B)?%_mtJP4Ff-dRTh<&)pi{IRbvMS7+V}* zY-xb8;{*N7D=>Q4P7#{tCx7*G>}`O0w&va%hD?rB1hU@ zv{*yfL^dT5QD4w8bVq8N{g_rTd%aNA!b$Tp6A$_hZa53{lGHqxz?*@mC7=a zYdZF9gxzEiGesf_Q*y(x>Q$Qvu1>@Ytv)Exp?=`DnP$DXrOrB4q|Vrj>I8Daeo5w>&tg8FRR~WzR!P~Z>G*Y5*|wrq=%tq>6)Lo?t}8PUdjiLGLLf)au4&( zsT+RcGar23L%DbDKK^Clr-h#zTVsCWV;@}UCEVCJv3Yv!^m=j&`iZMPDD@J)*qGd$ zTANzG-n{aOpZL@VQ$36W&2-2dxN;xdP#;`nk4r@(T*UP1#^J*Zh8YYoIK&{z zAi^NbfL-=5yLcpp)@cqy?9|%F8NbQA?BnC36i5g<3wG;Fy+!Rft?pe4tus};Q(S(3 z<&qCn_TRRwk9;t)Yc)@1eK7ui=1S2Aqr1+Pm)=~t=!4X*uX*x4ALP5t>=Q7)GtHzx zM2x&Twu{Lyku(iDhNcmzEh=rja&K9>Wz3>#12b75GHzcmNt7*6+cPUzw~bkiN-MHl zqCixm66I=+y`w3ZZ9>l;y^zmV9ay)}IedzG+2zn3rSLq!^Zeg}$b)|maeVAIF7{mJ iBFC5Hr^9bAMV}78P9b`DN!b<^zQq4?T;X#&EBX(a`6HG9 literal 0 HcmV?d00001 diff --git a/Backend/alembic/versions/add_about_page_fields.py b/Backend/alembic/versions/add_about_page_fields.py new file mode 100644 index 00000000..fba405ad --- /dev/null +++ b/Backend/alembic/versions/add_about_page_fields.py @@ -0,0 +1,36 @@ +"""add_about_page_fields + +Revision ID: f2a3b4c5d6e7 +Revises: a1b2c3d4e5f6 +Create Date: 2025-11-20 17:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'f2a3b4c5d6e7' +down_revision = 'a1b2c3d4e5f6' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Add about page specific fields (all as TEXT to avoid row size issues) + op.add_column('page_contents', sa.Column('about_hero_image', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('mission', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('vision', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('team', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('timeline', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('achievements', sa.Text(), nullable=True)) + + +def downgrade() -> None: + # Remove about page specific fields + op.drop_column('page_contents', 'achievements') + op.drop_column('page_contents', 'timeline') + op.drop_column('page_contents', 'team') + op.drop_column('page_contents', 'vision') + op.drop_column('page_contents', 'mission') + op.drop_column('page_contents', 'about_hero_image') + diff --git a/Backend/alembic/versions/add_copyright_text_to_page_content.py b/Backend/alembic/versions/add_copyright_text_to_page_content.py new file mode 100644 index 00000000..24de9b6a --- /dev/null +++ b/Backend/alembic/versions/add_copyright_text_to_page_content.py @@ -0,0 +1,26 @@ +"""add_copyright_text_to_page_content + +Revision ID: a1b2c3d4e5f6 +Revises: ff515d77abbe +Create Date: 2025-11-20 16:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'a1b2c3d4e5f6' +down_revision = '1444eb61188e' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Add copyright_text column to page_contents table + op.add_column('page_contents', sa.Column('copyright_text', sa.Text(), nullable=True)) + + +def downgrade() -> None: + # Remove copyright_text column from page_contents table + op.drop_column('page_contents', 'copyright_text') + diff --git a/Backend/alembic/versions/bfa74be4b256_add_luxury_content_fields_to_page_.py b/Backend/alembic/versions/bfa74be4b256_add_luxury_content_fields_to_page_.py new file mode 100644 index 00000000..66496e19 --- /dev/null +++ b/Backend/alembic/versions/bfa74be4b256_add_luxury_content_fields_to_page_.py @@ -0,0 +1,53 @@ +"""add_luxury_content_fields_to_page_content + +Revision ID: bfa74be4b256 +Revises: bd309b0742c1 +Create Date: 2025-11-20 13:27:52.106013 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'bfa74be4b256' +down_revision = 'bd309b0742c1' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Add luxury content fields to page_contents table + op.add_column('page_contents', sa.Column('amenities_section_title', sa.String(500), nullable=True)) + op.add_column('page_contents', sa.Column('amenities_section_subtitle', sa.String(1000), nullable=True)) + op.add_column('page_contents', sa.Column('amenities', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('testimonials_section_title', sa.String(500), nullable=True)) + op.add_column('page_contents', sa.Column('testimonials_section_subtitle', sa.String(1000), nullable=True)) + op.add_column('page_contents', sa.Column('testimonials', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('gallery_section_title', sa.String(500), nullable=True)) + op.add_column('page_contents', sa.Column('gallery_section_subtitle', sa.String(1000), nullable=True)) + op.add_column('page_contents', sa.Column('gallery_images', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('about_preview_title', sa.String(500), nullable=True)) + op.add_column('page_contents', sa.Column('about_preview_subtitle', sa.String(1000), nullable=True)) + op.add_column('page_contents', sa.Column('about_preview_content', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('about_preview_image', sa.String(1000), nullable=True)) + op.add_column('page_contents', sa.Column('stats', sa.Text(), nullable=True)) + + +def downgrade() -> None: + # Remove luxury content fields + op.drop_column('page_contents', 'stats') + op.drop_column('page_contents', 'about_preview_image') + op.drop_column('page_contents', 'about_preview_content') + op.drop_column('page_contents', 'about_preview_subtitle') + op.drop_column('page_contents', 'about_preview_title') + op.drop_column('page_contents', 'gallery_images') + op.drop_column('page_contents', 'gallery_section_subtitle') + op.drop_column('page_contents', 'gallery_section_title') + op.drop_column('page_contents', 'testimonials') + op.drop_column('page_contents', 'testimonials_section_subtitle') + op.drop_column('page_contents', 'testimonials_section_title') + op.drop_column('page_contents', 'amenities') + op.drop_column('page_contents', 'amenities_section_subtitle') + op.drop_column('page_contents', 'amenities_section_title') + diff --git a/Backend/alembic/versions/ff515d77abbe_add_more_luxury_sections_to_page_content.py b/Backend/alembic/versions/ff515d77abbe_add_more_luxury_sections_to_page_content.py new file mode 100644 index 00000000..743e69d1 --- /dev/null +++ b/Backend/alembic/versions/ff515d77abbe_add_more_luxury_sections_to_page_content.py @@ -0,0 +1,43 @@ +"""add_more_luxury_sections_to_page_content + +Revision ID: ff515d77abbe +Revises: 17efc6439cc3 +Create Date: 2025-11-20 15:17:50.977961 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'ff515d77abbe' +down_revision = '17efc6439cc3' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Add more luxury sections to page_contents table + op.add_column('page_contents', sa.Column('luxury_services', sa.Text(), nullable=True)) # JSON array of services + op.add_column('page_contents', sa.Column('luxury_experiences', sa.Text(), nullable=True)) # JSON array of experiences + op.add_column('page_contents', sa.Column('awards', sa.Text(), nullable=True)) # JSON array of awards + op.add_column('page_contents', sa.Column('cta_title', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('cta_subtitle', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('cta_button_text', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('cta_button_link', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('cta_image', sa.Text(), nullable=True)) + op.add_column('page_contents', sa.Column('partners', sa.Text(), nullable=True)) # JSON array of partners + + +def downgrade() -> None: + # Remove luxury sections fields + op.drop_column('page_contents', 'partners') + op.drop_column('page_contents', 'cta_image') + op.drop_column('page_contents', 'cta_button_link') + op.drop_column('page_contents', 'cta_button_text') + op.drop_column('page_contents', 'cta_subtitle') + op.drop_column('page_contents', 'cta_title') + op.drop_column('page_contents', 'awards') + op.drop_column('page_contents', 'luxury_experiences') + op.drop_column('page_contents', 'luxury_services') + diff --git a/Backend/seed_about_page.py b/Backend/seed_about_page.py new file mode 100644 index 00000000..02fbdd0f --- /dev/null +++ b/Backend/seed_about_page.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +""" +Seed sample data for the About page +""" + +import sys +import os +from pathlib import Path +import json + +# Add the parent directory to the path so we can import from src +sys.path.insert(0, str(Path(__file__).parent)) + +from sqlalchemy.orm import Session +from src.config.database import SessionLocal +from src.models.page_content import PageContent, PageType +from datetime import datetime + +def get_db(): + """Get database session""" + db = SessionLocal() + try: + return db + finally: + pass + +def seed_about_page(db: Session): + """Seed about page content""" + print("=" * 80) + print("SEEDING ABOUT PAGE CONTENT") + print("=" * 80) + + # Sample data + about_data = { + "title": "About Luxury Hotel", + "subtitle": "Where Excellence Meets Unforgettable Experiences", + "description": "Discover the story behind our commitment to luxury hospitality and exceptional service.", + "story_content": """Welcome to Luxury Hotel, where timeless elegance meets modern sophistication. Since our founding in 2010, we have been dedicated to providing exceptional hospitality and creating unforgettable memories for our guests. + +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. + +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. + +Over the years, we have hosted thousands of guests from around the world, each leaving with cherished memories and a desire to return. Our team of dedicated professionals works tirelessly to ensure that every moment of your stay is perfect.""", + "mission": "To provide unparalleled luxury hospitality experiences that exceed expectations, creating lasting memories for our guests through exceptional service, attention to detail, and genuine care.", + "vision": "To be recognized as the world's premier luxury hotel brand, setting the standard for excellence in hospitality while maintaining our commitment to sustainability and community engagement.", + "about_hero_image": "https://images.unsplash.com/photo-1566073771259-6a8506099945?w=1920&h=1080&fit=crop", + "values": json.dumps([ + { + "icon": "Heart", + "title": "Passion", + "description": "We are passionate about hospitality and dedicated to creating exceptional experiences for every guest." + }, + { + "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", + "title": "Integrity", + "description": "We conduct our business with honesty, transparency, and respect for our guests and community." + }, + { + "icon": "Users", + "title": "Service", + "description": "Our guests are at the heart of everything we do. Your comfort and satisfaction are our top priorities." + } + ]), + "features": json.dumps([ + { + "icon": "Star", + "title": "Premium Accommodations", + "description": "Luxuriously appointed rooms and suites designed for ultimate comfort and relaxation." + }, + { + "icon": "Clock", + "title": "24/7 Service", + "description": "Round-the-clock concierge and room service to attend to your needs at any time." + }, + { + "icon": "Award", + "title": "Award-Winning", + "description": "Recognized for excellence in hospitality and guest satisfaction." + } + ]), + "team": json.dumps([ + { + "name": "Sarah Johnson", + "role": "General Manager", + "image": "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400&h=400&fit=crop", + "bio": "With over 15 years of experience in luxury hospitality, Sarah leads our team with passion and dedication.", + "social_links": { + "linkedin": "https://linkedin.com/in/sarahjohnson", + "twitter": "https://twitter.com/sarahjohnson" + } + }, + { + "name": "Michael Chen", + "role": "Head Chef", + "image": "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=400&fit=crop", + "bio": "Award-winning chef with expertise in international cuisine, bringing world-class dining experiences to our guests.", + "social_links": { + "linkedin": "https://linkedin.com/in/michaelchen", + "twitter": "https://twitter.com/michaelchen" + } + }, + { + "name": "Emily Rodriguez", + "role": "Guest Relations Manager", + "image": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=400&h=400&fit=crop", + "bio": "Ensuring every guest feels valued and receives personalized attention throughout their stay.", + "social_links": { + "linkedin": "https://linkedin.com/in/emilyrodriguez" + } + } + ]), + "timeline": json.dumps([ + { + "year": "2010", + "title": "Grand Opening", + "description": "Luxury Hotel opened its doors, welcoming guests to a new standard of luxury hospitality.", + "image": "https://images.unsplash.com/photo-1566073771259-6a8506099945?w=800&h=600&fit=crop" + }, + { + "year": "2015", + "title": "First Award", + "description": "Received our first 'Best Luxury Hotel' award, recognizing our commitment to excellence.", + "image": "https://images.unsplash.com/photo-1571896349842-33c89424de2d?w=800&h=600&fit=crop" + }, + { + "year": "2018", + "title": "Major Renovation", + "description": "Completed a comprehensive renovation, adding state-of-the-art facilities and expanding our capacity.", + "image": "https://images.unsplash.com/photo-1590490360182-c33d57733427?w=800&h=600&fit=crop" + }, + { + "year": "2020", + "title": "Sustainability Initiative", + "description": "Launched our sustainability program, committing to eco-friendly practices and community engagement.", + "image": "https://images.unsplash.com/photo-1611892440504-42a792e24d32?w=800&h=600&fit=crop" + }, + { + "year": "2023", + "title": "International Recognition", + "description": "Achieved international recognition as one of the world's top luxury hotels.", + "image": "https://images.unsplash.com/photo-1564501049412-61c2a3083791?w=800&h=600&fit=crop" + } + ]), + "achievements": json.dumps([ + { + "icon": "Award", + "title": "Best Luxury Hotel 2023", + "description": "Recognized as the best luxury hotel in the region for exceptional service and amenities.", + "year": "2023", + "image": "https://images.unsplash.com/photo-1571896349842-33c89424de2d?w=400&h=300&fit=crop" + }, + { + "icon": "Star", + "title": "5-Star Rating", + "description": "Maintained our prestigious 5-star rating for over a decade, a testament to our consistent excellence.", + "year": "2022", + "image": "https://images.unsplash.com/photo-1566073771259-6a8506099945?w=400&h=300&fit=crop" + }, + { + "icon": "Award", + "title": "Sustainable Hotel of the Year", + "description": "Awarded for our commitment to environmental sustainability and green practices.", + "year": "2021", + "image": "https://images.unsplash.com/photo-1611892440504-42a792e24d32?w=400&h=300&fit=crop" + }, + { + "icon": "Users", + "title": "Guest Satisfaction Excellence", + "description": "Achieved 98% guest satisfaction rate, the highest in our category.", + "year": "2023", + "image": "https://images.unsplash.com/photo-1590490360182-c33d57733427?w=400&h=300&fit=crop" + } + ]), + "meta_title": "About Us - Luxury Hotel | Our Story, Mission & Vision", + "meta_description": "Learn about Luxury Hotel's commitment to excellence, our story, values, and the dedicated team that makes every stay unforgettable." + } + + # Check if about page content exists + existing = db.query(PageContent).filter(PageContent.page_type == PageType.ABOUT).first() + + if existing: + # Update existing + for key, value in about_data.items(): + setattr(existing, key, value) + existing.updated_at = datetime.utcnow() + print("✓ Updated existing about page content") + else: + # Create new + new_content = PageContent( + page_type=PageType.ABOUT, + **about_data + ) + db.add(new_content) + print("✓ Created new about page content") + + db.commit() + print("\n✅ About page content seeded successfully!") + print("=" * 80) + +if __name__ == "__main__": + db = get_db() + try: + seed_about_page(db) + except Exception as e: + print(f"\n❌ Error: {e}") + import traceback + traceback.print_exc() + db.rollback() + finally: + db.close() + diff --git a/Backend/seed_banners_company.py b/Backend/seed_banners_company.py new file mode 100644 index 00000000..f839c652 --- /dev/null +++ b/Backend/seed_banners_company.py @@ -0,0 +1,214 @@ +""" +Seed script to populate banners and company information with sample data. +Run this script to add default banners and company settings. +""" +import sys +import os +from datetime import datetime, timedelta + +# Add the src directory to the path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) + +from sqlalchemy.orm import Session +from src.config.database import SessionLocal +from src.models.banner import Banner +from src.models.system_settings import SystemSettings +from src.models.user import User + +def seed_banners(db: Session): + """Seed sample banners""" + print("Seeding banners...") + + # Get admin user for updated_by_id (if exists) + admin_user = db.query(User).filter(User.email == "admin@hotel.com").first() + admin_id = admin_user.id if admin_user else None + + # Delete all existing banners + existing_banners = db.query(Banner).all() + if existing_banners: + for banner in existing_banners: + db.delete(banner) + db.commit() + print(f" ✓ Removed {len(existing_banners)} existing banner(s)") + + # New luxury banners with premium content + banners_data = [ + { + "title": "Welcome to Unparalleled Luxury", + "description": "Where timeless elegance meets modern sophistication. Experience the pinnacle of hospitality in our award-winning luxury hotel.", + "image_url": "https://images.unsplash.com/photo-1564501049412-61c2a3083791?w=1920", + "link_url": "/rooms", + "position": "home", + "display_order": 1, + "is_active": True, + "start_date": datetime.utcnow() - timedelta(days=30), + "end_date": datetime.utcnow() + timedelta(days=365), + }, + { + "title": "Exclusive Presidential Suites", + "description": "Indulge in our most opulent accommodations. Spacious suites with panoramic views, private terraces, and personalized butler service.", + "image_url": "https://images.unsplash.com/photo-1590490360182-c33d57733427?w=1920", + "link_url": "/rooms", + "position": "home", + "display_order": 2, + "is_active": True, + "start_date": datetime.utcnow() - timedelta(days=7), + "end_date": datetime.utcnow() + timedelta(days=365), + }, + { + "title": "World-Class Spa & Wellness", + "description": "Rejuvenate your mind, body, and soul. Our award-winning spa offers bespoke treatments using the finest luxury products.", + "image_url": "https://images.unsplash.com/photo-1544161515-4ab6ce6db874?w=1920", + "link_url": "/services", + "position": "home", + "display_order": 3, + "is_active": True, + "start_date": datetime.utcnow() - timedelta(days=1), + "end_date": datetime.utcnow() + timedelta(days=365), + }, + { + "title": "Michelin-Starred Culinary Excellence", + "description": "Savor extraordinary flavors crafted by world-renowned chefs. Our fine dining restaurants offer an unforgettable gastronomic journey.", + "image_url": "https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=1920", + "link_url": "/services", + "position": "home", + "display_order": 4, + "is_active": True, + "start_date": datetime.utcnow(), + "end_date": datetime.utcnow() + timedelta(days=365), + }, + { + "title": "Private Yacht & Exclusive Experiences", + "description": "Create unforgettable memories with our curated luxury experiences. From private yacht charters to exclusive cultural tours.", + "image_url": "https://images.unsplash.com/photo-1544551763-46a013bb70d5?w=1920", + "link_url": "/services", + "position": "home", + "display_order": 5, + "is_active": True, + "start_date": datetime.utcnow() - timedelta(days=15), + "end_date": datetime.utcnow() + timedelta(days=365), + } + ] + + for banner_data in banners_data: + # Create new banner + new_banner = Banner(**banner_data) + db.add(new_banner) + print(f" ✓ Created banner: {banner_data['title']}") + + db.commit() + print("✓ Banners seeded successfully!\n") + +def seed_company_info(db: Session): + """Seed company information""" + print("Seeding company information...") + + # Get admin user for updated_by_id (if exists) + admin_user = db.query(User).filter(User.email == "admin@hotel.com").first() + admin_id = admin_user.id if admin_user else None + + # Company settings + company_settings = [ + { + "key": "company_name", + "value": "Luxury Hotel", + "description": "Company name displayed throughout the application" + }, + { + "key": "company_tagline", + "value": "Experience Unparalleled Elegance", + "description": "Company tagline or slogan" + }, + { + "key": "company_logo_url", + "value": "", + "description": "URL to company logo image (upload via admin dashboard)" + }, + { + "key": "company_favicon_url", + "value": "", + "description": "URL to company favicon image (upload via admin dashboard)" + }, + { + "key": "company_phone", + "value": "+1 (555) 123-4567", + "description": "Company contact phone number" + }, + { + "key": "company_email", + "value": "info@luxuryhotel.com", + "description": "Company contact email address" + }, + { + "key": "company_address", + "value": "123 Luxury Avenue, Premium District, City 12345, Country", + "description": "Company physical address" + }, + { + "key": "tax_rate", + "value": "10.0", + "description": "Default tax rate percentage (e.g., 10.0 for 10%)" + }, + { + "key": "platform_currency", + "value": "EUR", + "description": "Platform-wide currency setting for displaying prices" + } + ] + + for setting_data in company_settings: + # Check if setting exists + existing = db.query(SystemSettings).filter( + SystemSettings.key == setting_data["key"] + ).first() + + if existing: + # Update existing setting + existing.value = setting_data["value"] + existing.description = setting_data["description"] + if admin_id: + existing.updated_by_id = admin_id + print(f" ✓ Updated setting: {setting_data['key']}") + else: + # Create new setting + new_setting = SystemSettings( + key=setting_data["key"], + value=setting_data["value"], + description=setting_data["description"], + updated_by_id=admin_id + ) + db.add(new_setting) + print(f" ✓ Created setting: {setting_data['key']}") + + db.commit() + print("✓ Company information seeded successfully!\n") + +def main(): + """Main seed function""" + db: Session = SessionLocal() + + try: + print("=" * 80) + print("SEEDING BANNERS AND COMPANY INFORMATION") + print("=" * 80) + print() + + seed_banners(db) + seed_company_info(db) + + print("=" * 80) + print("✓ All data seeded successfully!") + print("=" * 80) + + except Exception as e: + db.rollback() + print(f"\n✗ Error seeding data: {e}") + import traceback + traceback.print_exc() + raise + finally: + db.close() + +if __name__ == "__main__": + main() + diff --git a/Backend/seed_homepage_footer.py b/Backend/seed_homepage_footer.py new file mode 100644 index 00000000..3b29ad26 --- /dev/null +++ b/Backend/seed_homepage_footer.py @@ -0,0 +1,456 @@ +""" +Comprehensive seed script to populate homepage and footer with sample luxury content. +Run this script to add default content to the page_content table. +""" +import sys +import os +import json + +# Add the src directory to the path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) + +from sqlalchemy.orm import Session +from src.config.database import SessionLocal +from src.models.page_content import PageContent, PageType + +def seed_homepage_content(db: Session): + """Seed comprehensive homepage content""" + existing = db.query(PageContent).filter(PageContent.page_type == PageType.HOME).first() + + # Luxury Features + luxury_features = [ + { + "icon": "Sparkles", + "title": "Premium Amenities", + "description": "World-class facilities designed for your comfort and relaxation" + }, + { + "icon": "Crown", + "title": "Royal Service", + "description": "Dedicated concierge service available 24/7 for all your needs" + }, + { + "icon": "Award", + "title": "Award-Winning", + "description": "Recognized for excellence in hospitality and guest satisfaction" + }, + { + "icon": "Shield", + "title": "Secure & Private", + "description": "Your privacy and security are our top priorities" + }, + { + "icon": "Heart", + "title": "Personalized Care", + "description": "Tailored experiences crafted just for you" + }, + { + "icon": "Gem", + "title": "Luxury Design", + "description": "Elegantly designed spaces with attention to every detail" + } + ] + + # Luxury Gallery + luxury_gallery = [ + "https://images.unsplash.com/photo-1566073771259-6a8506099945?w=800", + "https://images.unsplash.com/photo-1571896349842-33c89424de2d?w=800", + "https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?w=800", + "https://images.unsplash.com/photo-1564501049412-61c2a3083791?w=800", + "https://images.unsplash.com/photo-1590490360182-c33d57733427?w=800", + "https://images.unsplash.com/photo-1551882547-ff40c63fe5fa?w=800" + ] + + # Luxury Testimonials + luxury_testimonials = [ + { + "name": "Sarah Johnson", + "title": "Business Executive", + "quote": "An absolutely stunning experience. The attention to detail and level of service exceeded all expectations.", + "image": "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=200" + }, + { + "name": "Michael Chen", + "title": "Travel Enthusiast", + "quote": "The epitome of luxury. Every moment was perfect, from check-in to check-out.", + "image": "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=200" + }, + { + "name": "Emma Williams", + "title": "Luxury Traveler", + "quote": "This hotel redefines what luxury means. I will definitely return.", + "image": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=200" + } + ] + + # Luxury Services + luxury_services = [ + { + "icon": "UtensilsCrossed", + "title": "Fine Dining", + "description": "Michelin-starred restaurants offering world-class cuisine", + "image": "https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=600" + }, + { + "icon": "Wine", + "title": "Premium Bar", + "description": "Extensive wine collection and craft cocktails in elegant settings", + "image": "https://images.unsplash.com/photo-1514362545857-3bc16c4c7d1b?w=600" + }, + { + "icon": "Dumbbell", + "title": "Spa & Wellness", + "description": "Rejuvenating spa treatments and state-of-the-art fitness center", + "image": "https://images.unsplash.com/photo-1544161515-4ab6ce6db874?w=600" + }, + { + "icon": "Car", + "title": "Concierge Services", + "description": "Personalized assistance for all your travel and entertainment needs", + "image": "https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=600" + } + ] + + # Luxury Experiences + luxury_experiences = [ + { + "icon": "Sunset", + "title": "Sunset Rooftop", + "description": "Breathtaking views and exclusive rooftop experiences", + "image": "https://images.unsplash.com/photo-1514933651103-005eec06c04b?w=600" + }, + { + "icon": "Ship", + "title": "Yacht Excursions", + "description": "Private yacht charters for unforgettable sea adventures", + "image": "https://images.unsplash.com/photo-1544551763-46a013bb70d5?w=600" + }, + { + "icon": "Music", + "title": "Live Entertainment", + "description": "World-class performances and exclusive events", + "image": "https://images.unsplash.com/photo-1470229722913-7c0e2dbbafd3?w=600" + }, + { + "icon": "Palette", + "title": "Art & Culture", + "description": "Curated art collections and cultural experiences", + "image": "https://images.unsplash.com/photo-1578301978018-3005759f48f7?w=600" + } + ] + + # Awards + awards = [ + { + "icon": "Trophy", + "title": "Best Luxury Hotel 2024", + "description": "Awarded by International Luxury Travel Association", + "image": "https://images.unsplash.com/photo-1579783902614-a3fb3927b6a5?w=400", + "year": "2024" + }, + { + "icon": "Star", + "title": "5-Star Excellence", + "description": "Consistently rated 5 stars by leading travel publications", + "image": "https://images.unsplash.com/photo-1606761568499-6d2451b23c66?w=400", + "year": "2023" + }, + { + "icon": "Award", + "title": "Sustainable Luxury", + "description": "Recognized for environmental responsibility and sustainability", + "image": "https://images.unsplash.com/photo-1473341304170-971dccb5ac1e?w=400", + "year": "2024" + } + ] + + # Partners + partners = [ + { + "name": "Luxury Travel Group", + "logo": "https://images.unsplash.com/photo-1599305445671-ac291c95aaa9?w=200", + "link": "#" + }, + { + "name": "Premium Airlines", + "logo": "https://images.unsplash.com/photo-1436491865332-7a61a109cc05?w=200", + "link": "#" + }, + { + "name": "Exclusive Events", + "logo": "https://images.unsplash.com/photo-1511578314322-379afb476865?w=200", + "link": "#" + }, + { + "name": "Fine Dining Network", + "logo": "https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=200", + "link": "#" + } + ] + + # Stats + stats = [ + { + "icon": "Users", + "number": "50,000+", + "label": "Happy Guests" + }, + { + "icon": "Award", + "number": "25+", + "label": "Awards Won" + }, + { + "icon": "Star", + "number": "4.9", + "label": "Average Rating" + }, + { + "icon": "Globe", + "number": "100+", + "label": "Countries Served" + } + ] + + # Amenities + amenities = [ + { + "icon": "Wifi", + "title": "High-Speed WiFi", + "description": "Complimentary high-speed internet throughout the property", + "image": "" + }, + { + "icon": "Coffee", + "title": "24/7 Room Service", + "description": "Round-the-clock dining and beverage service", + "image": "" + }, + { + "icon": "Car", + "title": "Valet Parking", + "description": "Complimentary valet parking for all guests", + "image": "" + }, + { + "icon": "Plane", + "title": "Airport Transfer", + "description": "Luxury airport transfer service available", + "image": "" + } + ] + + # Testimonials + testimonials = [ + { + "name": "Robert Martinez", + "role": "CEO, Tech Corp", + "image": "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=200", + "rating": 5, + "comment": "Exceptional service and attention to detail. The staff went above and beyond to make our stay memorable." + }, + { + "name": "Lisa Anderson", + "role": "Travel Blogger", + "image": "https://images.unsplash.com/photo-1487412720507-e7ab37603c6f?w=200", + "rating": 5, + "comment": "The most luxurious hotel experience I've ever had. Every detail was perfect." + }, + { + "name": "David Thompson", + "role": "Investment Banker", + "image": "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=200", + "rating": 5, + "comment": "Outstanding facilities and impeccable service. Highly recommend for business travelers." + } + ] + + # Gallery Images + gallery_images = [ + "https://images.unsplash.com/photo-1564501049412-61c2a3083791?w=800", + "https://images.unsplash.com/photo-1590490360182-c33d57733427?w=800", + "https://images.unsplash.com/photo-1551882547-ff40c63fe5fa?w=800", + "https://images.unsplash.com/photo-1571896349842-33c89424de2d?w=800", + "https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?w=800", + "https://images.unsplash.com/photo-1566073771259-6a8506099945?w=800" + ] + + homepage_data = { + "page_type": PageType.HOME, + "title": "Luxury Hotel - Experience Unparalleled Elegance", + "subtitle": "Where timeless luxury meets modern sophistication", + "description": "Discover a world of refined elegance and exceptional service", + "hero_title": "Welcome to Luxury", + "hero_subtitle": "Experience the pinnacle of hospitality", + "hero_image": "https://images.unsplash.com/photo-1564501049412-61c2a3083791?w=1200", + "luxury_section_title": "Experience Unparalleled Luxury", + "luxury_section_subtitle": "Where elegance meets comfort in every detail", + "luxury_section_image": "https://images.unsplash.com/photo-1571896349842-33c89424de2d?w=1200", + "luxury_features": json.dumps(luxury_features), + "luxury_gallery_section_title": "Our Luxury Gallery", + "luxury_gallery_section_subtitle": "A glimpse into our world of elegance", + "luxury_gallery": json.dumps(luxury_gallery), + "luxury_testimonials_section_title": "What Our Guests Say", + "luxury_testimonials_section_subtitle": "Testimonials from our valued guests", + "luxury_testimonials": json.dumps(luxury_testimonials), + "luxury_services_section_title": "Premium Services", + "luxury_services_section_subtitle": "Indulge in our world-class amenities", + "luxury_services": json.dumps(luxury_services), + "luxury_experiences_section_title": "Exclusive Experiences", + "luxury_experiences_section_subtitle": "Create unforgettable memories", + "luxury_experiences": json.dumps(luxury_experiences), + "awards_section_title": "Awards & Recognition", + "awards_section_subtitle": "Recognized for excellence worldwide", + "awards": json.dumps(awards), + "partners_section_title": "Our Partners", + "partners_section_subtitle": "Trusted by leading brands", + "partners": json.dumps(partners), + "amenities_section_title": "Premium Amenities", + "amenities_section_subtitle": "Everything you need for a perfect stay", + "amenities": json.dumps(amenities), + "testimonials_section_title": "Guest Reviews", + "testimonials_section_subtitle": "Hear from our satisfied guests", + "testimonials": json.dumps(testimonials), + "gallery_section_title": "Photo Gallery", + "gallery_section_subtitle": "Explore our beautiful spaces", + "gallery_images": json.dumps(gallery_images), + "about_preview_title": "About Our Luxury Hotel", + "about_preview_subtitle": "A legacy of excellence", + "about_preview_content": "Discover a world of refined elegance and exceptional service. Our hotel combines timeless luxury with modern amenities to create an unforgettable experience. With over 50,000 satisfied guests and numerous awards, we continue to set the standard for luxury hospitality.", + "about_preview_image": "https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?w=800", + "stats": json.dumps(stats), + "cta_title": "Ready to Experience Luxury?", + "cta_subtitle": "Book your stay today and discover the difference", + "cta_button_text": "Book Now", + "cta_button_link": "/rooms", + "cta_image": "https://images.unsplash.com/photo-1566073771259-6a8506099945?w=1200", + "is_active": True + } + + if existing: + for key, value in homepage_data.items(): + if key != "page_type": + setattr(existing, key, value) + print("✓ Updated existing homepage content") + else: + new_content = PageContent(**homepage_data) + db.add(new_content) + print("✓ Created new homepage content") + + db.commit() + +def seed_footer_content(db: Session): + """Seed comprehensive footer content""" + existing = db.query(PageContent).filter(PageContent.page_type == PageType.FOOTER).first() + + # Contact Info + contact_info = { + "phone": "+1 (555) 123-4567", + "email": "info@luxuryhotel.com", + "address": "123 Luxury Avenue, Premium District, City 12345" + } + + # Social Links + social_links = { + "facebook": "https://facebook.com/luxuryhotel", + "twitter": "https://twitter.com/luxuryhotel", + "instagram": "https://instagram.com/luxuryhotel", + "linkedin": "https://linkedin.com/company/luxuryhotel", + "youtube": "https://youtube.com/luxuryhotel" + } + + # Footer Links + footer_links = { + "quick_links": [ + {"label": "Home", "url": "/"}, + {"label": "Rooms & Suites", "url": "/rooms"}, + {"label": "About Us", "url": "/about"}, + {"label": "Contact", "url": "/contact"}, + {"label": "Gallery", "url": "/gallery"} + ], + "support_links": [ + {"label": "FAQ", "url": "/faq"}, + {"label": "Privacy Policy", "url": "/privacy"}, + {"label": "Terms of Service", "url": "/terms"}, + {"label": "Cancellation Policy", "url": "/cancellation"}, + {"label": "Accessibility", "url": "/accessibility"} + ] + } + + # Badges + badges = [ + { + "text": "5-Star Rated", + "icon": "Star" + }, + { + "text": "Award Winning", + "icon": "Award" + }, + { + "text": "Eco Certified", + "icon": "Leaf" + }, + { + "text": "Luxury Collection", + "icon": "Crown" + } + ] + + footer_data = { + "page_type": PageType.FOOTER, + "title": "Luxury Hotel", + "subtitle": "Experience Unparalleled Elegance", + "description": "Your gateway to luxury hospitality and exceptional service", + "contact_info": json.dumps(contact_info), + "social_links": json.dumps(social_links), + "footer_links": json.dumps(footer_links), + "badges": json.dumps(badges), + "copyright_text": "© {YEAR} Luxury Hotel. All rights reserved.", + "is_active": True + } + + if existing: + for key, value in footer_data.items(): + if key != "page_type": + setattr(existing, key, value) + print("✓ Updated existing footer content") + else: + new_content = PageContent(**footer_data) + db.add(new_content) + print("✓ Created new footer content") + + db.commit() + +def main(): + """Main seed function""" + db: Session = SessionLocal() + + try: + print("=" * 80) + print("SEEDING HOMEPAGE AND FOOTER CONTENT") + print("=" * 80) + print() + + print("Seeding homepage content...") + seed_homepage_content(db) + + print("\nSeeding footer content...") + seed_footer_content(db) + + print("\n" + "=" * 80) + print("✓ All content seeded successfully!") + print("=" * 80) + + except Exception as e: + db.rollback() + print(f"\n✗ Error seeding content: {e}") + import traceback + traceback.print_exc() + raise + finally: + db.close() + +if __name__ == "__main__": + main() + diff --git a/Backend/seed_luxury_content.py b/Backend/seed_luxury_content.py new file mode 100644 index 00000000..e84a0ffc --- /dev/null +++ b/Backend/seed_luxury_content.py @@ -0,0 +1,123 @@ +""" +Seed script to populate initial luxury content for the homepage. +Run this script to add default luxury content to the page_content table. +""" +import sys +import os +import json + +# Add the src directory to the path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) + +from sqlalchemy.orm import Session +from src.config.database import SessionLocal, engine +from src.models.page_content import PageContent +from src.models.user import User + +def seed_luxury_content(): + """Seed luxury content for the homepage""" + db: Session = SessionLocal() + + try: + # Check if home page content already exists + existing = db.query(PageContent).filter(PageContent.page_type == 'home').first() + + luxury_features = [ + { + "icon": "Sparkles", + "title": "Premium Amenities", + "description": "World-class facilities designed for your comfort and relaxation" + }, + { + "icon": "Crown", + "title": "Royal Service", + "description": "Dedicated concierge service available 24/7 for all your needs" + }, + { + "icon": "Award", + "title": "Award-Winning", + "description": "Recognized for excellence in hospitality and guest satisfaction" + }, + { + "icon": "Shield", + "title": "Secure & Private", + "description": "Your privacy and security are our top priorities" + }, + { + "icon": "Heart", + "title": "Personalized Care", + "description": "Tailored experiences crafted just for you" + }, + { + "icon": "Gem", + "title": "Luxury Design", + "description": "Elegantly designed spaces with attention to every detail" + } + ] + + luxury_testimonials = [ + { + "name": "Sarah Johnson", + "title": "Business Executive", + "quote": "An absolutely stunning experience. The attention to detail and level of service exceeded all expectations.", + "image": "" + }, + { + "name": "Michael Chen", + "title": "Travel Enthusiast", + "quote": "The epitome of luxury. Every moment was perfect, from check-in to check-out.", + "image": "" + }, + { + "name": "Emma Williams", + "title": "Luxury Traveler", + "quote": "This hotel redefines what luxury means. I will definitely return.", + "image": "" + } + ] + + if existing: + # Update existing content + existing.luxury_section_title = "Experience Unparalleled Luxury" + existing.luxury_section_subtitle = "Where elegance meets comfort in every detail" + existing.luxury_section_image = None + existing.luxury_features = json.dumps(luxury_features) + existing.luxury_gallery = json.dumps([]) + existing.luxury_testimonials = json.dumps(luxury_testimonials) + existing.about_preview_title = "About Our Luxury Hotel" + existing.about_preview_content = "Discover a world of refined elegance and exceptional service. Our hotel combines timeless luxury with modern amenities to create an unforgettable experience." + existing.about_preview_image = None + print("✓ Updated existing home page content with luxury sections") + else: + # Create new content + new_content = PageContent( + page_type='home', + luxury_section_title="Experience Unparalleled Luxury", + luxury_section_subtitle="Where elegance meets comfort in every detail", + luxury_section_image=None, + luxury_features=json.dumps(luxury_features), + luxury_gallery=json.dumps([]), + luxury_testimonials=json.dumps(luxury_testimonials), + about_preview_title="About Our Luxury Hotel", + about_preview_content="Discover a world of refined elegance and exceptional service. Our hotel combines timeless luxury with modern amenities to create an unforgettable experience.", + about_preview_image=None, + is_active=True + ) + db.add(new_content) + print("✓ Created new home page content with luxury sections") + + db.commit() + print("✓ Luxury content seeded successfully!") + + except Exception as e: + db.rollback() + print(f"✗ Error seeding luxury content: {e}") + raise + finally: + db.close() + +if __name__ == "__main__": + print("Seeding luxury content...") + seed_luxury_content() + print("Done!") + diff --git a/Backend/seed_rooms.py b/Backend/seed_rooms.py new file mode 100644 index 00000000..277dafae --- /dev/null +++ b/Backend/seed_rooms.py @@ -0,0 +1,317 @@ +#!/usr/bin/env python3 +""" +Seed script to delete all existing rooms and create 50 sample luxury hotel rooms +""" + +import sys +import os +from pathlib import Path + +# Add the parent directory to the path so we can import from src +sys.path.insert(0, str(Path(__file__).parent)) + +from sqlalchemy.orm import Session +from src.config.database import SessionLocal, engine +from src.models.room import Room, RoomStatus +from src.models.room_type import RoomType +from datetime import datetime +import json +import random + +def get_db(): + """Get database session""" + db = SessionLocal() + try: + return db + finally: + pass + +def seed_rooms(db: Session): + """Delete all existing rooms and create 50 sample luxury rooms""" + print("=" * 80) + print("SEEDING ROOMS - DELETING EXISTING AND CREATING 50 NEW LUXURY ROOMS") + print("=" * 80) + + # Get all room types + room_types = db.query(RoomType).all() + if not room_types: + print("❌ No room types found! Please create room types first.") + return + + print(f"\n✓ Found {len(room_types)} room type(s)") + for rt in room_types: + print(f" - {rt.name} (ID: {rt.id}, Base Price: {rt.base_price})") + + # Delete all existing rooms + # First, we need to handle related records that reference these rooms + from src.models.booking import Booking + from src.models.review import Review + from src.models.favorite import Favorite + + existing_rooms = db.query(Room).all() + if existing_rooms: + print(f"\n🗑️ Deleting {len(existing_rooms)} existing room(s)...") + + # Get all room IDs + room_ids = [room.id for room in existing_rooms] + + # Delete bookings that reference these rooms + bookings_with_rooms = db.query(Booking).filter(Booking.room_id.in_(room_ids)).all() + if bookings_with_rooms: + print(f" ⚠️ Found {len(bookings_with_rooms)} booking(s) referencing these rooms") + for booking in bookings_with_rooms: + db.delete(booking) + print(f" ✓ Deleted {len(bookings_with_rooms)} booking(s)") + + # Delete reviews that reference these rooms + reviews_with_rooms = db.query(Review).filter(Review.room_id.in_(room_ids)).all() + if reviews_with_rooms: + print(f" ⚠️ Found {len(reviews_with_rooms)} review(s) referencing these rooms") + for review in reviews_with_rooms: + db.delete(review) + print(f" ✓ Deleted {len(reviews_with_rooms)} review(s)") + + # Delete favorites that reference these rooms + favorites_with_rooms = db.query(Favorite).filter(Favorite.room_id.in_(room_ids)).all() + if favorites_with_rooms: + print(f" ⚠️ Found {len(favorites_with_rooms)} favorite(s) referencing these rooms") + for favorite in favorites_with_rooms: + db.delete(favorite) + print(f" ✓ Deleted {len(favorites_with_rooms)} favorite(s)") + + # Now delete the rooms + for room in existing_rooms: + db.delete(room) + db.commit() + print(f"✓ Deleted {len(existing_rooms)} room(s)") + + # Luxury room configurations + views = [ + "Ocean View", "City View", "Garden View", "Mountain View", + "Pool View", "Beach View", "Panoramic View", "Sea View" + ] + + room_sizes = [ + "35 sqm", "40 sqm", "45 sqm", "50 sqm", "55 sqm", + "60 sqm", "70 sqm", "80 sqm", "90 sqm", "100 sqm", + "120 sqm", "150 sqm", "180 sqm", "200 sqm", "250 sqm" + ] + + # Real luxury hotel room images from Unsplash (defined once, used for all rooms) + luxury_room_images = [ + # Luxury hotel rooms + "https://images.unsplash.com/photo-1631049307264-da0ec9d70304?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1611892440504-42a792e24d32?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1590490360182-c33d57733427?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1564501049412-61c2a3083791?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1578683010236-d716f9a3f461?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1596394516093-501ba68a0ba6?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1618221195710-dd6b41faaea8?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1566665797739-1674de7a421a?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1566073771259-6a8506099945?w=1200&h=800&fit=crop", + # Suite images + "https://images.unsplash.com/photo-1618773928121-c32242e63f39?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1582719508461-905c673771fd?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1595576508898-0ad5c879a061?w=1200&h=800&fit=crop", + # Additional luxury rooms + "https://images.unsplash.com/photo-1571003123894-1f0594d2b5d9?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1566073771259-6a8506099945?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1578683010236-d716f9a3f461?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1596394516093-501ba68a0ba6?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1631049307264-da0ec9d70304?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1611892440504-42a792e24d32?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1590490360182-c33d57733427?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1564501049412-61c2a3083791?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1618221195710-dd6b41faaea8?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1566665797739-1674de7a421a?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1618773928121-c32242e63f39?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1582719508461-905c673771fd?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1595576508898-0ad5c879a061?w=1200&h=800&fit=crop", + "https://images.unsplash.com/photo-1571003123894-1f0594d2b5d9?w=1200&h=800&fit=crop", + ] + + # Comprehensive luxury amenities + all_amenities = [ + "Free WiFi", "High-Speed Internet", "Smart TV", "Netflix", + "Air Conditioning", "Climate Control", "Private Balcony", + "Ocean View", "City View", "Minibar", "Coffee Maker", + "Espresso Machine", "Refrigerator", "Safe", "Iron & Ironing Board", + "Hair Dryer", "Premium Toiletries", "Bathrobes", "Slippers", + "Work Desk", "Ergonomic Chair", "USB Charging Ports", + "Bluetooth Speaker", "Sound System", "Blackout Curtains", + "Pillow Menu", "Turndown Service", "Room Service", "24/7 Concierge" + ] + + premium_amenities = [ + "Jacuzzi Bathtub", "Steam Shower", "Rain Shower", "Bidet", + "Private Pool", "Outdoor Terrace", "Fireplace", "Wine Cellar", + "Private Bar", "Butler Service", "Private Elevator", "Helipad Access" + ] + + # Room configurations: (floor_range, room_numbers_per_floor, view, size_range, amenities_count, featured) + room_configs = [ + # Standard Rooms (Floors 1-3) + ((1, 3), 8, "Garden View", (35, 45), 8, False), + ((1, 3), 8, "City View", (40, 50), 9, False), + + # Superior Rooms (Floors 4-6) + ((4, 6), 6, "City View", (45, 55), 10, False), + ((4, 6), 6, "Pool View", (50, 60), 11, True), + + # Deluxe Rooms (Floors 7-9) + ((7, 9), 5, "Ocean View", (55, 70), 12, True), + ((7, 9), 5, "Mountain View", (60, 75), 12, False), + + # Executive Suites (Floors 10-12) + ((10, 12), 4, "Panoramic View", (80, 100), 14, True), + ((10, 12), 4, "Sea View", (90, 110), 15, True), + + # Luxury Suites (Floors 13-15) + ((13, 15), 3, "Ocean View", (120, 150), 16, True), + ((13, 15), 3, "Beach View", (130, 160), 17, True), + + # Presidential Suites (Floor 16) + ((16, 16), 2, "Panoramic View", (200, 250), 20, True), + ] + + rooms_created = [] + room_counter = 101 # Starting room number + + print(f"\n🏨 Creating 50 luxury rooms...\n") + + for config in room_configs: + floor_range, rooms_per_floor, view_type, size_range, amenities_count, featured = config + + for floor in range(floor_range[0], floor_range[1] + 1): + for _ in range(rooms_per_floor): + if len(rooms_created) >= 50: + break + + # Select random room type based on floor + if floor <= 3: + room_type = random.choice([rt for rt in room_types if 'standard' in rt.name.lower() or 'superior' in rt.name.lower()] or room_types) + elif floor <= 6: + room_type = random.choice([rt for rt in room_types if 'superior' in rt.name.lower() or 'deluxe' in rt.name.lower()] or room_types) + elif floor <= 9: + room_type = random.choice([rt for rt in room_types if 'deluxe' in rt.name.lower() or 'executive' in rt.name.lower()] or room_types) + elif floor <= 12: + room_type = random.choice([rt for rt in room_types if 'executive' in rt.name.lower() or 'suite' in rt.name.lower()] or room_types) + else: + room_type = random.choice([rt for rt in room_types if 'suite' in rt.name.lower() or 'presidential' in rt.name.lower()] or room_types) + + # If no matching room type, use random + if not room_type: + room_type = random.choice(room_types) + + # Calculate price (base price + floor premium + view premium + random variation) + base_price = float(room_type.base_price) + floor_premium = (floor - 1) * 5 # +5 per floor + view_premium = 20 if "Ocean" in view_type or "Sea" in view_type or "Beach" in view_type else 0 + view_premium += 15 if "Panoramic" in view_type else 0 + view_premium += 10 if "Mountain" in view_type else 0 + view_premium += 5 if "Pool" in view_type else 0 + # Add random variation (-5% to +10% of base price) + random_variation = base_price * random.uniform(-0.05, 0.10) + # Size premium (larger rooms cost more) + size_min, size_max = size_range + size_premium = (size_min + size_max) / 2 * 0.5 # ~0.5 per sqm + price = base_price + floor_premium + view_premium + random_variation + size_premium + # Ensure minimum price and round to 2 decimal places + price = max(base_price * 0.95, price) + price = round(price, 2) + + # Select amenities + selected_amenities = random.sample(all_amenities, min(amenities_count, len(all_amenities))) + if floor >= 13: # Add premium amenities for luxury suites + premium_count = min(2, len(premium_amenities)) + selected_amenities.extend(random.sample(premium_amenities, premium_count)) + + # Room size + size_min, size_max = size_range + room_size = f"{random.randint(size_min, size_max)} sqm" + + # Capacity (based on room type, with some variation) + capacity = room_type.capacity + if random.random() > 0.7: # 30% chance to have different capacity + capacity = max(1, capacity + random.randint(-1, 1)) + + # Room number + room_number = f"{floor}{room_counter % 100:02d}" + room_counter += 1 + + # Select 3 unique images for each room (ensure we always have images) + # Shuffle the list each time to get different combinations + shuffled_images = luxury_room_images.copy() + random.shuffle(shuffled_images) + image_urls = shuffled_images[:3] # Always take first 3 after shuffle + + # Description + descriptions = [ + f"Elegantly designed {view_type.lower()} room with modern luxury amenities and breathtaking views.", + f"Spacious {view_type.lower()} accommodation featuring premium furnishings and world-class comfort.", + f"Luxurious {view_type.lower()} room with sophisticated decor and exceptional attention to detail.", + f"Exquisite {view_type.lower()} suite offering unparalleled elegance and personalized service.", + f"Opulent {view_type.lower()} accommodation with bespoke interiors and premium amenities.", + ] + description = random.choice(descriptions) + + # Status (mostly available, some in maintenance/cleaning) + status_weights = [0.85, 0.05, 0.05, 0.05] # available, occupied, maintenance, cleaning + status = random.choices( + [RoomStatus.available, RoomStatus.occupied, RoomStatus.maintenance, RoomStatus.cleaning], + weights=status_weights + )[0] + + # Create room + room = Room( + room_type_id=room_type.id, + room_number=room_number, + floor=floor, + status=status, + price=price, + featured=featured, + capacity=capacity, + room_size=room_size, + view=view_type, + images=json.dumps(image_urls), + amenities=json.dumps(selected_amenities), + description=description + ) + + db.add(room) + rooms_created.append({ + 'number': room_number, + 'floor': floor, + 'type': room_type.name, + 'view': view_type, + 'price': price + }) + + print(f" ✓ Created Room {room_number} - Floor {floor}, {room_type.name}, {view_type}, {room_size}, €{price:.2f}") + + db.commit() + print(f"\n✅ Successfully created {len(rooms_created)} luxury rooms!") + print(f"\n📊 Summary:") + featured_count = sum(1 for r in rooms_created if any( + config[5] and r['floor'] >= config[0][0] and r['floor'] <= config[0][1] + for config in room_configs + )) + print(f" - Featured rooms: {featured_count}") + print(f" - Floors: {min(r['floor'] for r in rooms_created)} - {max(r['floor'] for r in rooms_created)}") + print(f" - Price range: €{min(r['price'] for r in rooms_created):.2f} - €{max(r['price'] for r in rooms_created):.2f}") + print("=" * 80) + +if __name__ == "__main__": + db = get_db() + try: + seed_rooms(db) + except Exception as e: + print(f"\n❌ Error: {e}") + import traceback + traceback.print_exc() + db.rollback() + finally: + db.close() + diff --git a/Backend/src/__pycache__/main.cpython-312.pyc b/Backend/src/__pycache__/main.cpython-312.pyc index 65d9cdf6682761477030d9b9c124d0f14024b9f8..adc77dfcae924f322b3292480c24be13362474e6 100644 GIT binary patch delta 599 zcmZ3RKc|fEG%qg~0}!Y>$Y(Cso5&}@#3a5^;}EAnZbei@Ze>(uZdFuOZgo`kDZ3CvaObR)WbYFj);EH!tRWzyg$X;PVAag2*Z`Sq&m5?-dfA#H|XXViUy@q1`I3q)6ITbw8la<#^C$Oe$Za-Pmt$mXogAj2%i0Z;;N0Ay z!N|zivAIjL9Vot8NIQ#(v3+u_ZiH%4Jy5==0Yo%{h$bL$iz7cTKD7*JR}skEqVCDh zbkn#m1DT*;DVF8h9Ih9^qR7U`^qm2d_zDvHAjH7PGWmhA!sIw3{>eJVT$5#uB{$0& z+cSbW&y9^Io0v#WHZ$g*oNMH;d5+0`#>wd>3X|`dDol1Vm7Dz7M1FF#*>+%X37ZEo e7EVqxHxkX2Vt6aXnrp}WR$e66lKHJAhz$TnSE(Za delta 417 zcmbPJwl<&dG%qg~0}wdRmdOm&naC%>bU|dJ#v#th0i2SPXY+|p7T{Lh?8}wLym>a? z1D45eMZh9JI)?G%WG_)o*2>g?tdEl`L^Xx0Qma$^vo#nPil(P9r10j{MAb~*BI;~j z%fyhHkXoA(P{qx_u$mEM8v{dB9Z<9;>mP)NOr`{;%1>a-Vwh|oCeFw-`GA1vuC&bXnU$hHU<$#mLCmviYBOJ5YRcn{E~pWAo&X`VoS+IP&x2Q_E8G zN{T>c6tz#zGf3k;4`hOTP;ADz`MyB}iwr9x({~0G;wy;zL4bjgWpb{W!sJz^k|4Z! zm8m`BOf$!idP1{)b+Y35&58!N(pJE{LG$dzN>rw+{Dy&*%I5bHC@@``&%O{L$%@ z5UgK=E%7aJP4?rxwUgPKs!g>g8)m#wFGdW~_-z^kE9wIcYrbv#N3UouON%k8Llab| zW>;nHSWHlrWj^XxU0Ne}>X*4_U|xv0*UFn>1Q)n#sid}J;v9qP&1vqAP!A=E+dSa5 z_)dVXokzSEOE!S?JtXE^p>@PKYK)M z7ecpAAyiGa*X$Ourk`apjNW88ExRFGDB-eDZCCLdX8VZTJ)B_@ZnRkQ9-%CoVOP1^ znEZcP_Mfr{Cdge`@Z8Wf5+ZP5!?2U9A#nrB=$A zv{4ZTQLxe6@wiIbK{Ln1=KcaVJ2|68c+yRS+%$-zK?OMty~Zo?G=g-2OQ$mr!CE36 z<}0yps)rgMfcUBLKE}>5Wuf2q2};jxHotFY|(psBW9+)@(p~j z=na%u*bf|@64lEBlH_kTK&4LP@PFcbq#p`In#}ig)40V9Nau&`GbvR|>Ya2tl@S+2 zU7JdixG`VhSYUO75K7=Rs$`Q6z(2$R$V(@+L2?OHddu~=xh}PvAEW`L3HzU2C?nV^ G8NUJBbj-m3 delta 816 zcmYjO%}Z2K6ugktDqLi1jSnwt!6i&fwbz}t5Y7h=lsq)zjMy--uEN%ARhi03i%P( zt)IzU*|!=_;18=O7A7P_)eZg~EFFUk)7S>y%S9HgXO=_)R3kweAR&6h(nxq&O~*-u zHbEu2tfUk3n$hqGuWi1<6$wY(d&*rc3X@oayQ{oZPXUZZRG6dH(X@`P*73hc)VJEk{>b~dj6kVx+*3`G z&&_(O?UDi*~6!MfAa+PahL2y;st?K6&B^Kjf;@}LGwLoN~M-qCnpC* zH9JMKw<#+fH;eAOLv0=1oXD@XyM(?swXC0eUD|5wU(vrMTi2SOHm}coZSUV|@2?~W z+_V1fMVZOKW&E$Q2slF$tJnZwn?J?P)ZVs;S#+ifELWmJmCY_dk+<0fI9Rr$qmw;f zdWI_PZR}N%wi<=D$_vtpx7Fo`j y7h*iwc9_`l28kY_;*UO User: - # Map role IDs to role names - role_map = {1: "admin", 2: "staff", 3: "customer"} - user_role_name = role_map.get(current_user.role_id) + def role_checker( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) + ) -> User: + # Query the role from database instead of using hardcoded IDs + role = db.query(Role).filter(Role.id == current_user.role_id).first() + + if not role: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="User role not found" + ) + + user_role_name = role.name if user_role_name not in allowed_roles: raise HTTPException( diff --git a/Backend/src/models/__pycache__/page_content.cpython-312.pyc b/Backend/src/models/__pycache__/page_content.cpython-312.pyc index ce674008010f9c01b2439caaa392036e5aa8c366..4cae9e9113c2888cd59373107c8d6c72917321f1 100644 GIT binary patch literal 5351 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 literal 0 HcmV?d00001 diff --git a/Backend/src/routes/__pycache__/auth_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/auth_routes.cpython-312.pyc index 29636daef701dc793f1604072c1294c312fb39c8..6ab38810a1344bf850b8789fb9c3196d12c3c80e 100644 GIT binary patch delta 428 zcmW;Ize@sP7zgm@@x9;O#gU$6h)NnL4GR6ZK}A%8dO4D6YH4ph0vm#Y8Xd%g{)nI; zZggs}qtl@!aEivpD;pYnUmtj0KHv9!p5^@*;O_v9v0-Sy>ad(WD?S-_JDSeY4eMFL zf7m${Q1!H`zbzbt4gFZJl85sgd!1JSTmj((`q1glIQEmZ zDaK2JyU&(fi6002==f3n5%pmV;0yk)Au{*;`^FoJE9q%6>s>aFWYsja2u$z2S;v|e zZ*{H|ylq<4hUXMgRd`-)7g-n)lj<;vH+gueB27y#HNxGn=o$$UNHbDk3EZg#b{`aw z=7eCqm1iMT&`~f{ky}@3ii8$l4p)#nC2(huEr+?y`Oum~x8$&hbVCnIGIvGhL-vv* zlCf$#KqeqEPekGfzwmxCr{y%-REMBEvAUxH8mb;h4(bhZ$rEdAL+thEuSlyVsJ;r1j=* z>0+9R6PYqlIbfZe=ufc|CIb=w+BF0H(dv)HkRKBi_VZQ;3yA1B$--K}-OG9I!+D?g zdEfV(`{ly#Y5509vH|*xcBj4ze=V2#dleRdX8;7NzzT{m7cgK`K)_(yEY0F1=XSuW z%-X%Zw_p+C&+Q&o53~qg@9^L`zim&h|Nk8j+Y(P*2A?v!&C;#D+7+AM2Q?GocIF#W zJJFEaYz^Y)t$XottA{La2VbxQpb8XMP!Xlwbfiv;YPo7{4+X(}pCU}#@HgRzxD0a) z1gdR?Rqgn8_+^LKmXCw;P?hk}f#I<91_IzCb``zDabU$#v^=PFN#5By{$L<9x+|Yl zljz&7bDmLKan$lBaR>fwAbP~2I@OM%wP;seR~XfOg?#};!Nm(?>10ijH4*}&pqWo@ zxz%43a3BZsWTq43)*-k!y^KzPJPZK?;JUl}N%8*usU9QTKF*@JcJI&F*}W>Qh$S%( z^PuJO70Sb~7ytNZ=$vzQQF%4>nx-u2i)T`*rs`9~tH7r+$pvk=u779XOKAFvG`cRF@&(9X5W;J4?WtNPyczW<)XPDJv)QYE&q%cY0aROw5 z)j?%{O+2}nHHiJ3adt`56%wgg=t#6D-AUD13X3$rWlpIn9ajcZu!Nr&dS&=;a$L7T z<4s`7F>dl#ho|o9t_OQ6!RS^nTJ?wTaL6s207g%rh$8UA^R)Y_3bI2=iTP;qT2`pBd*-3FAG7g#?CC6=86<$i%GyIcvk9hFCX2_D_#xr80v zw4;Q4{rlmUAbj_F*XSHGHVxrqRq~jKl3RAK$x&kR!0rD-p!81!m>IauP_Bo_-Tw delta 983 zcmZuuUr19?7(eHp-FtVpyX)NDc4wP7x9K+B9~H~~Xa%ta8d2m!^-#_36i3dQH0SLY zwS*R_3i${s#yy#@qD5FiDWS^j*Mm8npv79<1eqn%TAoBXl&aNOCnGaUY;_+5`}=Yb ztDF_5T3qHyovc@+>XMWLo4G^yQ_JBRu4?QAGf+0*KP^$waG%`DoNgXHrfD##&#|dS ze8(Z;XRUnuKN0UE5YhN*y6*tlJ1eaV~Tqq=}x5GyL{#(8H7XOMiV=bNT-vT@CKC~IFCoe zM;kIXh?eOgDQMf^Z;qywWLmkdjNeXX+=Rr1up7gO#OVjg6$*J_`T zm;C|C4}dB()7_gjRxCuDDJg;nqnB|$+IfWHyec4R`C`e{_@?(&@9T5x_Q2D#C5t%k zf8l@W|7!89^PV-{vqO`%9j#Y2;J&t|(~`R+b#B%-mt4Lr8rg&`mNgqxz;XsvBwgi_ zJy0(3p0cVL%e&6qw584Xz3sst+d8J>PB?p}A=V1=UKkUYybo#EZ;jbe{sfGfnS8f{ z;DQy#EKI?s({P=U^b~wBX4Ms%0t6RLkjyCxP{U?OdWx3aOk9MqGqC_$bir5yU#w>} z>|-^))e;vSsMuqVi)dA(;|{dyKyl~Fc>7fdXIH*;T&Kbz*j+MNRYdBh(WcU*stgdN TpDaPY5TJVD7DZsjOP=B{edyQ$ diff --git a/Backend/src/routes/__pycache__/booking_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/booking_routes.cpython-312.pyc index d585b0615df976c646be4d810e8fc4daf6a177ba..d3f86caa2f4bc2b4106d8902d5e593cdf403f412 100644 GIT binary patch delta 10071 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 h5qc0k1|=-!QV7V)T17LsMg>yCymNLvuorS> z2$$l@O)Ql>1uKzoTUYf8)v+9!8A?R?61)%l;SvdLDi|zlQZ} z5DiXgmCV}I^WDds>H?;=tM%NhbJBGnX!D@$se@`FFm7PLTMjm01Ym5BdY%Ku15Ds~ z6lcJAfeAHaL|~d5G7>Q1hKvuG)`pB9m{>z508FAG69lHcArk_o<9`{}ZFD>6Y|@eT z$Hq@NzFGobg&k=Itt0!-I1(@|!1Oxv{SlZjFh}rV`qP4 z9gXq-XHnyR#Z42nX?IMuuB4_Z)0Bd?LX&l2k}Q^0T1Xn@q^e~x?i99cx-`|4<{^A! z1LDUN49zG|Q`8qO?n-`@>RG1Qamt!tou;oy4MpB%n{ByH#SGfW99>>4GYE9CoUf^d zK&n?nMk~UL&0tn3l}lPyDaZ?~Un0x__z~U&ON3u5T&;!{a?9k&_Cw5)XneJH~ zl9NyV62J3@&VPxx{ zz+0~n%N+@L9*Tjr{717N&RRUow#CT$5313lmFUrG^h70k!V&$JQ$ z7l{;EAFsCdR$6;42@xNWV(W|5`0+~oxaCJAK&0^c(8jCRUbTWa4-qN4p0CFGDzQGR z36W+Z9lAbV?HH_d3|cLSgo%{69noZNgnV|Aid*Z!Kjah@bn ztFs^0VMLC=SKRehPrh;ga7_~fGlJb#ImgCj7wW9K9GxCw z>{Xdw!p>jjjlJ43i;;Ptse*w4YPVJ1vHElJ3zzD-?RD-g95FC>?Pl!NkVZA*>+RhZ z9c%AHJ-*EUh+pIu$rAq#S>%2Pa!OiBj8h|7OGrtKK6`?94zIqD&BA5H4AckZM9Rgm zs+(>|O3F%djIlCH=BSaq1aZG-sTRWDPR_u^Mx;wwJK5p2X7?rpSwN+xM}HZ^sAD&=P1KwUfc+qoIOUd9Sya#Y8= zO%fVAh6{<{6|J^gP9kGxfWgwK{|&l-k*^?A`4WQfVYqdp?^@rD?`(ySRm1(2aR2Qe zR@0-E^ypv3>e&kw_)A~d3cphg%ayRa6;{@~kDFrK?LFJAM{bTD> zry-K)|4n`?{>oYa(px;a-Q0CE07}siC^h>)N%DR5G(ZB;FNq-d9*Lok0v`r8<3oSq z?+*Xbb7yHY`r4ld*8&g3$w#3>j}l$g#OX@n^uy@Of5_fF{QKOU3!9NMPX)dSO!1QM z-uNzcZOa4Fh{)gj*067c=Rf5In0zYopp=T4 z-kbubJ+CpmU^3jvx>E`1?-rCo_7W{F_d}UsUZnAEV&a81r{(+6!7eIL8ze-dH588; zJPmgrlo;L?&4^w2_5ONCwS8&VeMDI?CS??8?KygW-&KeTj#2wa@XlgyH`N z_Gv#8Pl4Tq0zJdd0fxCt*P*j`j^jQjr?<%I&&ZL#6aPO+?`Nd{b29W8%FNj2G zu@v5HKe0JDw)xHPK9I(*KuwBro||u41cp1KJ2+Sa#3lZu{m1QVBOi1>4!2!D`f>UR X-@*AFicMeQxWf}y%M%w!_M-j^#Sjr# literal 0 HcmV?d00001 diff --git a/Backend/src/routes/__pycache__/footer_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/footer_routes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..35267d80fe75515afa44e02cf503772ad7c17eb6 GIT binary patch literal 3093 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 delta 1548 zcmZ{kU2NM_6vwaQIBC8Ur*^)aCUI8UG-*q_(E-i64wZH*Y@+*A0TH@78|k{Fx^*Z> z?Yapbc;aFA0ff+~7!s8R2#CZ3NPI}BeL$KT$tVz**u=|tvsNBZpSZ_aNj3>f{@?Te z-Q#n7?Q6d?_3=^nPcD}o(PJ|kDcp>G;U09@ZPdO&&uv5|9aj!7o93*L@!+Ah?fM%` zBbsyUlIGaVnsbu0Gr@SscVdDRop!EGv3+8`DtGfrqJR``nI(*3Up7&7khO$prh${( zaR)q3#dX8fC`p;yVD1N)$6y}C+mh!4MqYB+pb&4EOAX^U%(aGTH_W#UBY<%Uy9=Th ziUtu|NWdV077{c_u!V#S5^5n~AhB?hyv?j|N<@*A=rUJel#ch=6NuzI0y!hQJrUu` zfjLNH&*xRL#K*~Jyd13CPtVMk^G{=3!gv639;K49lHd8L2|r0zr4SkS`-B%LouudD zKKe%QqJJ(U-JK!x{!#LcKfC(31^E*-K3nCp_mIzT|22@%SvubF_;ikrx&=kLKInh1 zU$=s5L!w+0I;%qG+T7&*_tkE!ehD%up5OR zsfAM2P-=C(PYXSvdw_adi)hk8-3OGX{cGW#YPe_ppcWq1{Q%n^f&xGhN!_(bx*ADu zWwKi2fF1xEL>)b~Xm2&zyVWK z(2O4{f5kdk!B{L0m$fXGB$JOb&yyFE`?-sB>bJ_f$v>IZlQgWeg`(QH!+yBv05J1H zX;H14mBOrw;UlQC3uV=K5XQaXS0EF5VyI?3=4~1vypPIGTlQi}DbMFe@Cvo?zg_%@ zen2|QFw7rl>}ND~3(2=oq_Wc)XL<&i>@U2ivvj;oM~;s4%dvUyS!uM*A``EhfXy+L MBVCh>d70Mx7Z0FzU;qFB diff --git a/Backend/src/routes/__pycache__/page_content_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/page_content_routes.cpython-312.pyc index 2e11cc3c2598ca9fb1118c922a2070f004c6eca2..030cbc55c2865b0c8a76f4d5c9f9fe3e529e0bb1 100644 GIT binary patch literal 35501 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 G;r{}PQc0%( delta 6586 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 BE!O}5 diff --git a/Backend/src/routes/__pycache__/payment_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/payment_routes.cpython-312.pyc index bd5a77c990f20c2cd00a3bd4c570ff48563cb680..918a552cd83e44a1d92b328af29c7682cd23bdb2 100644 GIT binary patch delta 21 bcmezVjp_S0Ca%-Gyj%=GU=z2ID|a^lUZMw8 delta 21 bcmezVjp_S0Ca%-Gyj%=G&=tFpD|a^lU~vbc diff --git a/Backend/src/routes/__pycache__/room_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/room_routes.cpython-312.pyc index 3655bf7b465f228d762b2d097a91f77b3298054b..b9d6e939e5b54add17c276e5e043c86b38e5abe8 100644 GIT binary patch delta 885 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 diff --git a/Backend/src/routes/about_routes.py b/Backend/src/routes/about_routes.py new file mode 100644 index 00000000..e753b119 --- /dev/null +++ b/Backend/src/routes/about_routes.py @@ -0,0 +1,75 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +import json + +from ..config.database import get_db +from ..config.logging_config import get_logger +from ..models.page_content import PageContent, PageType + +logger = get_logger(__name__) + +router = APIRouter(prefix="/about", tags=["about"]) + + +def serialize_page_content(content: PageContent) -> dict: + """Serialize PageContent model to dictionary""" + return { + "id": content.id, + "page_type": content.page_type.value, + "title": content.title, + "subtitle": content.subtitle, + "description": content.description, + "content": content.content, + "meta_title": content.meta_title, + "meta_description": content.meta_description, + "meta_keywords": content.meta_keywords, + "og_title": content.og_title, + "og_description": content.og_description, + "og_image": content.og_image, + "canonical_url": content.canonical_url, + "story_content": content.story_content, + "values": json.loads(content.values) if content.values else None, + "features": json.loads(content.features) if content.features else None, + "about_hero_image": content.about_hero_image, + "mission": content.mission, + "vision": content.vision, + "team": json.loads(content.team) if content.team else None, + "timeline": json.loads(content.timeline) if content.timeline else None, + "achievements": json.loads(content.achievements) if content.achievements else None, + "is_active": content.is_active, + "created_at": content.created_at.isoformat() if content.created_at else None, + "updated_at": content.updated_at.isoformat() if content.updated_at else None, + } + + +@router.get("/") +async def get_about_content( + db: Session = Depends(get_db) +): + """Get about page content""" + try: + content = db.query(PageContent).filter(PageContent.page_type == PageType.ABOUT).first() + + if not content: + return { + "status": "success", + "data": { + "page_content": None + } + } + + content_dict = serialize_page_content(content) + + return { + "status": "success", + "data": { + "page_content": content_dict + } + } + except Exception as e: + logger.error(f"Error fetching about content: {str(e)}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error fetching about content: {str(e)}" + ) + diff --git a/Backend/src/routes/auth_routes.py b/Backend/src/routes/auth_routes.py index f19ea597..4dfddad4 100644 --- a/Backend/src/routes/auth_routes.py +++ b/Backend/src/routes/auth_routes.py @@ -495,6 +495,7 @@ async def upload_avatar( full_url = normalize_image_url(image_url, base_url) return { + "success": True, "status": "success", "message": "Avatar uploaded successfully", "data": { diff --git a/Backend/src/routes/banner_routes.py b/Backend/src/routes/banner_routes.py index 3328f36a..45c4e906 100644 --- a/Backend/src/routes/banner_routes.py +++ b/Backend/src/routes/banner_routes.py @@ -246,11 +246,25 @@ async def upload_banner_image( ): """Upload banner image (Admin only)""" try: + # Validate file exists + if not image: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="No file provided" + ) + # Validate file type if not image.content_type or not image.content_type.startswith('image/'): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="File must be an image" + detail=f"File must be an image. Received: {image.content_type}" + ) + + # Validate filename + if not image.filename: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Filename is required" ) # Create uploads directory @@ -258,21 +272,27 @@ async def upload_banner_image( upload_dir.mkdir(parents=True, exist_ok=True) # Generate filename - ext = Path(image.filename).suffix + ext = Path(image.filename).suffix or '.jpg' filename = f"banner-{uuid.uuid4()}{ext}" file_path = upload_dir / filename # Save file async with aiofiles.open(file_path, 'wb') as f: content = await image.read() + if not content: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="File is empty" + ) await f.write(content) # Return the image URL image_url = f"/uploads/banners/{filename}" base_url = get_base_url(request) full_url = normalize_image_url(image_url, base_url) - + return { + "success": True, "status": "success", "message": "Image uploaded successfully", "data": { diff --git a/Backend/src/routes/booking_routes.py b/Backend/src/routes/booking_routes.py index ad952ba4..24e94d6f 100644 --- a/Backend/src/routes/booking_routes.py +++ b/Backend/src/routes/booking_routes.py @@ -61,6 +61,11 @@ def _generate_invoice_email_html(invoice: dict, is_proforma: bool = False) -> st text-align: center; border-radius: 8px 8px 0 0; }} + .company-logo {{ + max-width: 150px; + max-height: 80px; + margin-bottom: 10px; + }} .content {{ background-color: #ffffff; padding: 30px; @@ -109,6 +114,8 @@ def _generate_invoice_email_html(invoice: dict, is_proforma: bool = False) -> st
+ {f'' if invoice.get('company_logo_url') else ''} + {f'

{invoice.get("company_name", "")}

' if invoice.get('company_name') else ''}

{invoice_type}

@@ -139,6 +146,7 @@ def _generate_invoice_email_html(invoice: dict, is_proforma: bool = False) -> st

Subtotal: {invoice.get('subtotal', 0):.2f}

{f'

Discount: -{invoice.get("discount_amount", 0):.2f}

' if invoice.get('discount_amount', 0) > 0 else ''} + {f'

Promotion Code: {invoice.get("promotion_code", "")}

' if invoice.get('promotion_code') else ''}

Tax: {invoice.get('tax_amount', 0):.2f}

Total Amount: {invoice.get('total_amount', 0):.2f}

Amount Paid: {invoice.get('amount_paid', 0):.2f}

@@ -729,19 +737,31 @@ async def create_booking( # Get discount from booking booking_discount = float(booking.discount_amount) if booking.discount_amount else 0.0 + # Add promotion code to invoice notes if present + invoice_notes = invoice_kwargs.get("notes", "") + if booking.promotion_code: + promotion_note = f"Promotion Code: {booking.promotion_code}" + invoice_notes = f"{promotion_note}\n{invoice_notes}".strip() if invoice_notes else promotion_note + invoice_kwargs["notes"] = invoice_notes + # Create invoices based on payment method if payment_method == "cash": # For cash bookings: create invoice for 20% deposit + proforma for 80% remaining deposit_amount = float(total_price) * 0.2 remaining_amount = float(total_price) * 0.8 + # Calculate proportional discount for partial invoices + # Deposit invoice gets 20% of the discount, proforma gets 80% + deposit_discount = booking_discount * 0.2 if booking_discount > 0 else 0.0 + proforma_discount = booking_discount * 0.8 if booking_discount > 0 else 0.0 + # Create invoice for deposit (20%) deposit_invoice = InvoiceService.create_invoice_from_booking( booking_id=booking.id, db=db, created_by_id=current_user.id, tax_rate=tax_rate, - discount_amount=booking_discount, + discount_amount=deposit_discount, due_days=30, is_proforma=False, invoice_amount=deposit_amount, @@ -754,7 +774,7 @@ async def create_booking( db=db, created_by_id=current_user.id, tax_rate=tax_rate, - discount_amount=booking_discount, + discount_amount=proforma_discount, due_days=30, is_proforma=True, invoice_amount=remaining_amount, diff --git a/Backend/src/routes/contact_content_routes.py b/Backend/src/routes/contact_content_routes.py new file mode 100644 index 00000000..b239df30 --- /dev/null +++ b/Backend/src/routes/contact_content_routes.py @@ -0,0 +1,68 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +import json + +from ..config.database import get_db +from ..config.logging_config import get_logger +from ..models.page_content import PageContent, PageType + +logger = get_logger(__name__) + +router = APIRouter(prefix="/contact-content", tags=["contact-content"]) + + +def serialize_page_content(content: PageContent) -> dict: + """Serialize PageContent model to dictionary""" + return { + "id": content.id, + "page_type": content.page_type.value, + "title": content.title, + "subtitle": content.subtitle, + "description": content.description, + "content": content.content, + "meta_title": content.meta_title, + "meta_description": content.meta_description, + "meta_keywords": content.meta_keywords, + "og_title": content.og_title, + "og_description": content.og_description, + "og_image": content.og_image, + "canonical_url": content.canonical_url, + "contact_info": json.loads(content.contact_info) if content.contact_info else None, + "map_url": content.map_url, + "is_active": content.is_active, + "created_at": content.created_at.isoformat() if content.created_at else None, + "updated_at": content.updated_at.isoformat() if content.updated_at else None, + } + + +@router.get("/") +async def get_contact_content( + db: Session = Depends(get_db) +): + """Get contact page content""" + try: + content = db.query(PageContent).filter(PageContent.page_type == PageType.CONTACT).first() + + if not content: + return { + "status": "success", + "data": { + "page_content": None + } + } + + content_dict = serialize_page_content(content) + + return { + "status": "success", + "data": { + "page_content": content_dict + } + } + except Exception as e: + logger.error(f"Error fetching contact content: {str(e)}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error fetching contact content: {str(e)}" + ) + diff --git a/Backend/src/routes/footer_routes.py b/Backend/src/routes/footer_routes.py new file mode 100644 index 00000000..ba577829 --- /dev/null +++ b/Backend/src/routes/footer_routes.py @@ -0,0 +1,63 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +import json + +from ..config.database import get_db +from ..config.logging_config import get_logger +from ..models.page_content import PageContent, PageType + +logger = get_logger(__name__) + +router = APIRouter(prefix="/footer", tags=["footer"]) + + +def serialize_page_content(content: PageContent) -> dict: + """Serialize PageContent model to dictionary""" + return { + "id": content.id, + "page_type": content.page_type.value, + "title": content.title, + "subtitle": content.subtitle, + "description": content.description, + "content": content.content, + "social_links": json.loads(content.social_links) if content.social_links else None, + "footer_links": json.loads(content.footer_links) if content.footer_links else None, + "badges": json.loads(content.badges) if content.badges else None, + "copyright_text": content.copyright_text, + "is_active": content.is_active, + "created_at": content.created_at.isoformat() if content.created_at else None, + "updated_at": content.updated_at.isoformat() if content.updated_at else None, + } + + +@router.get("/") +async def get_footer_content( + db: Session = Depends(get_db) +): + """Get footer content""" + try: + content = db.query(PageContent).filter(PageContent.page_type == PageType.FOOTER).first() + + if not content: + return { + "status": "success", + "data": { + "page_content": None + } + } + + content_dict = serialize_page_content(content) + + return { + "status": "success", + "data": { + "page_content": content_dict + } + } + except Exception as e: + logger.error(f"Error fetching footer content: {str(e)}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error fetching footer content: {str(e)}" + ) + diff --git a/Backend/src/routes/home_routes.py b/Backend/src/routes/home_routes.py new file mode 100644 index 00000000..5163c81e --- /dev/null +++ b/Backend/src/routes/home_routes.py @@ -0,0 +1,110 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +import json + +from ..config.database import get_db +from ..config.logging_config import get_logger +from ..models.page_content import PageContent, PageType + +logger = get_logger(__name__) + +router = APIRouter(prefix="/home", tags=["home"]) + + +def serialize_page_content(content: PageContent) -> dict: + """Serialize PageContent model to dictionary""" + return { + "id": content.id, + "page_type": content.page_type.value, + "title": content.title, + "subtitle": content.subtitle, + "description": content.description, + "content": content.content, + "meta_title": content.meta_title, + "meta_description": content.meta_description, + "meta_keywords": content.meta_keywords, + "og_title": content.og_title, + "og_description": content.og_description, + "og_image": content.og_image, + "canonical_url": content.canonical_url, + "hero_title": content.hero_title, + "hero_subtitle": content.hero_subtitle, + "hero_image": content.hero_image, + "amenities_section_title": content.amenities_section_title, + "amenities_section_subtitle": content.amenities_section_subtitle, + "amenities": json.loads(content.amenities) if content.amenities else None, + "testimonials_section_title": content.testimonials_section_title, + "testimonials_section_subtitle": content.testimonials_section_subtitle, + "testimonials": json.loads(content.testimonials) if content.testimonials else None, + "gallery_section_title": content.gallery_section_title, + "gallery_section_subtitle": content.gallery_section_subtitle, + "gallery_images": json.loads(content.gallery_images) if content.gallery_images else None, + "luxury_section_title": content.luxury_section_title, + "luxury_section_subtitle": content.luxury_section_subtitle, + "luxury_section_image": content.luxury_section_image, + "luxury_features": json.loads(content.luxury_features) if content.luxury_features else None, + "luxury_gallery_section_title": content.luxury_gallery_section_title, + "luxury_gallery_section_subtitle": content.luxury_gallery_section_subtitle, + "luxury_gallery": json.loads(content.luxury_gallery) if content.luxury_gallery else None, + "luxury_testimonials_section_title": content.luxury_testimonials_section_title, + "luxury_testimonials_section_subtitle": content.luxury_testimonials_section_subtitle, + "luxury_testimonials": json.loads(content.luxury_testimonials) if content.luxury_testimonials else None, + "about_preview_title": content.about_preview_title, + "about_preview_subtitle": content.about_preview_subtitle, + "about_preview_content": content.about_preview_content, + "about_preview_image": content.about_preview_image, + "stats": json.loads(content.stats) if content.stats else None, + "luxury_services_section_title": content.luxury_services_section_title, + "luxury_services_section_subtitle": content.luxury_services_section_subtitle, + "luxury_services": json.loads(content.luxury_services) if content.luxury_services else None, + "luxury_experiences_section_title": content.luxury_experiences_section_title, + "luxury_experiences_section_subtitle": content.luxury_experiences_section_subtitle, + "luxury_experiences": json.loads(content.luxury_experiences) if content.luxury_experiences else None, + "awards_section_title": content.awards_section_title, + "awards_section_subtitle": content.awards_section_subtitle, + "awards": json.loads(content.awards) if content.awards else None, + "cta_title": content.cta_title, + "cta_subtitle": content.cta_subtitle, + "cta_button_text": content.cta_button_text, + "cta_button_link": content.cta_button_link, + "cta_image": content.cta_image, + "partners_section_title": content.partners_section_title, + "partners_section_subtitle": content.partners_section_subtitle, + "partners": json.loads(content.partners) if content.partners else None, + "is_active": content.is_active, + "created_at": content.created_at.isoformat() if content.created_at else None, + "updated_at": content.updated_at.isoformat() if content.updated_at else None, + } + + +@router.get("/") +async def get_home_content( + db: Session = Depends(get_db) +): + """Get homepage content""" + try: + content = db.query(PageContent).filter(PageContent.page_type == PageType.HOME).first() + + if not content: + return { + "status": "success", + "data": { + "page_content": None + } + } + + content_dict = serialize_page_content(content) + + return { + "status": "success", + "data": { + "page_content": content_dict + } + } + except Exception as e: + logger.error(f"Error fetching home content: {str(e)}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error fetching home content: {str(e)}" + ) + diff --git a/Backend/src/routes/invoice_routes.py b/Backend/src/routes/invoice_routes.py index 72a6be88..c121b123 100644 --- a/Backend/src/routes/invoice_routes.py +++ b/Backend/src/routes/invoice_routes.py @@ -87,11 +87,38 @@ async def create_invoice( if not booking_id: raise HTTPException(status_code=400, detail="booking_id is required") + # Ensure booking_id is an integer + try: + booking_id = int(booking_id) + except (ValueError, TypeError): + raise HTTPException(status_code=400, detail="booking_id must be a valid integer") + # Check if booking exists booking = db.query(Booking).filter(Booking.id == booking_id).first() if not booking: raise HTTPException(status_code=404, detail="Booking not found") + # Prepare invoice kwargs + invoice_kwargs = { + "company_name": invoice_data.get("company_name"), + "company_address": invoice_data.get("company_address"), + "company_phone": invoice_data.get("company_phone"), + "company_email": invoice_data.get("company_email"), + "company_tax_id": invoice_data.get("company_tax_id"), + "company_logo_url": invoice_data.get("company_logo_url"), + "customer_tax_id": invoice_data.get("customer_tax_id"), + "notes": invoice_data.get("notes"), + "terms_and_conditions": invoice_data.get("terms_and_conditions"), + "payment_instructions": invoice_data.get("payment_instructions"), + } + + # Add promotion code to invoice notes if present in booking + invoice_notes = invoice_kwargs.get("notes", "") + if booking.promotion_code: + promotion_note = f"Promotion Code: {booking.promotion_code}" + invoice_notes = f"{promotion_note}\n{invoice_notes}".strip() if invoice_notes else promotion_note + invoice_kwargs["notes"] = invoice_notes + # Create invoice invoice = InvoiceService.create_invoice_from_booking( booking_id=booking_id, @@ -100,16 +127,7 @@ async def create_invoice( tax_rate=invoice_data.get("tax_rate", 0.0), discount_amount=invoice_data.get("discount_amount", 0.0), due_days=invoice_data.get("due_days", 30), - company_name=invoice_data.get("company_name"), - company_address=invoice_data.get("company_address"), - company_phone=invoice_data.get("company_phone"), - company_email=invoice_data.get("company_email"), - company_tax_id=invoice_data.get("company_tax_id"), - company_logo_url=invoice_data.get("company_logo_url"), - customer_tax_id=invoice_data.get("customer_tax_id"), - notes=invoice_data.get("notes"), - terms_and_conditions=invoice_data.get("terms_and_conditions"), - payment_instructions=invoice_data.get("payment_instructions"), + **invoice_kwargs ) return { diff --git a/Backend/src/routes/page_content_routes.py b/Backend/src/routes/page_content_routes.py index 05e1291f..bebef6eb 100644 --- a/Backend/src/routes/page_content_routes.py +++ b/Backend/src/routes/page_content_routes.py @@ -1,14 +1,21 @@ -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, status, Request, UploadFile, File from sqlalchemy.orm import Session from typing import Optional from datetime import datetime +from pathlib import Path import json +import os +import aiofiles +import uuid from ..config.database import get_db +from ..config.logging_config import get_logger from ..middleware.auth import get_current_user, authorize_roles from ..models.user import User from ..models.page_content import PageContent, PageType +logger = get_logger(__name__) + router = APIRouter(prefix="/page-content", tags=["page-content"]) @@ -40,12 +47,60 @@ async def get_all_page_contents( "social_links": json.loads(content.social_links) if content.social_links else None, "footer_links": json.loads(content.footer_links) if content.footer_links else None, "badges": json.loads(content.badges) if content.badges else None, + "copyright_text": content.copyright_text, "hero_title": content.hero_title, "hero_subtitle": content.hero_subtitle, "hero_image": content.hero_image, "story_content": content.story_content, "values": json.loads(content.values) if content.values else None, "features": json.loads(content.features) if content.features else None, + "about_hero_image": content.about_hero_image, + "mission": content.mission, + "vision": content.vision, + "team": json.loads(content.team) if content.team else None, + "timeline": json.loads(content.timeline) if content.timeline else None, + "achievements": json.loads(content.achievements) if content.achievements else None, + "amenities_section_title": content.amenities_section_title, + "amenities_section_subtitle": content.amenities_section_subtitle, + "amenities": json.loads(content.amenities) if content.amenities else None, + "testimonials_section_title": content.testimonials_section_title, + "testimonials_section_subtitle": content.testimonials_section_subtitle, + "testimonials": json.loads(content.testimonials) if content.testimonials else None, + "gallery_section_title": content.gallery_section_title, + "gallery_section_subtitle": content.gallery_section_subtitle, + "gallery_images": json.loads(content.gallery_images) if content.gallery_images else None, + "luxury_section_title": content.luxury_section_title, + "luxury_section_subtitle": content.luxury_section_subtitle, + "luxury_section_image": content.luxury_section_image, + "luxury_features": json.loads(content.luxury_features) if content.luxury_features else None, + "luxury_gallery_section_title": content.luxury_gallery_section_title, + "luxury_gallery_section_subtitle": content.luxury_gallery_section_subtitle, + "luxury_gallery": json.loads(content.luxury_gallery) if content.luxury_gallery else None, + "luxury_testimonials_section_title": content.luxury_testimonials_section_title, + "luxury_testimonials_section_subtitle": content.luxury_testimonials_section_subtitle, + "luxury_testimonials": json.loads(content.luxury_testimonials) if content.luxury_testimonials else None, + "about_preview_title": content.about_preview_title, + "about_preview_subtitle": content.about_preview_subtitle, + "about_preview_content": content.about_preview_content, + "about_preview_image": content.about_preview_image, + "stats": json.loads(content.stats) if content.stats else None, + "luxury_services_section_title": content.luxury_services_section_title, + "luxury_services_section_subtitle": content.luxury_services_section_subtitle, + "luxury_services": json.loads(content.luxury_services) if content.luxury_services else None, + "luxury_experiences_section_title": content.luxury_experiences_section_title, + "luxury_experiences_section_subtitle": content.luxury_experiences_section_subtitle, + "luxury_experiences": json.loads(content.luxury_experiences) if content.luxury_experiences else None, + "awards_section_title": content.awards_section_title, + "awards_section_subtitle": content.awards_section_subtitle, + "awards": json.loads(content.awards) if content.awards else None, + "cta_title": content.cta_title, + "cta_subtitle": content.cta_subtitle, + "cta_button_text": content.cta_button_text, + "cta_button_link": content.cta_button_link, + "cta_image": content.cta_image, + "partners_section_title": content.partners_section_title, + "partners_section_subtitle": content.partners_section_subtitle, + "partners": json.loads(content.partners) if content.partners else None, "is_active": content.is_active, "created_at": content.created_at.isoformat() if content.created_at else None, "updated_at": content.updated_at.isoformat() if content.updated_at else None, @@ -65,6 +120,104 @@ async def get_all_page_contents( ) +def get_base_url(request: Request) -> str: + """Get base URL for image normalization""" + return os.getenv("SERVER_URL") or f"http://{request.headers.get('host', 'localhost:8000')}" + + +def normalize_image_url(image_url: str, base_url: str) -> str: + """Normalize image URL to absolute URL""" + if not image_url: + return image_url + if image_url.startswith('http://') or image_url.startswith('https://'): + return image_url + if image_url.startswith('/'): + return f"{base_url}{image_url}" + return f"{base_url}/{image_url}" + + +@router.post("/upload", dependencies=[Depends(authorize_roles("admin"))]) +async def upload_page_content_image( + request: Request, + image: UploadFile = File(...), + current_user: User = Depends(authorize_roles("admin")), +): + """Upload page content image (Admin only)""" + try: + logger.info(f"Upload request received: filename={image.filename}, content_type={image.content_type}") + + # Validate file exists + if not image: + logger.error("No file provided in upload request") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="No file provided" + ) + + # Validate file type + if not image.content_type or not image.content_type.startswith('image/'): + logger.error(f"Invalid file type: {image.content_type}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"File must be an image. Received: {image.content_type}" + ) + + # Validate filename + if not image.filename: + logger.error("No filename provided in upload request") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Filename is required" + ) + + # Create uploads directory + upload_dir = Path(__file__).parent.parent.parent / "uploads" / "page-content" + upload_dir.mkdir(parents=True, exist_ok=True) + logger.info(f"Upload directory: {upload_dir}") + + # Generate filename + ext = Path(image.filename).suffix or '.jpg' + filename = f"page-content-{uuid.uuid4()}{ext}" + file_path = upload_dir / filename + + # Save file + async with aiofiles.open(file_path, 'wb') as f: + content = await image.read() + if not content: + logger.error("Empty file uploaded") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="File is empty" + ) + await f.write(content) + logger.info(f"File saved successfully: {file_path}, size: {len(content)} bytes") + + # Return the image URL + image_url = f"/uploads/page-content/{filename}" + base_url = get_base_url(request) + full_url = normalize_image_url(image_url, base_url) + + logger.info(f"Upload successful: {image_url}") + return { + "success": True, + "status": "success", + "message": "Image uploaded successfully", + "data": { + "image_url": image_url, + "full_url": full_url + } + } + except HTTPException as e: + logger.error(f"HTTPException in upload: {e.detail}") + raise + except Exception as e: + logger.error(f"Unexpected error uploading image: {str(e)}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error uploading image: {str(e)}" + ) + + @router.get("/{page_type}") async def get_page_content( page_type: PageType, @@ -102,12 +255,60 @@ async def get_page_content( "social_links": json.loads(content.social_links) if content.social_links else None, "footer_links": json.loads(content.footer_links) if content.footer_links else None, "badges": json.loads(content.badges) if content.badges else None, + "copyright_text": content.copyright_text, "hero_title": content.hero_title, "hero_subtitle": content.hero_subtitle, "hero_image": content.hero_image, "story_content": content.story_content, "values": json.loads(content.values) if content.values else None, "features": json.loads(content.features) if content.features else None, + "about_hero_image": content.about_hero_image, + "mission": content.mission, + "vision": content.vision, + "team": json.loads(content.team) if content.team else None, + "timeline": json.loads(content.timeline) if content.timeline else None, + "achievements": json.loads(content.achievements) if content.achievements else None, + "amenities_section_title": content.amenities_section_title, + "amenities_section_subtitle": content.amenities_section_subtitle, + "amenities": json.loads(content.amenities) if content.amenities else None, + "testimonials_section_title": content.testimonials_section_title, + "testimonials_section_subtitle": content.testimonials_section_subtitle, + "testimonials": json.loads(content.testimonials) if content.testimonials else None, + "gallery_section_title": content.gallery_section_title, + "gallery_section_subtitle": content.gallery_section_subtitle, + "gallery_images": json.loads(content.gallery_images) if content.gallery_images else None, + "luxury_section_title": content.luxury_section_title, + "luxury_section_subtitle": content.luxury_section_subtitle, + "luxury_section_image": content.luxury_section_image, + "luxury_features": json.loads(content.luxury_features) if content.luxury_features else None, + "luxury_gallery_section_title": content.luxury_gallery_section_title, + "luxury_gallery_section_subtitle": content.luxury_gallery_section_subtitle, + "luxury_gallery": json.loads(content.luxury_gallery) if content.luxury_gallery else None, + "luxury_testimonials_section_title": content.luxury_testimonials_section_title, + "luxury_testimonials_section_subtitle": content.luxury_testimonials_section_subtitle, + "luxury_testimonials": json.loads(content.luxury_testimonials) if content.luxury_testimonials else None, + "about_preview_title": content.about_preview_title, + "about_preview_subtitle": content.about_preview_subtitle, + "about_preview_content": content.about_preview_content, + "about_preview_image": content.about_preview_image, + "stats": json.loads(content.stats) if content.stats else None, + "luxury_services_section_title": content.luxury_services_section_title, + "luxury_services_section_subtitle": content.luxury_services_section_subtitle, + "luxury_services": json.loads(content.luxury_services) if content.luxury_services else None, + "luxury_experiences_section_title": content.luxury_experiences_section_title, + "luxury_experiences_section_subtitle": content.luxury_experiences_section_subtitle, + "luxury_experiences": json.loads(content.luxury_experiences) if content.luxury_experiences else None, + "awards_section_title": content.awards_section_title, + "awards_section_subtitle": content.awards_section_subtitle, + "awards": json.loads(content.awards) if content.awards else None, + "cta_title": content.cta_title, + "cta_subtitle": content.cta_subtitle, + "cta_button_text": content.cta_button_text, + "cta_button_link": content.cta_button_link, + "cta_image": content.cta_image, + "partners_section_title": content.partners_section_title, + "partners_section_subtitle": content.partners_section_subtitle, + "partners": json.loads(content.partners) if content.partners else None, "is_active": content.is_active, "created_at": content.created_at.isoformat() if content.created_at else None, "updated_at": content.updated_at.isoformat() if content.updated_at else None, @@ -151,6 +352,12 @@ async def create_or_update_page_content( story_content: Optional[str] = None, values: Optional[str] = None, features: Optional[str] = None, + about_hero_image: Optional[str] = None, + mission: Optional[str] = None, + vision: Optional[str] = None, + team: Optional[str] = None, + timeline: Optional[str] = None, + achievements: Optional[str] = None, is_active: bool = True, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) @@ -263,6 +470,18 @@ async def create_or_update_page_content( existing_content.values = values if features is not None: existing_content.features = features + if about_hero_image is not None: + existing_content.about_hero_image = about_hero_image + if mission is not None: + existing_content.mission = mission + if vision is not None: + existing_content.vision = vision + if team is not None: + existing_content.team = team + if timeline is not None: + existing_content.timeline = timeline + if achievements is not None: + existing_content.achievements = achievements if is_active is not None: existing_content.is_active = is_active @@ -308,6 +527,12 @@ async def create_or_update_page_content( story_content=story_content, values=values, features=features, + about_hero_image=about_hero_image, + mission=mission, + vision=vision, + team=team, + timeline=timeline, + achievements=achievements, is_active=is_active, ) @@ -362,7 +587,10 @@ async def update_page_content( for key, value in page_data.items(): if hasattr(existing_content, key): # Handle JSON fields - convert dict/list to JSON string - if key in ["contact_info", "social_links", "footer_links", "badges", "values", "features"] and value is not None: + if key in ["contact_info", "social_links", "footer_links", "badges", "values", "features", + "amenities", "testimonials", "gallery_images", "stats", "luxury_features", + "luxury_gallery", "luxury_testimonials", "luxury_services", "luxury_experiences", + "awards", "partners", "team", "timeline", "achievements"] and value is not None: if isinstance(value, str): # Already a string, validate it's valid JSON try: @@ -403,12 +631,60 @@ async def update_page_content( "social_links": json.loads(existing_content.social_links) if existing_content.social_links else None, "footer_links": json.loads(existing_content.footer_links) if existing_content.footer_links else None, "badges": json.loads(existing_content.badges) if existing_content.badges else None, + "copyright_text": existing_content.copyright_text, "hero_title": existing_content.hero_title, "hero_subtitle": existing_content.hero_subtitle, "hero_image": existing_content.hero_image, "story_content": existing_content.story_content, "values": json.loads(existing_content.values) if existing_content.values else None, "features": json.loads(existing_content.features) if existing_content.features else None, + "about_hero_image": existing_content.about_hero_image, + "mission": existing_content.mission, + "vision": existing_content.vision, + "team": json.loads(existing_content.team) if existing_content.team else None, + "timeline": json.loads(existing_content.timeline) if existing_content.timeline else None, + "achievements": json.loads(existing_content.achievements) if existing_content.achievements else None, + "amenities_section_title": existing_content.amenities_section_title, + "amenities_section_subtitle": existing_content.amenities_section_subtitle, + "amenities": json.loads(existing_content.amenities) if existing_content.amenities else None, + "testimonials_section_title": existing_content.testimonials_section_title, + "testimonials_section_subtitle": existing_content.testimonials_section_subtitle, + "testimonials": json.loads(existing_content.testimonials) if existing_content.testimonials else None, + "gallery_section_title": existing_content.gallery_section_title, + "gallery_section_subtitle": existing_content.gallery_section_subtitle, + "gallery_images": json.loads(existing_content.gallery_images) if existing_content.gallery_images else None, + "luxury_section_title": existing_content.luxury_section_title, + "luxury_section_subtitle": existing_content.luxury_section_subtitle, + "luxury_section_image": existing_content.luxury_section_image, + "luxury_features": json.loads(existing_content.luxury_features) if existing_content.luxury_features else None, + "luxury_gallery_section_title": existing_content.luxury_gallery_section_title, + "luxury_gallery_section_subtitle": existing_content.luxury_gallery_section_subtitle, + "luxury_gallery": json.loads(existing_content.luxury_gallery) if existing_content.luxury_gallery else None, + "luxury_testimonials_section_title": existing_content.luxury_testimonials_section_title, + "luxury_testimonials_section_subtitle": existing_content.luxury_testimonials_section_subtitle, + "luxury_testimonials": json.loads(existing_content.luxury_testimonials) if existing_content.luxury_testimonials else None, + "about_preview_title": existing_content.about_preview_title, + "about_preview_subtitle": existing_content.about_preview_subtitle, + "about_preview_content": existing_content.about_preview_content, + "about_preview_image": existing_content.about_preview_image, + "stats": json.loads(existing_content.stats) if existing_content.stats else None, + "luxury_services_section_title": existing_content.luxury_services_section_title, + "luxury_services_section_subtitle": existing_content.luxury_services_section_subtitle, + "luxury_services": json.loads(existing_content.luxury_services) if existing_content.luxury_services else None, + "luxury_experiences_section_title": existing_content.luxury_experiences_section_title, + "luxury_experiences_section_subtitle": existing_content.luxury_experiences_section_subtitle, + "luxury_experiences": json.loads(existing_content.luxury_experiences) if existing_content.luxury_experiences else None, + "awards_section_title": existing_content.awards_section_title, + "awards_section_subtitle": existing_content.awards_section_subtitle, + "awards": json.loads(existing_content.awards) if existing_content.awards else None, + "cta_title": existing_content.cta_title, + "cta_subtitle": existing_content.cta_subtitle, + "cta_button_text": existing_content.cta_button_text, + "cta_button_link": existing_content.cta_button_link, + "cta_image": existing_content.cta_image, + "partners_section_title": existing_content.partners_section_title, + "partners_section_subtitle": existing_content.partners_section_subtitle, + "partners": json.loads(existing_content.partners) if existing_content.partners else None, "is_active": existing_content.is_active, "updated_at": existing_content.updated_at.isoformat() if existing_content.updated_at else None, } diff --git a/Backend/src/routes/room_routes.py b/Backend/src/routes/room_routes.py index 7b07d220..d8a93732 100644 --- a/Backend/src/routes/room_routes.py +++ b/Backend/src/routes/room_routes.py @@ -694,18 +694,24 @@ async def upload_room_images( image_urls = [] for image in images: # Validate file type - if not image.content_type.startswith('image/'): + if not image.content_type or not image.content_type.startswith('image/'): + continue + + # Validate filename + if not image.filename: continue # Generate filename import uuid - ext = Path(image.filename).suffix + ext = Path(image.filename).suffix or '.jpg' filename = f"room-{uuid.uuid4()}{ext}" file_path = upload_dir / filename # Save file async with aiofiles.open(file_path, 'wb') as f: content = await image.read() + if not content: + continue await f.write(content) image_urls.append(f"/uploads/rooms/{filename}") @@ -717,6 +723,7 @@ async def upload_room_images( db.commit() return { + "success": True, "status": "success", "message": "Images uploaded successfully", "data": {"images": updated_images} diff --git a/Backend/src/routes/system_settings_routes.py b/Backend/src/routes/system_settings_routes.py index 2ee87661..bb87387b 100644 --- a/Backend/src/routes/system_settings_routes.py +++ b/Backend/src/routes/system_settings_routes.py @@ -1127,6 +1127,7 @@ async def upload_company_logo( full_url = normalize_image_url(image_url, base_url) return { + "success": True, "status": "success", "message": "Logo uploaded successfully", "data": { @@ -1235,6 +1236,7 @@ async def upload_company_favicon( full_url = normalize_image_url(image_url, base_url) return { + "success": True, "status": "success", "message": "Favicon uploaded successfully", "data": { diff --git a/Backend/src/services/__pycache__/auth_service.cpython-312.pyc b/Backend/src/services/__pycache__/auth_service.cpython-312.pyc index c4839a65b49ae0a3930a8f9ef0b274cf86981dd4..723a4c40261fc23130b8385caab2d3a47f723f18 100644 GIT binary patch delta 1936 zcmbtUYitx%6ux(6XZHPYAAN5-?Y5P&wUF8t7NMov!h&78(1L_u#I>`QS+={)?6eDI zwjl9GRU(BOQzMTUVh{r)l#z%2Fp!W)AZb^rG2LL|FJfYXt;R^uAKts$0UCmToFCsg zbIy0p{m!|&^VeB8djVSRn9T-&pFOP=!d&O1rL)YE(E`u~AeaC$C6=S*<^r^t)1j(5 zC$ZIW$ip-xQ%R%5kV^9K)H#^<4nY}4wP=goLFoXB`aC8{m(KtYHKEGJN+bsyxrRTN zF|M2ZOLQEaHO?()H;P787Cn-gv<<`O{J2$CZ zymNqVtjTkx%B>DFz9UQ8AT3k-Mwg?b_L3^;Wi=oh&Rf;;5gbe#_2BWvH7#6Rlgf^^ zkj0?i+%DACY)0WKw+546L6fW9`ev0x6qd+(-d2nwvhn;2=jePYZNlU=xem)X3v5G2 zo7^;rcI6`fG#cyqqFl*BDPMeo?gbtotyMc$gK;P}q|LZZ2RYMdQ!~+Zs8ZvAc5*g* z9Pkb)aICV>p*`cASylc&XDlmf8{@;_n80(9fv^zc5+XMkP9{?ckso<6nCKVcTv(DK1A`Km5~ST+JQ05>vR6n- zk+_sES;dW1E+)Q>n+Wl6pCk-NxS?c3#FYB>zP`i|t`U+PFC@cn#Ugye25xgK5>7_A zzC>KYd@eaOIG7NnhLMKF9dkzu3qneuNxWs zB}MzOlhvleVqA##XZ1pIS6&<1UhRj6(I?eA(W7b~_L^$;HLk@!(OP_UyuIBRv>Gue-Hzt^2{cV6Fi zJ&-94&X_hm&6{I^&3R<>{n5w7)@wu8`ZJ~LXH0<{vFolqm@x&>e9cCO+N|r=x?3wU zrM)wzt%|Rl@o1al5yv^+1v*4um z|B++P4J%_?f{?vP8eLMdJtkOkhxj!FJQyD7Ru3fjp;)9NzR>R_;V2~n`X0tA?K|bPdpc1wRE}Jbsl}~m$0vw$2D6SK5RIO!43zT7ghZQzqAl`gYZ)e6lF=o6l$L{HJHB6OH5%1=QHMjlJ}3}lyTeeP(MYUV zk^)hmHOMt;S;CK(Z7z=-Py-pml0kiGiUhMpJ)UeRhah&g*ANT{?wxUQ+ir$^80&Cr zGLUE>hR0aaDxRh!**fn#!0}LD!ghjmR!p zb8ImO%wyt=LA4=qm`*3`3_-W85+wd7y=AHXCsj#DvX=kS;f>WYdfXHV)}EEyNQ!=tfy(mN)3$72%{=`l$}SIvE>!c><_bB=5fkNU$fgYWpC z*Ic0PFa8$d?%w+#M$r|A_BLux08vKa^Kg6ZO2>L%@!ceW~)A;)85u}L|AQ})e<|e<48Pj;7MP; zR7?8u0v9P)4%Th6Fo%x%YPgD_`Vsh9S*ZUDcFmF5r1Jy9>&l z6b2~F;e6L2-Cga3EXBw1URM*pLc^=*?tT+aDhu6T>tI27InoI3b&?2FN_3UdGKEhm z?4xiK*Zb!ARcfx_>HgF3o^rGQAzOEaM&>EppztAuMFMxV^5l4OSSn2PC5_&~+XMHT zBcL^08F|3ad-0{>{du%_Z^*@a>y@)RM*6}Zg~|tm%CFHAz~?B@veI_S1g`I>O@AiR o9s+-tw2h95lgZ%@=|_C=xk0DIgW-3kfC)2GS}~J--Bc+52Tu<`&j0`b diff --git a/Backend/src/services/__pycache__/invoice_service.cpython-312.pyc b/Backend/src/services/__pycache__/invoice_service.cpython-312.pyc index cc230db6afdbdb3007e22970023c433a7632122d..f10429655d7a697087148dce1d58a2efd6198cab 100644 GIT binary patch delta 4119 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 delta 3570 zcma)9X>3&26~6b)zP#Dq?3>4UW6Ur!_IP2nu~{bp1BnT012!&?f(Nk4cmRdmk&8qd zC{b(oR<&D9F;#!0 zo%5aLoqO)N=gz!+o!*?GwqJ>&nZR>yTXcA8%Wc~YqcCA9y=Ckd!pv~qTBt!KKFn~7 zE^$S1#KX;&RqZ0vo&#s5kxl0;5JE>t(Z)n(JBRkFu4pgREqM_ln=cS%=CdX@PdlU@ z2eUH=`WKzdDbwRYLh=oT`m%MdvCKVw7hQ#TS;{r@g|48ZD2+5T2e@wmcd1a%H^#%} zqDwY{!Uf`-HBzfeg_g3XyndL-7G~vCwlV@# zVD4lRP!|({N=yUP&1^tDObgV@>_C0Y0o2c&Km*JLG|2QoL(G_K=PfHGXQ&(%&~O>$ ze-<57)pNUTnR7;~ENd#3`^$R%ip{36U=x6@r4ie;5AQ{fDWCrFvE4Pvu(`W^T-m+EVmz|RL3Q@DyR7DHt`uR|IPz!eb z*y+BdW9w&;r;XZ{R~b=P83C6UDvPTybL%RrjFwk9UO5z$y!Hz%feLx>cosz%P5;Nh zsv1ioa}OrUA7eI0$QK+z|FUSJFB@=9c`d>L3*4vRCdX_O@8j zJx}5rN}srggwhUQE2VPj6+ai~OvxTHQrgD-;R2rw|C846S0hucb5+d)Gpz zc}pT~nZ^VzBDu^@B))3+4A`*}<>Q%6rNHVllunf*7sI z0HU=(?pwwRLxt00gGWYApUl63@K2E3Xm0I%=$@>s8ycA+*BEA%|B zr5w8jvmT#3ma3P()9Y!fh0Iof^aWSP7V>R+SU5@!3kM(YmwZ8iQM^g;_50o@N})oy z8AjX;INQ%WQFKJ0=o--R+f7e-161w(-2euLe)rTNxYF? z+Sy){7l_~%GLk zUlDrjCgd-pk#@Lnj3n-vX6pIXDy zM;D`Q3(+=32eP6kVqm&yF_>5gCKLlAMk0DA&Q6U_jw_}VpV-krQ~a$R^*|q}Jk7>7 zqn(S0zWL6@%;vk9%`-ZTnp}t`@A*^jNs5F9ZU{YpcCj;iw=+A_Fl$(hwJyY3?*%jO zjVYcKm$S7H^BdV(HIE?wAR87!DgM3+|0Nq4#~3jphNibKhSLk-v{H-63L-|Q&o0(< zEYx&7tV5)pi0U|PU5Kxc`8%5!GEh^X#0y8`&>&+s@#3b{Y&XQWOt> z7)>2id6F^#K*>J(CHn9&^zTPqqSrPCbDO-M*&N%QF!NJ8dHj3WX4jDX2+5C;+(u$U z@+y*1BvU-GYwh^Ug0Aypw@=X^^Ql!*bjUzQ`D3%1$)aLVMI({?ib-Y7Bv_+ZRMtu) zuOh0fjd+8KU1c34P&@1SzFl#uqMMj)7Y|QlE*`rSedIyt?;4_aDT1mM&ge>??>a!+ zTXg1GpF-euvwyyBer$f_8v~C}RIMKhz~!efZTy$f7pL3ek^Im3+1+m@-$C>a68s~h l>X#RYVj9SQj2ljl%BN2p>B|3-_wU(f-=nAIM+B)F^MAkQU6}v? diff --git a/Backend/src/services/__pycache__/paypal_service.cpython-312.pyc b/Backend/src/services/__pycache__/paypal_service.cpython-312.pyc index d222bf0bd0b12bf5ed28c75be3965588be1247f7..9d2b7f130161c3bee0988175d5c06f8d673b8b79 100644 GIT binary patch delta 1612 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 GDd<1)47}z5 delta 1687 zcmaJ>ZA?>V6n;-{dugFiuvUah1xhLvT3VIN34XK_8KBOO#0SrKhJd{u|unXxnu@Jueq-$4hdLittJNdj4c8_PerS9zHC|6Xu08 z@yJZ8pbuZh{=97TlonZvRm)2D&ex2^GS$qmL^^hs2H_g9rJ1LJN^{k%3`{Y`R5y2i zsYTQ4nTpf8sVFv|9C+SfwdthFC`e0VC2Fy}S;thXS{yz@wVfrsOj^c$Urkrg7+03} z>DBqT*;7&Q|GpJ!F<$aGbvN}G$X}eT*BS;U1rM@VsfnrL)Uk1iZ8p{wSux`Eq`bjf zDOE-M*ftY=MMgb^c->ndP;!4>GWlUy72L#6=Q_;N%9F;8>^0V->(0iVWlfNSx67&_ zfU|ub{~wY$=GyD(f-bg4sApZ^WXhm+k|HJ_jd>KC7E4vC2Y>eEz$)zV*)7$|Elp8H z=}o-kb3zU2mfBa8Iu!l?DE8H_C~LHt%jT)hbNRSydRd=tdJ+d1Z_%^YgWZ0+!K*^} zTiox+Ee4hpynDS}pj^=U;2n3zypl~aN4Cbw)Uxm$>?yGC^C_Qc4%Mrbmec+#8!c4s z{@WYAr{$(s{+`z=543o?E^l9m`CppaTRPQGW3jDMeXpKPJIx|sib<;^tyldo+YSI9 zK=Pd0J31?LfqO$-TzFQD-+lcU!1;K%ViQ0lK2Uj0myx!8hZKr*dUth(Wce7LTAr6b zJf~@EC>oNRl~7Y#boY+7)=*PudvjZ)X`8Y=A|J=0 zg@D&`l6N+&YYxdZoXGd^S-`=)=NfY`KR5?h2A7y8_#J-Ae%sFyg|+byg2z(OS*yTL z@e{Q^fF>NOTMR{U-RcTn9ig41}eVfNOGlsH3hgWz+52*DQwF9%7OLl_ihYc<7DxpqU$G@?11sVIWs(GxgL2syr@9mC+kYo?d)`TI+ z5+bwZo!EF}O(JC~HnP-=L#;gv4{bZ_m@p-CzY0v8kPjK^DhEp dict: """Login user with optional MFA verification""" + # Normalize email (lowercase and strip whitespace) + email = email.lower().strip() if email else "" + if not email: + raise ValueError("Invalid email or password") + # Find user with role and password user = db.query(User).filter(User.email == email).first() if not user: + logger.warning(f"Login attempt with non-existent email: {email}") raise ValueError("Invalid email or password") + # Check if user is active + if not user.is_active: + logger.warning(f"Login attempt for inactive user: {email}") + raise ValueError("Account is disabled. Please contact support.") + # Load role user.role = db.query(Role).filter(Role.id == user.role_id).first() # Check password if not self.verify_password(password, user.password): + logger.warning(f"Login attempt with invalid password for user: {email}") raise ValueError("Invalid email or password") # Check if MFA is enabled diff --git a/Backend/src/services/invoice_service.py b/Backend/src/services/invoice_service.py index 00a192a8..73fe769e 100644 --- a/Backend/src/services/invoice_service.py +++ b/Backend/src/services/invoice_service.py @@ -87,10 +87,20 @@ class InvoiceService: # Calculate amounts - subtotal will be recalculated after adding items # Initial subtotal is booking total (room + services) or invoice_amount if specified + booking_total = float(booking.total_price) if invoice_amount is not None: subtotal = float(invoice_amount) + # For partial invoices, ensure discount is proportional + # If discount_amount seems too large (greater than subtotal), recalculate proportionally + if invoice_amount < booking_total and discount_amount > 0: + # Check if discount seems disproportionate (greater than 50% of subtotal suggests it's the full discount) + if discount_amount > subtotal * 0.5: + # Recalculate proportionally from booking's original discount + proportion = float(invoice_amount) / booking_total + original_discount = float(booking.discount_amount) if booking.discount_amount else discount_amount + discount_amount = original_discount * proportion else: - subtotal = float(booking.total_price) + subtotal = booking_total # Calculate tax and total amounts tax_amount = (subtotal - discount_amount) * (tax_rate / 100) @@ -388,6 +398,14 @@ class InvoiceService: @staticmethod def invoice_to_dict(invoice: Invoice) -> Dict[str, Any]: """Convert invoice model to dictionary""" + # Extract promotion code from notes if present + promotion_code = None + if invoice.notes and "Promotion Code:" in invoice.notes: + try: + promotion_code = invoice.notes.split("Promotion Code:")[1].split("\n")[0].strip() + except: + pass + return { "id": invoice.id, "invoice_number": invoice.invoice_number, @@ -419,6 +437,7 @@ class InvoiceService: "terms_and_conditions": invoice.terms_and_conditions, "payment_instructions": invoice.payment_instructions, "is_proforma": invoice.is_proforma if hasattr(invoice, 'is_proforma') else False, + "promotion_code": promotion_code, "items": [ { "id": item.id, diff --git a/Backend/src/services/paypal_service.py b/Backend/src/services/paypal_service.py index d613741b..24c62f79 100644 --- a/Backend/src/services/paypal_service.py +++ b/Backend/src/services/paypal_service.py @@ -505,10 +505,36 @@ class PayPalService: except Exception as email_error: logger.error(f"Failed to send booking confirmation email: {str(email_error)}") + # Send invoice email if payment is completed and invoice is now paid + from ..utils.mailer import send_email + from ..services.invoice_service import InvoiceService + from ..routes.booking_routes import _generate_invoice_email_html + + # Load user for email + from ..models.user import User + user = db.query(User).filter(User.id == booking.user_id).first() + + for invoice in invoices: + if invoice.status == InvoiceStatus.paid and invoice.balance_due <= 0: + try: + invoice_dict = InvoiceService.invoice_to_dict(invoice) + invoice_html = _generate_invoice_email_html(invoice_dict, is_proforma=invoice.is_proforma) + invoice_type = "Proforma Invoice" if invoice.is_proforma else "Invoice" + if user: + await send_email( + to=user.email, + subject=f"{invoice_type} {invoice.invoice_number} - Payment Confirmed", + html=invoice_html + ) + logger.info(f"{invoice_type} {invoice.invoice_number} sent to {user.email}") + except Exception as email_error: + logger.error(f"Failed to send invoice email: {str(email_error)}") + # Send invoice email if payment is completed and invoice is now paid from ..utils.mailer import send_email from ..services.invoice_service import InvoiceService from ..models.invoice import InvoiceStatus + from ..routes.booking_routes import _generate_invoice_email_html # Load user for email from ..models.user import User diff --git a/Backend/src/services/stripe_service.py b/Backend/src/services/stripe_service.py index b63fa304..fd94ea8e 100644 --- a/Backend/src/services/stripe_service.py +++ b/Backend/src/services/stripe_service.py @@ -404,6 +404,7 @@ class StripeService: # Send invoice email if payment is completed and invoice is now paid from ..utils.mailer import send_email from ..services.invoice_service import InvoiceService + from ..routes.booking_routes import _generate_invoice_email_html # Load user for email from ..models.user import User diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx index 90398ab3..cd843de1 100644 --- a/Frontend/src/App.tsx +++ b/Frontend/src/App.tsx @@ -58,6 +58,8 @@ const ResetPasswordPage = lazy(() => import('./pages/auth/ResetPasswordPage')); // Lazy load admin pages const AdminDashboardPage = lazy(() => import('./pages/admin/DashboardPage')); +const InvoiceManagementPage = lazy(() => import('./pages/admin/InvoiceManagementPage')); +const PaymentManagementPage = lazy(() => import('./pages/admin/PaymentManagementPage')); const UserManagementPage = lazy(() => import('./pages/admin/UserManagementPage')); const PageContentDashboardPage = lazy(() => import('./pages/admin/PageContentDashboard')); const AnalyticsDashboardPage = lazy(() => import('./pages/admin/AnalyticsDashboardPage')); @@ -321,6 +323,18 @@ function App() { path="settings" element={} /> + } + /> + } + /> + } + /> {/* 404 Route */} diff --git a/Frontend/src/components/admin/IconPicker.tsx b/Frontend/src/components/admin/IconPicker.tsx new file mode 100644 index 00000000..88d720cd --- /dev/null +++ b/Frontend/src/components/admin/IconPicker.tsx @@ -0,0 +1,225 @@ +import React, { useState, useMemo } from 'react'; +import * as LucideIcons from 'lucide-react'; +import { Search, X } from 'lucide-react'; + +// Popular icons for hotel/luxury content +const popularIcons = [ + 'Sparkles', 'Star', 'Award', 'Shield', 'Heart', 'Crown', 'Gem', + 'Zap', 'Wifi', 'Coffee', 'Utensils', 'Bed', 'Home', 'MapPin', + 'Phone', 'Mail', 'Calendar', 'Clock', 'Users', 'UserCheck', + 'Car', 'Plane', 'Ship', 'Train', 'Bike', 'Umbrella', 'Sun', + 'Moon', 'Cloud', 'Droplet', 'Flame', 'TreePine', 'Mountain', + 'Palette', 'Music', 'Camera', 'Video', 'Gamepad2', 'Book', + 'Briefcase', 'ShoppingBag', 'Gift', 'Trophy', 'Medal', 'Ribbon', + 'CheckCircle', 'XCircle', 'AlertCircle', 'Info', 'HelpCircle', + 'Lock', 'Key', 'Eye', 'EyeOff', 'Bell', 'Settings', 'Menu', + 'Grid', 'List', 'Layout', 'Maximize', 'Minimize', 'ArrowRight', + 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'ChevronRight', 'ChevronLeft', + 'ChevronUp', 'ChevronDown', 'Plus', 'Minus', 'X', 'Check', + 'Trash2', 'Edit', 'Save', 'Download', 'Upload', 'Share', + 'Copy', 'Scissors', 'FileText', 'Image', 'Film', 'Headphones', + 'Mic', 'Radio', 'Tv', 'Monitor', 'Laptop', 'Smartphone', + 'Tablet', 'Watch', 'Printer', 'HardDrive', 'Database', 'Server', + 'Cloud', 'Globe', 'Compass', 'Navigation', 'Map', 'Route', + 'Building', 'Building2', 'Hotel', 'Home', 'Store', 'ShoppingCart', + 'CreditCard', 'DollarSign', 'Euro', 'PoundSterling', 'Yen', + 'Bitcoin', 'TrendingUp', 'TrendingDown', 'BarChart', 'LineChart', + 'PieChart', 'Activity', 'Target', 'Flag', 'Tag', 'Bookmark', + 'Folder', 'File', 'Archive', 'Inbox', 'Send', 'Inbox', + 'MessageSquare', 'MessageCircle', 'MessageCircleMore', 'PhoneCall', + 'Video', 'Voicemail', 'AtSign', 'Hash', 'Link', 'ExternalLink', + 'Unlink', 'Code', 'Terminal', 'Command', 'Slash', 'Brackets', + 'Braces', 'Parentheses', 'Percent', 'Infinity', 'Pi', 'Sigma', + 'Omega', 'Alpha', 'Beta', 'Gamma', 'Delta', 'Theta', 'Lambda', + 'Mu', 'Nu', 'Xi', 'Omicron', 'Rho', 'Tau', 'Upsilon', 'Phi', + 'Chi', 'Psi' +]; + +interface IconPickerProps { + value?: string; + onChange: (iconName: string) => void; + label?: string; +} + +const IconPicker: React.FC = ({ value, onChange, label = 'Icon' }) => { + const [isOpen, setIsOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + + // Get all available Lucide icons + const allIcons = useMemo(() => { + const icons: string[] = []; + const excludedNames = new Set([ + 'createLucideIcon', + 'Icon', + 'default', + 'lucideReact', + 'lucide', + 'createElement', + 'Fragment', + 'forwardRef', + 'memo' + ]); + + for (const iconName in LucideIcons) { + // Skip non-icon exports + if ( + excludedNames.has(iconName) || + iconName.startsWith('_') || + iconName[0] !== iconName[0].toUpperCase() // Lucide icons start with uppercase + ) { + continue; + } + + const iconComponent = (LucideIcons as any)[iconName]; + // Check if it's a React component (function) + if (typeof iconComponent === 'function') { + icons.push(iconName); + } + } + + const sorted = icons.sort(); + return sorted; + }, []); + + // Filter icons based on search + const filteredIcons = useMemo(() => { + if (!searchQuery.trim()) { + // Show popular icons first, then others + const popular = popularIcons.filter(icon => allIcons.includes(icon)); + const others = allIcons.filter(icon => !popularIcons.includes(icon)); + return [...popular, ...others]; + } + return allIcons.filter((icon) => + icon.toLowerCase().includes(searchQuery.toLowerCase()) + ); + }, [searchQuery, allIcons]); + + const selectedIcon = value && (LucideIcons as any)[value] ? (LucideIcons as any)[value] : null; + + const handleIconSelect = (iconName: string) => { + onChange(iconName); + setIsOpen(false); + setSearchQuery(''); + }; + + return ( +
+ + + + {isOpen && ( + <> +
setIsOpen(false)} + /> +
+
+
+ + setSearchQuery(e.target.value)} + placeholder="Search icons..." + className="w-full pl-10 pr-4 py-2 border-2 border-gray-200 rounded-lg focus:border-purple-400 focus:ring-4 focus:ring-purple-100 outline-none" + autoFocus + /> + {searchQuery && ( + + )} +
+
+
+ {filteredIcons.length > 0 ? ( + <> + {!searchQuery.trim() && ( +
+

+ Showing {filteredIcons.length} icons. Popular icons appear first. +

+
+ )} +
+ {filteredIcons.slice(0, searchQuery.trim() ? 500 : 300).map((iconName) => { + const IconComponent = (LucideIcons as any)[iconName]; + if (!IconComponent) return null; + + const isSelected = value === iconName; + const isPopular = !searchQuery.trim() && popularIcons.includes(iconName); + + try { + return ( + + ); + } catch (error) { + console.warn(`Failed to render icon: ${iconName}`, error); + return null; + } + })} +
+ {filteredIcons.length > (searchQuery.trim() ? 500 : 300) && ( +
+ Showing first {searchQuery.trim() ? 500 : 300} of {filteredIcons.length} icons. {!searchQuery.trim() && 'Use search to find more.'} +
+ )} + + ) : ( +
+

No icons found matching "{searchQuery}"

+

Try a different search term

+
+ )} +
+
+ + )} +
+ ); +}; + +export default IconPicker; + diff --git a/Frontend/src/components/layout/Footer.tsx b/Frontend/src/components/layout/Footer.tsx index 5f098593..51a18886 100644 --- a/Frontend/src/components/layout/Footer.tsx +++ b/Frontend/src/components/layout/Footer.tsx @@ -37,7 +37,7 @@ const Footer: React.FC = () => { useEffect(() => { const fetchPageContent = async () => { try { - const response = await pageContentService.getPageContent('footer'); + const response = await pageContentService.getFooterContent(); if (response.status === 'success' && response.data?.page_content) { setPageContent(response.data.page_content); } @@ -344,7 +344,12 @@ const Footer: React.FC = () => { {/* Copyright - Enhanced */}
- © {new Date().getFullYear()} Luxury Hotel. All rights reserved. + {(() => { + const currentYear = new Date().getFullYear(); + const copyrightText = pageContent?.copyright_text || '© {YEAR} Luxury Hotel. All rights reserved.'; + // Replace {YEAR} placeholder with current year + return copyrightText.replace(/{YEAR}/g, currentYear.toString()); + })()}
Privacy diff --git a/Frontend/src/components/rooms/BannerCarousel.tsx b/Frontend/src/components/rooms/BannerCarousel.tsx index 4b09199f..1c2b8995 100644 --- a/Frontend/src/components/rooms/BannerCarousel.tsx +++ b/Frontend/src/components/rooms/BannerCarousel.tsx @@ -82,11 +82,11 @@ const BannerCarousel: React.FC = ({ index === currentIndex ? 'opacity-100 z-0 pointer-events-auto' : 'opacity-0 z-0 pointer-events-none' }`} > - {banner.link ? ( + {banner.link_url ? ( = ({ }} /> - {/* Title - Positioned at top when search form is present */} + {/* Title - Positioned higher up on the banner */} {currentBanner.title && (
= ({ - {/* Animated decorative line below title */} + {/* Description text - centered below title */} + {currentBanner.description && ( +

+ {currentBanner.description} +

+ )} + + {/* Animated decorative line below title/description */}
diff --git a/Frontend/src/data/luxuryContentSeed.ts b/Frontend/src/data/luxuryContentSeed.ts new file mode 100644 index 00000000..efcda53c --- /dev/null +++ b/Frontend/src/data/luxuryContentSeed.ts @@ -0,0 +1,65 @@ +// Seed data for luxury hotel content +export const luxuryContentSeed = { + home: { + luxury_section_title: 'Experience Unparalleled Luxury', + luxury_section_subtitle: 'Where elegance meets comfort in every detail', + luxury_section_image: '', + luxury_features: [ + { + icon: 'Sparkles', + title: 'Premium Amenities', + description: 'World-class facilities designed for your comfort and relaxation' + }, + { + icon: 'Crown', + title: 'Royal Service', + description: 'Dedicated concierge service available 24/7 for all your needs' + }, + { + icon: 'Award', + title: 'Award-Winning', + description: 'Recognized for excellence in hospitality and guest satisfaction' + }, + { + icon: 'Shield', + title: 'Secure & Private', + description: 'Your privacy and security are our top priorities' + }, + { + icon: 'Heart', + title: 'Personalized Care', + description: 'Tailored experiences crafted just for you' + }, + { + icon: 'Gem', + title: 'Luxury Design', + description: 'Elegantly designed spaces with attention to every detail' + } + ], + luxury_gallery: [], + luxury_testimonials: [ + { + name: 'Sarah Johnson', + title: 'Business Executive', + quote: 'An absolutely stunning experience. The attention to detail and level of service exceeded all expectations.', + image: '' + }, + { + name: 'Michael Chen', + title: 'Travel Enthusiast', + quote: 'The epitome of luxury. Every moment was perfect, from check-in to check-out.', + image: '' + }, + { + name: 'Emma Williams', + title: 'Luxury Traveler', + quote: 'This hotel redefines what luxury means. I will definitely return.', + image: '' + } + ], + about_preview_title: 'About Our Luxury Hotel', + about_preview_content: 'Discover a world of refined elegance and exceptional service. Our hotel combines timeless luxury with modern amenities to create an unforgettable experience.', + about_preview_image: '' + } +}; + diff --git a/Frontend/src/pages/AboutPage.tsx b/Frontend/src/pages/AboutPage.tsx index 661ebae6..4e880692 100644 --- a/Frontend/src/pages/AboutPage.tsx +++ b/Frontend/src/pages/AboutPage.tsx @@ -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 ( -
+
{/* Hero Section */} -
-
-
-
-
-
- +
+ {pageContent?.about_hero_image && ( +
+ About Hero +
+
+
+ )} + {!pageContent?.about_hero_image && ( +
+
+
+
+ )} +
+
+ {!pageContent?.about_hero_image && ( +
+
+
+
+ +
+
+
+ )} +
+
+
-

- {pageContent?.title || 'About Luxury Hotel'} +

+ + {pageContent?.title || 'About Luxury Hotel'} +

-

+

{pageContent?.subtitle || pageContent?.description || 'Where Excellence Meets Unforgettable Experiences'}

+
+
+
+
+
{/* Our Story Section */} -
-
-
-
-

+
+
+
+
+
+
+ Our Heritage +
+

Our Story

-
+
+
+
+
+
-
+
{pageContent?.story_content ? ( -
') }} /> +
') }} + /> ) : ( <> -

+

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.

-

+

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.

-

+

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 = () => {

+
{/* Values Section */} -
-
-
-
-

+
+
+
+
+
+
+ Core Principles +
+

Our Values

-
+
+
+
+
+
-
+
{values.map((value, index) => (
-
- +
+
+
+ {(() => { + const ValueIcon = getIconComponent(value.icon); + return ; + })()} +
+

+ {value.title} +

+

+ {value.description} +

-

- {value.title} -

-

- {value.description} -

))}
@@ -206,60 +281,303 @@ const AboutPage: React.FC = () => {
{/* Features Section */} -
-
-
-
-

+
+
+
+
+
+
+ Excellence Defined +
+

Why Choose Us

-
+
+
+
+
+
-
- {features.map((feature, index) => ( -
-
- +
+ {features.map((feature, index) => { + const FeatureIcon = getIconComponent(feature.icon); + return ( +
+
+
+
+ +
+

+ {feature.title} +

+

+ {feature.description} +

+
-

- {feature.title} -

-

- {feature.description} -

-
- ))} + ); + })}
+ {/* Mission & Vision Section */} + {(pageContent?.mission || pageContent?.vision) && ( +
+
+
+
+
+
+
+ {pageContent.mission && ( +
+
+
+
+
+

Our Mission

+
+

{pageContent.mission}

+
+
+ )} + {pageContent.vision && ( +
+
+
+
+
+

Our Vision

+
+

{pageContent.vision}

+
+
+ )} +
+
+
+ )} + + {/* Team Section */} + {team && team.length > 0 && ( +
+
+
+
+
+ Meet The Experts +
+

+ Our Team +

+
+
+
+
+
+
+
+ {team.map((member: any, index: number) => ( +
+
+ {member.image && ( +
+ {member.name} +
+
+ )} +
+

{member.name}

+

{member.role}

+ {member.bio &&

{member.bio}

} + {member.social_links && ( +
+ {member.social_links.linkedin && ( + + + + )} + {member.social_links.twitter && ( + + + + )} +
+ )} +
+
+ ))} +
+
+
+
+ )} + + {/* Timeline Section */} + {timeline && timeline.length > 0 && ( +
+
+
+
+
+
+ Our Journey +
+

+ Our History +

+
+
+
+
+
+
+
+
+
+ {timeline.map((event: any, index: number) => ( +
+
+
+
+
+
{event.year}
+
+
+

{event.title}

+

{event.description}

+ {event.image && ( +
+ {event.title} +
+ )} +
+
+
+ ))} +
+
+
+
+
+ )} + + {/* Achievements Section */} + {achievements && achievements.length > 0 && ( +
+
+
+
+
+
+ Recognition +
+

+ Achievements & Awards +

+
+
+
+
+
+
+
+ {achievements.map((achievement: any, index: number) => { + const AchievementIcon = getIconComponent(achievement.icon); + return ( +
+
+
+
+
+ +
+ {achievement.year && ( +
{achievement.year}
+ )} +
+

{achievement.title}

+

{achievement.description}

+ {achievement.image && ( +
+ {achievement.title} +
+ )} +
+
+ ); + })} +
+
+
+
+ )} + {/* Contact Section */} -
-
-
-
-

+
+
+
+
+
+
+ Connect With Us +
+

Get In Touch

-
-

+

+
+
+
+
+

We'd love to hear from you. Contact us for reservations or inquiries.

-
-
-
- +
+
+
+
-

+

Address

-

+

{displayAddress .split('\n').map((line, i) => ( @@ -269,40 +587,41 @@ const AboutPage: React.FC = () => { ))}

-
-
- +
+
+
-

+

Phone

-

- +

+ {displayPhone}

-
-
- +
+
+
-

+

Email

-

- +

+ {displayEmail}

-
+
- Explore Our Rooms - + Explore Our Rooms + +
diff --git a/Frontend/src/pages/ContactPage.tsx b/Frontend/src/pages/ContactPage.tsx index 32668000..619729da 100644 --- a/Frontend/src/pages/ContactPage.tsx +++ b/Frontend/src/pages/ContactPage.tsx @@ -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); diff --git a/Frontend/src/pages/HomePage.tsx b/Frontend/src/pages/HomePage.tsx index 723abf87..15ff2648 100644 --- a/Frontend/src/pages/HomePage.tsx +++ b/Frontend/src/pages/HomePage.tsx @@ -3,7 +3,13 @@ import { Link } from 'react-router-dom'; import { ArrowRight, AlertCircle, + Star, + X, + ChevronLeft, + ChevronRight, + ZoomIn, } from 'lucide-react'; +import * as LucideIcons from 'lucide-react'; import { BannerCarousel, BannerSkeleton, @@ -32,6 +38,33 @@ const HomePage: React.FC = () => { const [isLoadingNewest, setIsLoadingNewest] = useState(true); const [isLoadingContent, setIsLoadingContent] = useState(true); const [error, setError] = useState(null); + const [lightboxOpen, setLightboxOpen] = useState(false); + const [lightboxIndex, setLightboxIndex] = useState(0); + const [lightboxImages, setLightboxImages] = useState([]); + + // Handle keyboard navigation for lightbox + useEffect(() => { + if (!lightboxOpen) return; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + setLightboxOpen(false); + } else if (e.key === 'ArrowLeft' && lightboxImages.length > 1) { + setLightboxIndex((prev) => (prev === 0 ? lightboxImages.length - 1 : prev - 1)); + } else if (e.key === 'ArrowRight' && lightboxImages.length > 1) { + setLightboxIndex((prev) => (prev === lightboxImages.length - 1 ? 0 : prev + 1)); + } + }; + + window.addEventListener('keydown', handleKeyDown); + // Prevent body scroll when lightbox is open + document.body.style.overflow = 'hidden'; + + return () => { + window.removeEventListener('keydown', handleKeyDown); + document.body.style.overflow = 'unset'; + }; + }, [lightboxOpen, lightboxImages.length]); // Combine featured and newest rooms, removing duplicates const combinedRooms = useMemo(() => { @@ -57,22 +90,118 @@ const HomePage: React.FC = () => { const fetchPageContent = async () => { try { setIsLoadingContent(true); - const response = await pageContentService.getPageContent('home'); + const response = await pageContentService.getHomeContent(); if (response.status === 'success' && response.data?.page_content) { - setPageContent(response.data.page_content); + const content = response.data.page_content; + + // Parse JSON fields if they come as strings (backward compatibility) + if (typeof content.features === 'string') { + try { + content.features = JSON.parse(content.features); + } catch (e) { + content.features = []; + } + } + if (typeof content.amenities === 'string') { + try { + content.amenities = JSON.parse(content.amenities); + } catch (e) { + content.amenities = []; + } + } + if (typeof content.testimonials === 'string') { + try { + content.testimonials = JSON.parse(content.testimonials); + } catch (e) { + content.testimonials = []; + } + } + if (typeof content.gallery_images === 'string') { + try { + content.gallery_images = JSON.parse(content.gallery_images); + } catch (e) { + content.gallery_images = []; + } + } + if (typeof content.stats === 'string') { + try { + content.stats = JSON.parse(content.stats); + } catch (e) { + content.stats = []; + } + } + // Parse luxury fields + if (typeof content.luxury_features === 'string') { + try { + content.luxury_features = JSON.parse(content.luxury_features); + } catch (e) { + content.luxury_features = []; + } + } + if (typeof content.luxury_gallery === 'string') { + try { + const parsed = JSON.parse(content.luxury_gallery); + content.luxury_gallery = Array.isArray(parsed) ? parsed.filter(img => img && typeof img === 'string' && img.trim() !== '') : []; + } catch (e) { + content.luxury_gallery = []; + } + } + // Ensure luxury_gallery is an array and filter out empty values + if (Array.isArray(content.luxury_gallery)) { + content.luxury_gallery = content.luxury_gallery.filter(img => img && typeof img === 'string' && img.trim() !== ''); + } else { + content.luxury_gallery = []; + } + if (typeof content.luxury_testimonials === 'string') { + try { + content.luxury_testimonials = JSON.parse(content.luxury_testimonials); + } catch (e) { + content.luxury_testimonials = []; + } + } + if (typeof content.luxury_services === 'string') { + try { + content.luxury_services = JSON.parse(content.luxury_services); + } catch (e) { + content.luxury_services = []; + } + } + if (typeof content.luxury_experiences === 'string') { + try { + content.luxury_experiences = JSON.parse(content.luxury_experiences); + } catch (e) { + content.luxury_experiences = []; + } + } + if (typeof content.awards === 'string') { + try { + content.awards = JSON.parse(content.awards); + } catch (e) { + content.awards = []; + } + } + if (typeof content.partners === 'string') { + try { + content.partners = JSON.parse(content.partners); + } catch (e) { + content.partners = []; + } + } + + setPageContent(content); // Update document title and meta tags - if (response.data.page_content.meta_title) { - document.title = response.data.page_content.meta_title; + if (content.meta_title) { + document.title = content.meta_title; } - if (response.data.page_content.meta_description) { + if (content.meta_description) { let metaDescription = document.querySelector('meta[name="description"]'); if (!metaDescription) { metaDescription = document.createElement('meta'); metaDescription.setAttribute('name', 'description'); document.head.appendChild(metaDescription); } - metaDescription.setAttribute('content', response.data.page_content.meta_description); + metaDescription.setAttribute('content', content.meta_description); } } } catch (err: any) { @@ -215,27 +344,34 @@ const HomePage: React.FC = () => { )}
-
+
+ {/* Subtle background pattern */} +
+
{/* Featured & Newest Rooms Section - Combined Carousel */} -
+
{/* Section Header - Centered */}
-

+
+
+
+

{pageContent?.hero_title || 'Featured & Newest Rooms'}

-

+

{pageContent?.hero_subtitle || pageContent?.description || 'Discover our most popular accommodations and latest additions'}

{/* View All Rooms Button - Golden, Centered */} -
+
+ View All Rooms - +
@@ -300,79 +436,818 @@ const HomePage: React.FC = () => { )}
- {/* Features Section */} -
-
- {/* Decorative gold accent */} -
- -
-
-
- 🏨 -
-

- Easy Booking -

-

- Search and book rooms with just a few clicks -

-
+ {/* Features Section - Dynamic from page content */} + {(() => { + // Filter out empty features (no title or description) + const validFeatures = pageContent?.features?.filter( + (f: any) => f && (f.title || f.description) + ) || []; + + return (validFeatures.length > 0 || !pageContent) && ( +
+
+ {/* Decorative gold accents */} +
+
+ + {/* Subtle background pattern */} +
+ +
+ {validFeatures.length > 0 ? ( + validFeatures.map((feature: any, index: number) => ( +
+ {feature.image ? ( +
+ {feature.title +
+ ) : ( +
+ {feature.icon && (LucideIcons as any)[feature.icon] ? ( + React.createElement((LucideIcons as any)[feature.icon], { + className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md' + }) + ) : ( + + )} +
+ )} + {feature.title && ( +

+ {feature.title} +

+ )} + {feature.description && ( +

+ {feature.description} +

+ )} +
+ )) + ) : ( + <> +
+
+ 🏨 +
+

+ Easy Booking +

+

+ Search and book rooms with just a few clicks +

+
-
-
- 💰 -
-

- Best Prices -

-

- Best price guarantee in the market -

-
+
+
+ 💰 +
+

+ Best Prices +

+

+ Best price guarantee in the market +

+
-
-
- 🎧 +
+
+ 🎧 +
+

+ 24/7 Support +

+

+ Support team always ready to serve +

+
+ + )}
-

- 24/7 Support -

-

- Support team always ready to serve +

+
+ ); + })()} + + {/* Luxury Section - Dynamic from page content */} + {(pageContent?.luxury_section_title || (pageContent?.luxury_features && pageContent.luxury_features.length > 0)) && ( +
+
+
+
+
+

+ {pageContent.luxury_section_title || 'Experience Unparalleled Luxury'} +

+ {pageContent.luxury_section_subtitle && ( +

+ {pageContent.luxury_section_subtitle}

+ )} +
+ {pageContent.luxury_section_image && ( +
+
+ Luxury Experience +
+
+
+ )} + {pageContent.luxury_features && pageContent.luxury_features.length > 0 && ( +
+ {pageContent.luxury_features.map((feature, index) => ( +
+
+
+ {feature.icon && (LucideIcons as any)[feature.icon] ? ( + React.createElement((LucideIcons as any)[feature.icon], { + className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md' + }) + ) : ( + + )} +
+

+ {feature.title} +

+

+ {feature.description} +

+
+ ))} +
+ )} +
+ )} + + {/* Luxury Gallery Section - Dynamic from page content */} + {pageContent?.luxury_gallery && Array.isArray(pageContent.luxury_gallery) && pageContent.luxury_gallery.length > 0 && ( +
+
+
+
+
+

+ {pageContent.luxury_gallery_section_title || 'Luxury Gallery'} +

+ {pageContent.luxury_gallery_section_subtitle && ( +

+ {pageContent.luxury_gallery_section_subtitle} +

+ )} +
+
+ {pageContent.luxury_gallery.map((image, index) => { + // Normalize image URL - if it's a relative path, prepend the API URL + const imageUrl = image && typeof image === 'string' + ? (image.startsWith('http://') || image.startsWith('https://') + ? image + : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${image.startsWith('/') ? image : '/' + image}`) + : ''; + + if (!imageUrl) return null; + + return ( +
{ + const normalizedImages = pageContent.luxury_gallery + .map(img => { + if (!img || typeof img !== 'string') return null; + return img.startsWith('http://') || img.startsWith('https://') + ? img + : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${img.startsWith('/') ? img : '/' + img}`; + }) + .filter(Boolean) as string[]; + setLightboxImages(normalizedImages); + setLightboxIndex(index); + setLightboxOpen(true); + }} + > + {`Luxury { + console.error(`Failed to load luxury gallery image: ${imageUrl}`); + (e.target as HTMLImageElement).style.display = 'none'; + }} + /> +
+
+ +
+
+
+
+ ); + })} +
+
+ )} + + {/* Luxury Testimonials Section - Dynamic from page content */} + {pageContent?.luxury_testimonials && pageContent.luxury_testimonials.length > 0 && ( +
+
+
+
+
+

+ {pageContent.luxury_testimonials_section_title || 'Guest Experiences'} +

+ {pageContent.luxury_testimonials_section_subtitle && ( +

+ {pageContent.luxury_testimonials_section_subtitle} +

+ )} +

+ Hear from our valued guests about their luxury stay +

+
+
+ {pageContent.luxury_testimonials.map((testimonial, index) => ( +
+
+
+ {testimonial.image ? ( +
+ {testimonial.name} +
+
+ ) : ( +
+ {testimonial.name.charAt(0).toUpperCase()} +
+ )} +
+

{testimonial.name}

+ {testimonial.title && ( +

{testimonial.title}

+ )} +
+
+
+
"
+

{testimonial.quote}

+
+
+ ))} +
+
+ )} + + {/* Stats Section - Dynamic from page content */} + {pageContent?.stats && Array.isArray(pageContent.stats) && pageContent.stats.length > 0 && ( +
+
+ {/* Decorative elements */} +
+
+ +
+ {pageContent.stats.map((stat, index) => ( +
+ {stat?.icon && ( +
+ {stat.icon && (LucideIcons as any)[stat.icon] ? ( + React.createElement((LucideIcons as any)[stat.icon], { + className: 'w-8 h-8 md:w-10 md:h-10 text-[#d4af37] drop-shadow-md' + }) + ) : ( + {stat.icon} + )} +
+ )} + {stat?.number && ( +
+ {stat.number} +
+ )} + {stat?.label && ( +
+ {stat.label} +
+ )} +
+ ))} +
+
+
+ )} + + {/* Amenities Section - Dynamic from page content */} + {pageContent?.amenities && pageContent.amenities.length > 0 && ( +
+
+
+
+
+

+ {pageContent.amenities_section_title || 'Luxury Amenities'} +

+

+ {pageContent.amenities_section_subtitle || 'Experience world-class amenities designed for your comfort'} +

+
+
+ {pageContent.amenities.map((amenity, index) => ( +
+
+ {amenity.image ? ( +
+ {amenity.title} +
+ ) : ( +
+ {amenity.icon && (LucideIcons as any)[amenity.icon] ? ( + React.createElement((LucideIcons as any)[amenity.icon], { + className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md' + }) + ) : ( + + )} +
+ )} +

+ {amenity.title} +

+

+ {amenity.description} +

+
+ ))} +
+
+ )} + + {/* Testimonials Section - Dynamic from page content */} + {pageContent?.testimonials && pageContent.testimonials.length > 0 && ( +
+
+
+
+
+

+ {pageContent.testimonials_section_title || 'Guest Testimonials'} +

+

+ {pageContent.testimonials_section_subtitle || 'See what our guests say about their experience'} +

+
+
+ {pageContent.testimonials.map((testimonial, index) => ( +
+
+
+ {testimonial.image ? ( +
+ {testimonial.name} +
+
+ ) : ( +
+ {testimonial.name.charAt(0).toUpperCase()} +
+ )} +
+

{testimonial.name}

+

{testimonial.role}

+
+
+
+ {[...Array(5)].map((_, i) => ( + + ))} +
+
+
"
+

"{testimonial.comment}"

+
+
+ ))} +
+
+ )} + + + {/* About Preview Section - Dynamic from page content */} + {(pageContent?.about_preview_title || pageContent?.about_preview_content) && ( +
+
+ {/* Decorative gold accents */} +
+
+ +
+ {pageContent.about_preview_image && ( +
+ About us +
+
+ )} +
+
+
+
+

+ {pageContent.about_preview_title || 'About Our Hotel'} +

+ {pageContent.about_preview_subtitle && ( +

+ {pageContent.about_preview_subtitle} +

+ )} + {pageContent.about_preview_content && ( +

+ {pageContent.about_preview_content} +

+ )} + + + Learn More + + +
+
+
+
+ )} + + {/* Luxury Services Section */} + {pageContent?.luxury_services && pageContent.luxury_services.length > 0 && ( +
+
+
+
+
+

+ {pageContent.luxury_services_section_title || 'Luxury Services'} +

+ {pageContent.luxury_services_section_subtitle && ( +

+ {pageContent.luxury_services_section_subtitle} +

+ )} +
+
+ {pageContent.luxury_services.map((service: any, index: number) => ( +
+
+ {service.image ? ( +
+ {service.title} +
+ ) : ( +
+ {service.icon && (LucideIcons as any)[service.icon] ? ( + React.createElement((LucideIcons as any)[service.icon], { + className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md' + }) + ) : ( + + )} +
+ )} +

+ {service.title} +

+

+ {service.description} +

+
+ ))} +
+
+ )} + + {/* Luxury Experiences Section */} + {pageContent?.luxury_experiences && pageContent.luxury_experiences.length > 0 && ( +
+
+
+
+
+

+ {pageContent.luxury_experiences_section_title || 'Unique Experiences'} +

+ {pageContent.luxury_experiences_section_subtitle && ( +

+ {pageContent.luxury_experiences_section_subtitle} +

+ )} +
+
+ {pageContent.luxury_experiences.map((experience: any, index: number) => ( +
+
+ {experience.image ? ( +
+ {experience.title} +
+ ) : ( +
+ {experience.icon && (LucideIcons as any)[experience.icon] ? ( + React.createElement((LucideIcons as any)[experience.icon], { + className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md' + }) + ) : ( + + )} +
+ )} +

+ {experience.title} +

+

+ {experience.description} +

+
+ ))} +
+
+ )} + + {/* Awards Section */} + {pageContent?.awards && pageContent.awards.length > 0 && ( +
+
+
+
+
+

+ {pageContent.awards_section_title || 'Awards & Recognition'} +

+ {pageContent.awards_section_subtitle && ( +

+ {pageContent.awards_section_subtitle} +

+ )} +
+
+ {pageContent.awards.map((award: any, index: number) => ( +
+
+ {award.image ? ( +
+ {award.title} +
+ ) : ( +
+ {award.icon && (LucideIcons as any)[award.icon] ? ( + React.createElement((LucideIcons as any)[award.icon], { + className: 'w-8 h-8 md:w-10 md:h-10 text-[#d4af37] drop-shadow-md' + }) + ) : ( + 🏆 + )} +
+ )} + {award.year && ( +
{award.year}
+ )} +

+ {award.title} +

+ {award.description && ( +

+ {award.description} +

+ )} +
+ ))} +
+
+ )} + + {/* CTA Section */} + {(pageContent?.cta_title || pageContent?.cta_subtitle) && ( +
+
+ {pageContent.cta_image && ( +
+ CTA Background +
+ )} +
+
+
+
+
+

+ {pageContent.cta_title} +

+ {pageContent.cta_subtitle && ( +

+ {pageContent.cta_subtitle} +

+ )} + {pageContent.cta_button_text && pageContent.cta_button_link && ( + + + {pageContent.cta_button_text} + + + )} +
+
+
+ )} + + {/* Partners Section */} + {pageContent?.partners && pageContent.partners.length > 0 && ( +
+
+
+
+
+

+ {pageContent.partners_section_title || 'Our Partners'} +

+ {pageContent.partners_section_subtitle && ( +

+ {pageContent.partners_section_subtitle} +

+ )} +
+
+ {pageContent.partners.map((partner: any, index: number) => ( +
+ {partner.link ? ( + + {partner.logo ? ( + {partner.name} + ) : ( + {partner.name} + )} + + ) : ( + <> + {partner.logo ? ( + {partner.name} + ) : ( + {partner.name} + )} + + )} +
+ ))} +
+
+ )} + +
+
+ + {/* Luxury Gallery Lightbox Modal */} + {lightboxOpen && lightboxImages.length > 0 && ( +
setLightboxOpen(false)} + > + {/* Close Button */} + + + {/* Previous Button */} + {lightboxImages.length > 1 && ( + + )} + + {/* Next Button */} + {lightboxImages.length > 1 && ( + + )} + + {/* Image Container */} +
e.stopPropagation()} + > +
+ {`Luxury { + console.error(`Failed to load lightbox image: ${lightboxImages[lightboxIndex]}`); + }} + /> + + {/* Image Counter */} + {lightboxImages.length > 1 && ( +
+ {lightboxIndex + 1} / {lightboxImages.length} +
+ )} + + {/* Thumbnail Strip (if more than 1 image) */} + {lightboxImages.length > 1 && lightboxImages.length <= 10 && ( +
+ {lightboxImages.map((thumb, idx) => ( + + ))} +
+ )}
-
-
+ )} ); }; diff --git a/Frontend/src/pages/admin/BookingManagementPage.tsx b/Frontend/src/pages/admin/BookingManagementPage.tsx index 9c7bd13f..ce037b6f 100644 --- a/Frontend/src/pages/admin/BookingManagementPage.tsx +++ b/Frontend/src/pages/admin/BookingManagementPage.tsx @@ -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([]); const [loading, setLoading] = useState(true); const [selectedBooking, setSelectedBooking] = useState(null); const [showDetailModal, setShowDetailModal] = useState(false); const [updatingBookingId, setUpdatingBookingId] = useState(null); const [cancellingBookingId, setCancellingBookingId] = useState(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 = { pending: { @@ -622,7 +651,24 @@ const BookingManagementPage: React.FC = () => {
{/* Modal Footer */} -
+
+
- {/* Search Booking */} + {/* Date and Search Filters */}
-

1. Search booking

-
-
- +
+
+ 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" />
- +
+ +
+ 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" + /> + +
+
+
+ +
- {/* Booking Info */} + {/* Check-ins and Check-outs Lists */} + {!booking && ( +
+ {/* Check-ins for Today */} +
+
+

+ + Check-ins for {formatDate(selectedDate)} +

+ + {checkInBookings.length} + +
+ {loadingBookings ? ( +
+ +
+ ) : checkInBookings.length === 0 ? ( +
+ +

No check-ins scheduled for this date

+
+ ) : ( +
+ {checkInBookings.map((b) => ( +
handleSelectBooking(b.booking_number)} + className="p-4 border border-gray-200 rounded-lg hover:border-blue-500 hover:shadow-md cursor-pointer transition-all" + > +
+
+
{b.booking_number}
+
{b.user?.full_name}
+
+ {b.room?.room_type?.name} • {formatCurrency(b.total_price)} +
+
+
+ + {b.status} + +
+
+
+ ))} +
+ )} +
+ + {/* Check-outs for Today */} +
+
+

+ + Check-outs for {formatDate(selectedDate)} +

+ + {checkOutBookings.length} + +
+ {loadingBookings ? ( +
+ +
+ ) : checkOutBookings.length === 0 ? ( +
+ +

No check-outs scheduled for this date

+
+ ) : ( +
+ {checkOutBookings.map((b) => ( +
handleSelectBooking(b.booking_number)} + className="p-4 border border-gray-200 rounded-lg hover:border-orange-500 hover:shadow-md cursor-pointer transition-all" + > +
+
+
{b.booking_number}
+
{b.user?.full_name}
+
+ {b.room?.room_type?.name} • {formatCurrency(b.total_price)} +
+
+
+ + {b.status} + +
+
+
+ ))} +
+ )} +
+
+ )} + + {/* Booking Info and Check-in Form */} {booking && ( <>
-

- - 2. Booking Information -

+
+

+ + 2. Booking Information +

+ +
@@ -299,7 +541,6 @@ const CheckInPage: React.FC = () => {
{(() => { - // 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 = () => {
)} - - {/* Empty State */} - {!booking && !searching && ( -
- -

- No booking selected -

-

- Please enter booking number above to start check-in process -

-
- )}
); }; diff --git a/Frontend/src/pages/admin/InvoiceManagementPage.tsx b/Frontend/src/pages/admin/InvoiceManagementPage.tsx index 3d897a7a..e7d6245d 100644 --- a/Frontend/src/pages/admin/InvoiceManagementPage.tsx +++ b/Frontend/src/pages/admin/InvoiceManagementPage.tsx @@ -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 = () => {

Manage and track all invoices

@@ -190,6 +191,9 @@ const InvoiceManagementPage: React.FC = () => { Amount + + Promotion + Status @@ -235,6 +239,25 @@ const InvoiceManagementPage: React.FC = () => { Due: {formatCurrency(invoice.balance_due)}
)} + {invoice.discount_amount > 0 && ( +
+ Discount: -{formatCurrency(invoice.discount_amount)} +
+ )} + + + {invoice.promotion_code ? ( + + {invoice.promotion_code} + + ) : ( + + )} + {invoice.is_proforma && ( +
+ Proforma +
+ )} @@ -274,7 +297,7 @@ const InvoiceManagementPage: React.FC = () => { }) ) : ( - +

No invoices found

diff --git a/Frontend/src/pages/admin/PageContentDashboard.tsx b/Frontend/src/pages/admin/PageContentDashboard.tsx index d52089e8..d33edbec 100644 --- a/Frontend/src/pages/admin/PageContentDashboard.tsx +++ b/Frontend/src/pages/admin/PageContentDashboard.tsx @@ -7,13 +7,6 @@ import { Search, Save, Globe, - Facebook, - Twitter, - Instagram, - Linkedin, - Youtube, - MapPin, - Phone, X, Plus, Trash2, @@ -31,6 +24,8 @@ import { pageContentService, PageContent, PageType, UpdatePageContentData, banne import { toast } from 'react-toastify'; import Loading from '../../components/common/Loading'; import { ConfirmationDialog } from '../../components/common'; +import IconPicker from '../../components/admin/IconPicker'; +import { luxuryContentSeed } from '../../data/luxuryContentSeed'; type ContentTab = 'overview' | 'home' | 'contact' | 'about' | 'footer' | 'seo'; @@ -62,7 +57,7 @@ const PageContentDashboard: React.FC = () => { title: '', description: '', image_url: '', - link: '', + link_url: '', position: 'home', display_order: 0, is_active: true, @@ -113,6 +108,21 @@ const PageContentDashboard: React.FC = () => { } }; + // Helper function to normalize arrays (handle both arrays and JSON strings) + const normalizeArray = (value: any): any[] => { + if (!value) return []; + if (Array.isArray(value)) return value; + if (typeof value === 'string') { + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } + } + return []; + }; + const initializeFormData = (contents: Record) => { // Home if (contents.home) { @@ -130,6 +140,45 @@ const PageContentDashboard: React.FC = () => { og_title: contents.home.og_title || '', og_description: contents.home.og_description || '', og_image: contents.home.og_image || '', + features: normalizeArray(contents.home.features), + amenities_section_title: contents.home.amenities_section_title || '', + amenities_section_subtitle: contents.home.amenities_section_subtitle || '', + amenities: normalizeArray(contents.home.amenities), + testimonials_section_title: contents.home.testimonials_section_title || '', + testimonials_section_subtitle: contents.home.testimonials_section_subtitle || '', + testimonials: normalizeArray(contents.home.testimonials), + about_preview_title: contents.home.about_preview_title || '', + about_preview_subtitle: contents.home.about_preview_subtitle || '', + about_preview_content: contents.home.about_preview_content || '', + about_preview_image: contents.home.about_preview_image || '', + stats: normalizeArray(contents.home.stats), + luxury_section_title: contents.home.luxury_section_title || '', + luxury_section_subtitle: contents.home.luxury_section_subtitle || '', + luxury_section_image: contents.home.luxury_section_image || '', + luxury_features: normalizeArray(contents.home.luxury_features), + luxury_gallery_section_title: contents.home.luxury_gallery_section_title || '', + luxury_gallery_section_subtitle: contents.home.luxury_gallery_section_subtitle || '', + luxury_gallery: normalizeArray(contents.home.luxury_gallery), + luxury_testimonials_section_title: contents.home.luxury_testimonials_section_title || '', + luxury_testimonials_section_subtitle: contents.home.luxury_testimonials_section_subtitle || '', + luxury_testimonials: normalizeArray(contents.home.luxury_testimonials), + luxury_services_section_title: contents.home.luxury_services_section_title || '', + luxury_services_section_subtitle: contents.home.luxury_services_section_subtitle || '', + luxury_services: normalizeArray(contents.home.luxury_services), + 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), + awards_section_title: contents.home.awards_section_title || '', + awards_section_subtitle: contents.home.awards_section_subtitle || '', + awards: normalizeArray(contents.home.awards), + cta_title: contents.home.cta_title || '', + cta_subtitle: contents.home.cta_subtitle || '', + cta_button_text: contents.home.cta_button_text || '', + cta_button_link: contents.home.cta_button_link || '', + cta_image: contents.home.cta_image || '', + partners_section_title: contents.home.partners_section_title || '', + partners_section_subtitle: contents.home.partners_section_subtitle || '', + partners: normalizeArray(contents.home.partners), }); } @@ -154,8 +203,14 @@ const PageContentDashboard: React.FC = () => { description: contents.about.description || '', content: contents.about.content || '', story_content: contents.about.story_content || '', - values: contents.about.values || [], - features: contents.about.features || [], + values: normalizeArray(contents.about.values), + features: normalizeArray(contents.about.features), + about_hero_image: contents.about.about_hero_image || '', + mission: contents.about.mission || '', + vision: contents.about.vision || '', + team: normalizeArray(contents.about.team), + timeline: normalizeArray(contents.about.timeline), + achievements: normalizeArray(contents.about.achievements), meta_title: contents.about.meta_title || '', meta_description: contents.about.meta_description || '', }); @@ -169,6 +224,7 @@ const PageContentDashboard: React.FC = () => { social_links: contents.footer.social_links || {}, footer_links: contents.footer.footer_links || { quick_links: [], support_links: [] }, badges: contents.footer.badges || [], + copyright_text: contents.footer.copyright_text || '', meta_title: contents.footer.meta_title || '', meta_description: contents.footer.meta_description || '', }); @@ -244,7 +300,7 @@ const PageContentDashboard: React.FC = () => { try { setUploadingImage(true); const response = await bannerService.uploadBannerImage(file); - if (response.status === 'success' || response.success) { + if (response.success) { setBannerFormData({ ...bannerFormData, image_url: response.data.image_url }); toast.success('Image uploaded successfully'); } @@ -257,6 +313,27 @@ const PageContentDashboard: React.FC = () => { } }; + // Generic image upload handler for page content images + const handlePageContentImageUpload = async ( + file: File, + onSuccess: (imageUrl: string) => void + ) => { + if (file.size > 5 * 1024 * 1024) { + toast.error('Image size must be less than 5MB'); + return; + } + + try { + const response = await pageContentService.uploadImage(file); + if (response.success) { + onSuccess(response.data.image_url); + toast.success('Image uploaded successfully'); + } + } catch (error: any) { + toast.error(error.response?.data?.message || 'Failed to upload image'); + } + }; + const handleBannerSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -270,7 +347,7 @@ const PageContentDashboard: React.FC = () => { if (imageFile && !imageUrl) { setUploadingImage(true); const uploadResponse = await bannerService.uploadBannerImage(imageFile); - if (uploadResponse.status === 'success' || uploadResponse.success) { + if (uploadResponse.success) { imageUrl = uploadResponse.data.image_url; } else { throw new Error('Failed to upload image'); @@ -305,9 +382,9 @@ const PageContentDashboard: React.FC = () => { setEditingBanner(banner); setBannerFormData({ title: banner.title || '', - description: '', + description: banner.description || '', image_url: banner.image_url || '', - link: banner.link || '', + link_url: banner.link_url || '', position: banner.position || 'home', display_order: banner.display_order || 0, is_active: banner.is_active ?? true, @@ -343,7 +420,7 @@ const PageContentDashboard: React.FC = () => { title: '', description: '', image_url: '', - link: '', + link_url: '', position: 'home', display_order: 0, is_active: true, @@ -531,6 +608,42 @@ const PageContentDashboard: React.FC = () => { {/* Home Tab */} {activeTab === 'home' && (
+ {/* Seed Data Button */} +
+
+
+

Quick Start

+

Load pre-configured luxury content to get started quickly

+
+ +
+
+ {/* Home Page Content Section */}

Home Page Content

@@ -559,14 +672,38 @@ const PageContentDashboard: React.FC = () => {
- - setHomeData({ ...homeData, hero_image: e.target.value })} - className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200" - placeholder="https://example.com/hero-image.jpg" - /> + +
+ setHomeData({ ...homeData, hero_image: e.target.value })} + className="flex-1 px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200" + placeholder="https://example.com/hero-image.jpg or upload" + /> + +
+ {homeData.hero_image && ( +
+ Hero preview +
+ )}
@@ -621,17 +758,1388 @@ const PageContentDashboard: React.FC = () => { placeholder="SEO Meta Description" />
+
+
-
- + {/* Amenities Section */} +
+

Amenities Section

+
+
+ + setHomeData({ ...homeData, amenities_section_title: e.target.value })} + className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl" + placeholder="Luxury Amenities" + />
+
+ + setHomeData({ ...homeData, amenities_section_subtitle: e.target.value })} + className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl" + placeholder="Experience world-class amenities" + /> +
+
+
+

Amenities

+ +
+
+ {Array.isArray(homeData.amenities) && homeData.amenities.map((amenity, index) => ( +
+
+

Amenity {index + 1}

+ +
+
+
+ { + setHomeData((prevData) => { + const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : []; + currentAmenities[index] = { ...currentAmenities[index], icon: iconName }; + return { ...prevData, amenities: currentAmenities }; + }); + }} + label="Icon" + /> +
+
+ +
+ { + setHomeData((prevData) => { + const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : []; + currentAmenities[index] = { ...currentAmenities[index], image: e.target.value }; + return { ...prevData, amenities: currentAmenities }; + }); + }} + className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg" + placeholder="URL or upload" + /> + +
+ {amenity?.image && ( +
+ Amenity preview +
+ )} +
+
+
+ + { + setHomeData((prevData) => { + const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : []; + currentAmenities[index] = { ...currentAmenities[index], title: e.target.value }; + return { ...prevData, amenities: currentAmenities }; + }); + }} + className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg" + /> +
+
+ +