updates
This commit is contained in:
BIN
Backend/src/routes/__pycache__/about_routes.cpython-312.pyc
Normal file
BIN
Backend/src/routes/__pycache__/about_routes.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Backend/src/routes/__pycache__/footer_routes.cpython-312.pyc
Normal file
BIN
Backend/src/routes/__pycache__/footer_routes.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Backend/src/routes/__pycache__/home_routes.cpython-312.pyc
Normal file
BIN
Backend/src/routes/__pycache__/home_routes.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
75
Backend/src/routes/about_routes.py
Normal file
75
Backend/src/routes/about_routes.py
Normal file
@@ -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)}"
|
||||
)
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
{f'<img src="{invoice.get("company_logo_url")}" alt="Company Logo" class="company-logo" />' if invoice.get('company_logo_url') else ''}
|
||||
{f'<h2 style="margin: 10px 0; color: #0f0f0f;">{invoice.get("company_name", "")}</h2>' if invoice.get('company_name') else ''}
|
||||
<h1>{invoice_type}</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
@@ -139,6 +146,7 @@ def _generate_invoice_email_html(invoice: dict, is_proforma: bool = False) -> st
|
||||
<div class="total">
|
||||
<p>Subtotal: {invoice.get('subtotal', 0):.2f}</p>
|
||||
{f'<p style="color: #059669;">Discount: -{invoice.get("discount_amount", 0):.2f}</p>' if invoice.get('discount_amount', 0) > 0 else ''}
|
||||
{f'<p style="color: #059669; font-size: 14px;">Promotion Code: {invoice.get("promotion_code", "")}</p>' if invoice.get('promotion_code') else ''}
|
||||
<p>Tax: {invoice.get('tax_amount', 0):.2f}</p>
|
||||
<p><strong>Total Amount: {invoice.get('total_amount', 0):.2f}</strong></p>
|
||||
<p>Amount Paid: {invoice.get('amount_paid', 0):.2f}</p>
|
||||
@@ -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,
|
||||
|
||||
68
Backend/src/routes/contact_content_routes.py
Normal file
68
Backend/src/routes/contact_content_routes.py
Normal file
@@ -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)}"
|
||||
)
|
||||
|
||||
63
Backend/src/routes/footer_routes.py
Normal file
63
Backend/src/routes/footer_routes.py
Normal file
@@ -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)}"
|
||||
)
|
||||
|
||||
110
Backend/src/routes/home_routes.py
Normal file
110
Backend/src/routes/home_routes.py
Normal file
@@ -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)}"
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user