diff --git a/Backend/alembic/versions/163657e72e93_add_page_content_table.py b/Backend/alembic/versions/163657e72e93_add_page_content_table.py new file mode 100644 index 00000000..08c3a32b --- /dev/null +++ b/Backend/alembic/versions/163657e72e93_add_page_content_table.py @@ -0,0 +1,62 @@ +"""add_page_content_table + +Revision ID: 163657e72e93 +Revises: 6a126cc5b23c +Create Date: 2025-11-18 18:02:03.480951 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '163657e72e93' +down_revision = '6a126cc5b23c' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + # Only create the page_contents table, skip other schema changes + op.create_table('page_contents', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('page_type', sa.Enum('home', 'contact', 'about', 'footer', 'seo', name='pagetype'), nullable=False), + sa.Column('title', sa.String(length=500), nullable=True), + sa.Column('subtitle', sa.String(length=1000), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('content', sa.Text(), nullable=True), + sa.Column('meta_title', sa.String(length=500), nullable=True), + sa.Column('meta_description', sa.Text(), nullable=True), + sa.Column('meta_keywords', sa.String(length=1000), nullable=True), + sa.Column('og_title', sa.String(length=500), nullable=True), + sa.Column('og_description', sa.Text(), nullable=True), + sa.Column('og_image', sa.String(length=1000), nullable=True), + sa.Column('canonical_url', sa.String(length=1000), nullable=True), + sa.Column('contact_info', sa.Text(), nullable=True), + sa.Column('social_links', sa.Text(), nullable=True), + sa.Column('footer_links', sa.Text(), nullable=True), + sa.Column('hero_title', sa.String(length=500), nullable=True), + sa.Column('hero_subtitle', sa.String(length=1000), nullable=True), + sa.Column('hero_image', sa.String(length=1000), nullable=True), + sa.Column('story_content', sa.Text(), nullable=True), + sa.Column('values', sa.Text(), nullable=True), + sa.Column('features', sa.Text(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_page_contents_id'), 'page_contents', ['id'], unique=False) + op.create_index(op.f('ix_page_contents_page_type'), 'page_contents', ['page_type'], unique=True) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_page_contents_page_type'), table_name='page_contents') + op.drop_index(op.f('ix_page_contents_id'), table_name='page_contents') + op.drop_table('page_contents') + op.execute("DROP TYPE IF EXISTS pagetype") + # ### end Alembic commands ### + diff --git a/Backend/alembic/versions/__pycache__/163657e72e93_add_page_content_table.cpython-312.pyc b/Backend/alembic/versions/__pycache__/163657e72e93_add_page_content_table.cpython-312.pyc new file mode 100644 index 00000000..d09e68da Binary files /dev/null and b/Backend/alembic/versions/__pycache__/163657e72e93_add_page_content_table.cpython-312.pyc differ diff --git a/Backend/alembic/versions/__pycache__/6a126cc5b23c_add_capacity_room_size_view_to_rooms.cpython-312.pyc b/Backend/alembic/versions/__pycache__/6a126cc5b23c_add_capacity_room_size_view_to_rooms.cpython-312.pyc index 6a578129..30060d3e 100644 Binary files a/Backend/alembic/versions/__pycache__/6a126cc5b23c_add_capacity_room_size_view_to_rooms.cpython-312.pyc and b/Backend/alembic/versions/__pycache__/6a126cc5b23c_add_capacity_room_size_view_to_rooms.cpython-312.pyc differ diff --git a/Backend/alembic/versions/__pycache__/cce764ef7a50_add_map_url_to_page_content.cpython-312.pyc b/Backend/alembic/versions/__pycache__/cce764ef7a50_add_map_url_to_page_content.cpython-312.pyc new file mode 100644 index 00000000..dca8999b Binary files /dev/null and b/Backend/alembic/versions/__pycache__/cce764ef7a50_add_map_url_to_page_content.cpython-312.pyc differ diff --git a/Backend/alembic/versions/cce764ef7a50_add_map_url_to_page_content.py b/Backend/alembic/versions/cce764ef7a50_add_map_url_to_page_content.py new file mode 100644 index 00000000..112cf9eb --- /dev/null +++ b/Backend/alembic/versions/cce764ef7a50_add_map_url_to_page_content.py @@ -0,0 +1,28 @@ +"""add_map_url_to_page_content + +Revision ID: cce764ef7a50 +Revises: 163657e72e93 +Create Date: 2025-11-18 18:11:41.071053 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'cce764ef7a50' +down_revision = '163657e72e93' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + # Only add the map_url column to page_contents table + op.add_column('page_contents', sa.Column('map_url', sa.String(length=1000), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('page_contents', 'map_url') + # ### end Alembic commands ### diff --git a/Backend/src/__pycache__/main.cpython-312.pyc b/Backend/src/__pycache__/main.cpython-312.pyc index bc96cf66..65d9cdf6 100644 Binary files a/Backend/src/__pycache__/main.cpython-312.pyc and b/Backend/src/__pycache__/main.cpython-312.pyc differ diff --git a/Backend/src/main.py b/Backend/src/main.py index be697f93..6cc3a81a 100644 --- a/Backend/src/main.py +++ b/Backend/src/main.py @@ -42,15 +42,17 @@ if settings.is_development: logger.info("Creating database tables (development mode)") Base.metadata.create_all(bind=engine) else: - # Ensure new cookie-related tables exist even if full migrations haven't been run yet. + # Ensure new tables exist even if full migrations haven't been run yet. try: from .models.cookie_policy import CookiePolicy from .models.cookie_integration_config import CookieIntegrationConfig - logger.info("Ensuring cookie-related tables exist") + from .models.page_content import PageContent + logger.info("Ensuring required tables exist") CookiePolicy.__table__.create(bind=engine, checkfirst=True) CookieIntegrationConfig.__table__.create(bind=engine, checkfirst=True) + PageContent.__table__.create(bind=engine, checkfirst=True) except Exception as e: - logger.error(f"Failed to ensure cookie tables exist: {e}") + logger.error(f"Failed to ensure required tables exist: {e}") from .routes import auth_routes from .routes import privacy_routes @@ -125,9 +127,11 @@ app.add_exception_handler(Exception, general_exception_handler) # Enhanced Health check with database connectivity @app.get("/health", tags=["health"]) +@app.get("/api/health", tags=["health"]) async def health_check(db: Session = Depends(get_db)): """ Enhanced health check endpoint with database connectivity test + Available at both /health and /api/health for consistency """ health_status = { "status": "healthy", @@ -196,7 +200,7 @@ from .routes import ( room_routes, booking_routes, payment_routes, invoice_routes, banner_routes, favorite_routes, service_routes, service_booking_routes, promotion_routes, report_routes, review_routes, user_routes, audit_routes, admin_privacy_routes, - system_settings_routes, contact_routes + system_settings_routes, contact_routes, page_content_routes ) # Legacy routes (maintain backward compatibility) @@ -234,6 +238,8 @@ app.include_router(audit_routes.router, prefix=settings.API_V1_PREFIX) app.include_router(admin_privacy_routes.router, prefix=settings.API_V1_PREFIX) app.include_router(system_settings_routes.router, prefix=settings.API_V1_PREFIX) app.include_router(contact_routes.router, prefix=settings.API_V1_PREFIX) +app.include_router(page_content_routes.router, prefix="/api") +app.include_router(page_content_routes.router, prefix=settings.API_V1_PREFIX) logger.info("All routes registered successfully") diff --git a/Backend/src/models/__init__.py b/Backend/src/models/__init__.py index edb1f882..27187c9a 100644 --- a/Backend/src/models/__init__.py +++ b/Backend/src/models/__init__.py @@ -19,6 +19,7 @@ from .cookie_policy import CookiePolicy from .cookie_integration_config import CookieIntegrationConfig from .system_settings import SystemSettings from .invoice import Invoice, InvoiceItem +from .page_content import PageContent, PageType __all__ = [ "Role", @@ -48,5 +49,7 @@ __all__ = [ "SystemSettings", "Invoice", "InvoiceItem", + "PageContent", + "PageType", ] diff --git a/Backend/src/models/__pycache__/__init__.cpython-312.pyc b/Backend/src/models/__pycache__/__init__.cpython-312.pyc index b8a9853d..97f64fe3 100644 Binary files a/Backend/src/models/__pycache__/__init__.cpython-312.pyc and b/Backend/src/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/Backend/src/models/__pycache__/page_content.cpython-312.pyc b/Backend/src/models/__pycache__/page_content.cpython-312.pyc new file mode 100644 index 00000000..6ba79360 Binary files /dev/null and b/Backend/src/models/__pycache__/page_content.cpython-312.pyc differ diff --git a/Backend/src/models/page_content.py b/Backend/src/models/page_content.py new file mode 100644 index 00000000..6e382195 --- /dev/null +++ b/Backend/src/models/page_content.py @@ -0,0 +1,60 @@ +from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean, Enum as SQLEnum +from sqlalchemy.orm import relationship +from datetime import datetime +import enum + +from ..config.database import Base + + +class PageType(str, enum.Enum): + HOME = "home" + CONTACT = "contact" + ABOUT = "about" + FOOTER = "footer" + SEO = "seo" + + +class PageContent(Base): + __tablename__ = "page_contents" + + id = Column(Integer, primary_key=True, index=True) + page_type = Column(SQLEnum(PageType), nullable=False, unique=True, index=True) + + # General content fields + title = Column(String(500), nullable=True) + subtitle = Column(String(1000), nullable=True) + description = Column(Text, nullable=True) + content = Column(Text, nullable=True) # Rich text content + + # SEO fields + meta_title = Column(String(500), nullable=True) + meta_description = Column(Text, nullable=True) + meta_keywords = Column(String(1000), nullable=True) + og_title = Column(String(500), nullable=True) + og_description = Column(Text, nullable=True) + og_image = Column(String(1000), nullable=True) + canonical_url = Column(String(1000), nullable=True) + + # Contact/Footer specific fields (stored as JSON strings) + contact_info = Column(Text, nullable=True) # JSON: phone, email, address + map_url = Column(String(1000), nullable=True) # Google Maps embed URL + social_links = Column(Text, nullable=True) # JSON: facebook, twitter, instagram, etc. + footer_links = Column(Text, nullable=True) # JSON: quick links, support links + + # Home page specific + hero_title = Column(String(500), nullable=True) + hero_subtitle = Column(String(1000), nullable=True) + hero_image = Column(String(1000), nullable=True) + + # About page specific + story_content = Column(Text, nullable=True) + values = Column(Text, nullable=True) # JSON array of values + features = Column(Text, nullable=True) # JSON array of features + + # Status + is_active = Column(Boolean, default=True, nullable=False) + + # Timestamps + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) + diff --git a/Backend/src/routes/__pycache__/page_content_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/page_content_routes.cpython-312.pyc new file mode 100644 index 00000000..d97a9ae9 Binary files /dev/null and b/Backend/src/routes/__pycache__/page_content_routes.cpython-312.pyc differ diff --git a/Backend/src/routes/page_content_routes.py b/Backend/src/routes/page_content_routes.py new file mode 100644 index 00000000..0374f7d5 --- /dev/null +++ b/Backend/src/routes/page_content_routes.py @@ -0,0 +1,413 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import Optional +from datetime import datetime +import json + +from ..config.database import get_db +from ..middleware.auth import get_current_user, authorize_roles +from ..models.user import User +from ..models.page_content import PageContent, PageType + +router = APIRouter(prefix="/page-content", tags=["page-content"]) + + +@router.get("/") +async def get_all_page_contents( + db: Session = Depends(get_db) +): + """Get all page contents""" + try: + contents = db.query(PageContent).all() + result = [] + for content in contents: + content_dict = { + "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, + "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, + "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, + "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, + } + result.append(content_dict) + + return { + "status": "success", + "data": { + "page_contents": result + } + } + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error fetching page contents: {str(e)}" + ) + + +@router.get("/{page_type}") +async def get_page_content( + page_type: PageType, + db: Session = Depends(get_db) +): + """Get content for a specific page""" + try: + content = db.query(PageContent).filter(PageContent.page_type == page_type).first() + + if not content: + # Return default structure if not found + return { + "status": "success", + "data": { + "page_content": None + } + } + + content_dict = { + "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, + "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, + "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, + "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, + } + + return { + "status": "success", + "data": { + "page_content": content_dict + } + } + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error fetching page content: {str(e)}" + ) + + +@router.post("/{page_type}") +async def create_or_update_page_content( + page_type: PageType, + title: Optional[str] = None, + subtitle: Optional[str] = None, + description: Optional[str] = None, + content: Optional[str] = None, + meta_title: Optional[str] = None, + meta_description: Optional[str] = None, + meta_keywords: Optional[str] = None, + og_title: Optional[str] = None, + og_description: Optional[str] = None, + og_image: Optional[str] = None, + canonical_url: Optional[str] = None, + contact_info: Optional[str] = None, + map_url: Optional[str] = None, + social_links: Optional[str] = None, + footer_links: Optional[str] = None, + hero_title: Optional[str] = None, + hero_subtitle: Optional[str] = None, + hero_image: Optional[str] = None, + story_content: Optional[str] = None, + values: Optional[str] = None, + features: Optional[str] = None, + is_active: bool = True, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Create or update page content (admin only)""" + try: + authorize_roles(current_user, ["admin"]) + + # Validate JSON fields if provided + if contact_info: + try: + json.loads(contact_info) + except json.JSONDecodeError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid JSON in contact_info" + ) + + if social_links: + try: + json.loads(social_links) + except json.JSONDecodeError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid JSON in social_links" + ) + + if footer_links: + try: + json.loads(footer_links) + except json.JSONDecodeError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid JSON in footer_links" + ) + + if values: + try: + json.loads(values) + except json.JSONDecodeError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid JSON in values" + ) + + if features: + try: + json.loads(features) + except json.JSONDecodeError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid JSON in features" + ) + + # Check if content exists + existing_content = db.query(PageContent).filter(PageContent.page_type == page_type).first() + + if existing_content: + # Update existing + if title is not None: + existing_content.title = title + if subtitle is not None: + existing_content.subtitle = subtitle + if description is not None: + existing_content.description = description + if content is not None: + existing_content.content = content + if meta_title is not None: + existing_content.meta_title = meta_title + if meta_description is not None: + existing_content.meta_description = meta_description + if meta_keywords is not None: + existing_content.meta_keywords = meta_keywords + if og_title is not None: + existing_content.og_title = og_title + if og_description is not None: + existing_content.og_description = og_description + if og_image is not None: + existing_content.og_image = og_image + if canonical_url is not None: + existing_content.canonical_url = canonical_url + if contact_info is not None: + existing_content.contact_info = contact_info + if map_url is not None: + existing_content.map_url = map_url + if social_links is not None: + existing_content.social_links = social_links + if footer_links is not None: + existing_content.footer_links = footer_links + if hero_title is not None: + existing_content.hero_title = hero_title + if hero_subtitle is not None: + existing_content.hero_subtitle = hero_subtitle + if hero_image is not None: + existing_content.hero_image = hero_image + if story_content is not None: + existing_content.story_content = story_content + if values is not None: + existing_content.values = values + if features is not None: + existing_content.features = features + if is_active is not None: + existing_content.is_active = is_active + + existing_content.updated_at = datetime.utcnow() + db.commit() + db.refresh(existing_content) + + return { + "status": "success", + "message": "Page content updated successfully", + "data": { + "page_content": { + "id": existing_content.id, + "page_type": existing_content.page_type.value, + "title": existing_content.title, + "updated_at": existing_content.updated_at.isoformat() if existing_content.updated_at else None, + } + } + } + else: + # Create new + new_content = PageContent( + page_type=page_type, + title=title, + subtitle=subtitle, + description=description, + content=content, + meta_title=meta_title, + meta_description=meta_description, + meta_keywords=meta_keywords, + og_title=og_title, + og_description=og_description, + og_image=og_image, + canonical_url=canonical_url, + contact_info=contact_info, + map_url=map_url, + social_links=social_links, + footer_links=footer_links, + hero_title=hero_title, + hero_subtitle=hero_subtitle, + hero_image=hero_image, + story_content=story_content, + values=values, + features=features, + is_active=is_active, + ) + + db.add(new_content) + db.commit() + db.refresh(new_content) + + return { + "status": "success", + "message": "Page content created successfully", + "data": { + "page_content": { + "id": new_content.id, + "page_type": new_content.page_type.value, + "title": new_content.title, + "created_at": new_content.created_at.isoformat() if new_content.created_at else None, + } + } + } + except HTTPException: + raise + except Exception as e: + db.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error saving page content: {str(e)}" + ) + + +@router.put("/{page_type}") +async def update_page_content( + page_type: PageType, + page_data: dict, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Create or update page content using JSON body (admin only)""" + try: + authorize_roles(current_user, ["admin"]) + + existing_content = db.query(PageContent).filter(PageContent.page_type == page_type).first() + + if not existing_content: + # Create new content if it doesn't exist + existing_content = PageContent( + page_type=page_type, + is_active=True, + ) + db.add(existing_content) + + # Update fields from request body + 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", "values", "features"] and value is not None: + if isinstance(value, str): + # Already a string, validate it's valid JSON + try: + json.loads(value) + except json.JSONDecodeError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid JSON in {key}" + ) + elif isinstance(value, (dict, list)): + # Convert dict/list to JSON string for storage + value = json.dumps(value) + + # Skip None values to allow partial updates + if value is not None: + setattr(existing_content, key, value) + + existing_content.updated_at = datetime.utcnow() + db.commit() + db.refresh(existing_content) + + content_dict = { + "id": existing_content.id, + "page_type": existing_content.page_type.value, + "title": existing_content.title, + "subtitle": existing_content.subtitle, + "description": existing_content.description, + "content": existing_content.content, + "meta_title": existing_content.meta_title, + "meta_description": existing_content.meta_description, + "meta_keywords": existing_content.meta_keywords, + "og_title": existing_content.og_title, + "og_description": existing_content.og_description, + "og_image": existing_content.og_image, + "canonical_url": existing_content.canonical_url, + "contact_info": json.loads(existing_content.contact_info) if existing_content.contact_info else None, + "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, + "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, + "is_active": existing_content.is_active, + "updated_at": existing_content.updated_at.isoformat() if existing_content.updated_at else None, + } + + return { + "status": "success", + "message": "Page content updated successfully", + "data": { + "page_content": content_dict + } + } + except HTTPException: + raise + except Exception as e: + db.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error updating page content: {str(e)}" + ) + diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx index b13797a4..94e626c7 100644 --- a/Frontend/src/App.tsx +++ b/Frontend/src/App.tsx @@ -55,22 +55,12 @@ const ResetPasswordPage = lazy(() => import('./pages/auth/ResetPasswordPage')); // Lazy load admin pages const AdminDashboardPage = lazy(() => import('./pages/admin/DashboardPage')); -const RoomManagementPage = lazy(() => import('./pages/admin/RoomManagementPage')); const UserManagementPage = lazy(() => import('./pages/admin/UserManagementPage')); -const BookingManagementPage = lazy(() => import('./pages/admin/BookingManagementPage')); -const PaymentManagementPage = lazy(() => import('./pages/admin/PaymentManagementPage')); -const InvoiceManagementPage = lazy(() => import('./pages/admin/InvoiceManagementPage')); -const ServiceManagementPage = lazy(() => import('./pages/admin/ServiceManagementPage')); -const ReviewManagementPage = lazy(() => import('./pages/admin/ReviewManagementPage')); -const PromotionManagementPage = lazy(() => import('./pages/admin/PromotionManagementPage')); -const BannerManagementPage = lazy(() => import('./pages/admin/BannerManagementPage')); -const ReportsPage = lazy(() => import('./pages/admin/ReportsPage')); -const CookieSettingsPage = lazy(() => import('./pages/admin/CookieSettingsPage')); -const CurrencySettingsPage = lazy(() => import('./pages/admin/CurrencySettingsPage')); -const StripeSettingsPage = lazy(() => import('./pages/admin/StripeSettingsPage')); -const AuditLogsPage = lazy(() => import('./pages/admin/AuditLogsPage')); -const CheckInPage = lazy(() => import('./pages/admin/CheckInPage')); -const CheckOutPage = lazy(() => import('./pages/admin/CheckOutPage')); +const PageContentDashboardPage = lazy(() => import('./pages/admin/PageContentDashboard')); +const AnalyticsDashboardPage = lazy(() => import('./pages/admin/AnalyticsDashboardPage')); +const BusinessDashboardPage = lazy(() => import('./pages/admin/BusinessDashboardPage')); +const SettingsPage = lazy(() => import('./pages/admin/SettingsPage')); +const ReceptionDashboardPage = lazy(() => import('./pages/admin/ReceptionDashboardPage')); // Demo component for pages not yet created const DemoPage: React.FC<{ title: string }> = ({ title }) => ( @@ -299,65 +289,25 @@ function App() { path="users" element={} /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> } + path="business" + element={} /> - } + } + /> + } + /> + } /> } - /> - } - /> - } + element={} /> diff --git a/Frontend/src/components/layout/Footer.tsx b/Frontend/src/components/layout/Footer.tsx index 20c502ab..166117fb 100644 --- a/Frontend/src/components/layout/Footer.tsx +++ b/Frontend/src/components/layout/Footer.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import { Hotel, @@ -15,8 +15,51 @@ import { Star } from 'lucide-react'; import CookiePreferencesLink from '../common/CookiePreferencesLink'; +import { pageContentService } from '../../services/api'; +import type { PageContent } from '../../services/api/pageContentService'; const Footer: React.FC = () => { + const [pageContent, setPageContent] = useState(null); + + useEffect(() => { + const fetchPageContent = async () => { + try { + const response = await pageContentService.getPageContent('footer'); + if (response.status === 'success' && response.data?.page_content) { + setPageContent(response.data.page_content); + } + } catch (err: any) { + console.error('Error fetching footer content:', err); + // Silently fail - use default content + } + }; + + fetchPageContent(); + }, []); + + // Default links + const defaultQuickLinks = [ + { label: 'Home', url: '/' }, + { label: 'Rooms & Suites', url: '/rooms' }, + { label: 'My Bookings', url: '/bookings' }, + { label: 'About Us', url: '/about' } + ]; + + const defaultSupportLinks = [ + { label: 'FAQ', url: '/faq' }, + { label: 'Terms of Service', url: '/terms' }, + { label: 'Privacy Policy', url: '/privacy' }, + { label: 'Contact Us', url: '/contact' } + ]; + + const quickLinks = pageContent?.footer_links?.quick_links && pageContent.footer_links.quick_links.length > 0 + ? pageContent.footer_links.quick_links + : defaultQuickLinks; + + const supportLinks = pageContent?.footer_links?.support_links && pageContent.footer_links.support_links.length > 0 + ? pageContent.footer_links.support_links + : defaultSupportLinks; + return (