672 lines
28 KiB
Python
672 lines
28 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
FastAPI server for Property PDF Generator
|
|
Provides REST API endpoints for generating high-quality property brochures
|
|
"""
|
|
|
|
from fastapi import FastAPI, HTTPException, UploadFile, File, Form
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import FileResponse, JSONResponse
|
|
from pydantic import BaseModel, Field
|
|
from typing import List, Optional, Dict, Any
|
|
import uvicorn
|
|
import os
|
|
import tempfile
|
|
import json
|
|
from datetime import datetime
|
|
from property_pdf_generator import PropertyPDFGenerator
|
|
|
|
# Initialize FastAPI app
|
|
app = FastAPI(
|
|
title="Property PDF Generator API",
|
|
description="High-quality property brochure PDF generation service",
|
|
version="1.0.0"
|
|
)
|
|
|
|
# Add CORS middleware
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"], # In production, restrict to specific domains
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# Initialize PDF generator
|
|
pdf_generator = PropertyPDFGenerator()
|
|
|
|
# Store generated PDFs temporarily (in production, use proper file storage)
|
|
generated_pdfs = {}
|
|
|
|
# Cleanup old PDFs periodically
|
|
import asyncio
|
|
import time
|
|
|
|
async def cleanup_old_pdfs():
|
|
"""Clean up old PDF files to prevent disk space issues"""
|
|
while True:
|
|
try:
|
|
current_time = time.time()
|
|
expired_files = []
|
|
|
|
for filename, filepath in generated_pdfs.items():
|
|
# Remove files older than 1 hour
|
|
if current_time - os.path.getctime(filepath) > 3600:
|
|
try:
|
|
if os.path.exists(filepath):
|
|
os.unlink(filepath)
|
|
expired_files.append(filename)
|
|
except Exception:
|
|
pass
|
|
|
|
# Remove expired entries from dictionary
|
|
for filename in expired_files:
|
|
generated_pdfs.pop(filename, None)
|
|
|
|
# Run cleanup every 30 minutes
|
|
await asyncio.sleep(1800)
|
|
|
|
except Exception:
|
|
# Continue cleanup even if there's an error
|
|
await asyncio.sleep(1800)
|
|
|
|
# Start cleanup task
|
|
cleanup_task = None
|
|
|
|
@app.on_event("startup")
|
|
async def startup_event():
|
|
"""Start cleanup task on startup"""
|
|
global cleanup_task
|
|
cleanup_task = asyncio.create_task(cleanup_old_pdfs())
|
|
|
|
@app.on_event("shutdown")
|
|
async def shutdown_event():
|
|
"""Stop cleanup task and clean up files on shutdown"""
|
|
global cleanup_task
|
|
if cleanup_task:
|
|
cleanup_task.cancel()
|
|
|
|
# Clean up all stored PDFs
|
|
for filepath in generated_pdfs.values():
|
|
try:
|
|
if os.path.exists(filepath):
|
|
os.unlink(filepath)
|
|
except Exception:
|
|
pass
|
|
generated_pdfs.clear()
|
|
|
|
# Data models
|
|
class PropertyData(BaseModel):
|
|
"""Enhanced property information model with market data and analytics"""
|
|
# Template & Layout
|
|
template: str = Field(..., description="Template name to use")
|
|
layout: Optional[str] = Field(None, description="Layout configuration for custom templates")
|
|
|
|
# Basic Property Information
|
|
propertyName: str = Field(..., description="Name of the property")
|
|
propertyType: str = Field(..., description="Type of property")
|
|
location: str = Field(..., description="Property location")
|
|
price: str = Field(..., description="Property price")
|
|
bedrooms: str = Field(..., description="Number of bedrooms")
|
|
bathrooms: str = Field(..., description="Number of bathrooms")
|
|
area: str = Field(..., description="Property area in sq ft")
|
|
description: Optional[str] = Field(None, description="Property description")
|
|
amenities: List[str] = Field(default=[], description="List of amenities")
|
|
images: List[str] = Field(default=[], description="Base64 encoded images")
|
|
imageNames: List[str] = Field(default=[], description="Room names for each image")
|
|
|
|
# Market Data & Analytics
|
|
marketTrend: Optional[str] = Field(None, description="Market trend (rising/stable/declining)")
|
|
roiPotential: Optional[str] = Field(None, description="Expected ROI percentage")
|
|
avgPricePerSqft: Optional[str] = Field(None, description="Average market price per sq ft")
|
|
marketDemand: Optional[str] = Field(None, description="Market demand level (high/medium/low)")
|
|
locationAdvantages: Optional[str] = Field(None, description="Location benefits and advantages")
|
|
|
|
# Investment Information
|
|
investmentType: Optional[str] = Field(None, description="Investment type (buy-to-live/rent/sell)")
|
|
rentalYield: Optional[str] = Field(None, description="Expected rental yield percentage")
|
|
investmentHighlights: Optional[str] = Field(None, description="Key investment benefits")
|
|
|
|
# Content Modules
|
|
contentModules: List[str] = Field(default=[], description="Selected content modules")
|
|
additionalContent: Optional[str] = Field(None, description="Additional custom content")
|
|
|
|
# Customization Options
|
|
headerStyle: Optional[str] = Field("modern", description="Header style (modern/classic/luxury)")
|
|
colorScheme: Optional[str] = Field("blue", description="Color scheme (blue/green/purple/gold)")
|
|
fontStyle: Optional[str] = Field("sans-serif", description="Font style (sans-serif/serif/modern)")
|
|
|
|
class TemplateInfo(BaseModel):
|
|
"""Template information model"""
|
|
name: str
|
|
display_name: str
|
|
description: str
|
|
category: str
|
|
preview_color: str
|
|
|
|
class GeneratePDFRequest(BaseModel):
|
|
"""PDF generation request model"""
|
|
property_data: PropertyData
|
|
template_name: str
|
|
|
|
class GeneratePDFResponse(BaseModel):
|
|
"""PDF generation response model"""
|
|
success: bool
|
|
message: str
|
|
pdf_url: Optional[str] = None
|
|
error: Optional[str] = None
|
|
|
|
# Available templates
|
|
AVAILABLE_TEMPLATES = {
|
|
# Professional Templates
|
|
"professional-1pager": {
|
|
"name": "professional-1pager",
|
|
"display_name": "Professional 1-Pager",
|
|
"description": "Compact single-page brochure with 2x2 image grid",
|
|
"category": "Professional",
|
|
"preview_color": "#667eea",
|
|
"pages": 1,
|
|
"image_grid": "2x2"
|
|
},
|
|
"professional-3pager": {
|
|
"name": "professional-3pager",
|
|
"display_name": "Professional 3-Pager",
|
|
"description": "Comprehensive three-page brochure with detailed analysis",
|
|
"category": "Professional",
|
|
"preview_color": "#1e3a8a",
|
|
"pages": 3,
|
|
"image_grid": "4x4"
|
|
},
|
|
"professional-5pager": {
|
|
"name": "professional-5pager",
|
|
"display_name": "Professional 5-Pager",
|
|
"description": "Premium five-page brochure with comprehensive analysis",
|
|
"category": "Professional",
|
|
"preview_color": "#059669",
|
|
"pages": 5,
|
|
"image_grid": "6x6"
|
|
},
|
|
|
|
# Luxury Templates
|
|
"luxury-villa": {
|
|
"name": "luxury-villa",
|
|
"display_name": "Luxury Villa Brochure",
|
|
"description": "Exclusive villa template with premium styling",
|
|
"category": "Luxury",
|
|
"preview_color": "#2c1810",
|
|
"pages": 4,
|
|
"image_grid": "5x4"
|
|
},
|
|
"dubai-penthouse": {
|
|
"name": "dubai-penthouse",
|
|
"display_name": "Dubai Penthouse",
|
|
"description": "Dubai-specific luxury penthouse template",
|
|
"category": "Luxury",
|
|
"preview_color": "#dc2626",
|
|
"pages": 6,
|
|
"image_grid": "6x5"
|
|
},
|
|
|
|
# Modern Templates
|
|
"modern-apartment": {
|
|
"name": "modern-apartment",
|
|
"display_name": "Modern Apartment",
|
|
"description": "Contemporary apartment template with clean lines",
|
|
"category": "Modern",
|
|
"preview_color": "#7c3aed",
|
|
"pages": 3,
|
|
"image_grid": "3x5"
|
|
},
|
|
|
|
# Custom Template
|
|
"custom": {
|
|
"name": "custom",
|
|
"display_name": "Custom Template",
|
|
"description": "Build your own template with custom layouts",
|
|
"category": "Custom",
|
|
"preview_color": "#1f2937",
|
|
"pages": "flexible",
|
|
"image_grid": "configurable"
|
|
}
|
|
}
|
|
|
|
@app.get("/")
|
|
async def root():
|
|
"""Root endpoint"""
|
|
return {
|
|
"message": "Property PDF Generator API",
|
|
"version": "1.0.0",
|
|
"status": "running",
|
|
"endpoints": {
|
|
"templates": "/api/templates",
|
|
"preview": "/api/preview",
|
|
"generate": "/api/generate-pdf",
|
|
"health": "/api/health"
|
|
}
|
|
}
|
|
|
|
@app.get("/api/health")
|
|
async def health_check():
|
|
"""Health check endpoint"""
|
|
return {
|
|
"status": "healthy",
|
|
"timestamp": datetime.now().isoformat(),
|
|
"service": "Property PDF Generator"
|
|
}
|
|
|
|
@app.get("/api/templates", response_model=List[TemplateInfo])
|
|
async def get_templates():
|
|
"""Get all available templates"""
|
|
try:
|
|
templates = []
|
|
for template_id, template_info in AVAILABLE_TEMPLATES.items():
|
|
templates.append(TemplateInfo(**template_info))
|
|
return templates
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Error fetching templates: {str(e)}")
|
|
|
|
@app.get("/api/templates/{template_name}")
|
|
async def get_template(template_name: str):
|
|
"""Get specific template information"""
|
|
try:
|
|
if template_name not in AVAILABLE_TEMPLATES:
|
|
raise HTTPException(status_code=404, detail="Template not found")
|
|
|
|
return AVAILABLE_TEMPLATES[template_name]
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Error fetching template: {str(e)}")
|
|
|
|
@app.post("/api/preview")
|
|
async def generate_preview(property_data: PropertyData):
|
|
"""Generate a preview of the property brochure"""
|
|
try:
|
|
# Validate template
|
|
if property_data.template not in AVAILABLE_TEMPLATES:
|
|
raise HTTPException(status_code=400, detail="Invalid template selected")
|
|
|
|
# Generate preview content
|
|
preview_content = {
|
|
"template": property_data.template,
|
|
"template_info": AVAILABLE_TEMPLATES[property_data.template],
|
|
"property_data": property_data.dict(),
|
|
"preview_html": generate_preview_html(property_data),
|
|
"generated_at": datetime.now().isoformat()
|
|
}
|
|
|
|
return {
|
|
"success": True,
|
|
"preview": preview_content,
|
|
"message": "Preview generated successfully"
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Error generating preview: {str(e)}")
|
|
|
|
# Add a new endpoint for Salesforce compatibility
|
|
@app.post("/api/preview-simple")
|
|
async def generate_preview_simple(request_data: dict):
|
|
"""Generate a preview using the simpler format sent from Salesforce"""
|
|
try:
|
|
# Extract template and property data from the request
|
|
template = request_data.get('template')
|
|
if not template or template not in AVAILABLE_TEMPLATES:
|
|
raise HTTPException(status_code=400, detail="Invalid template selected")
|
|
|
|
# Create a PropertyData object from the simple request
|
|
property_data = PropertyData(
|
|
template=template,
|
|
propertyName=request_data.get('propertyName', 'Unknown Property'),
|
|
propertyType=request_data.get('propertyType', 'Unknown'),
|
|
location=request_data.get('location', 'Unknown Location'),
|
|
price=request_data.get('price', 'Price on Request'),
|
|
bedrooms=request_data.get('bedrooms', 'N/A'),
|
|
bathrooms=request_data.get('bathrooms', 'N/A'),
|
|
area=request_data.get('area', 'N/A'),
|
|
description=request_data.get('description', 'No description available')
|
|
)
|
|
|
|
# Generate preview content
|
|
preview_content = {
|
|
"template": property_data.template,
|
|
"template_info": AVAILABLE_TEMPLATES[property_data.template],
|
|
"property_data": property_data.dict(),
|
|
"preview_html": generate_preview_html(property_data),
|
|
"generated_at": datetime.now().isoformat()
|
|
}
|
|
|
|
return {
|
|
"success": True,
|
|
"preview": preview_content,
|
|
"message": "Preview generated successfully"
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Error generating preview: {str(e)}")
|
|
|
|
@app.post("/api/generate-pdf", response_model=GeneratePDFResponse)
|
|
async def generate_pdf(request: GeneratePDFRequest):
|
|
"""Generate a PDF property brochure"""
|
|
try:
|
|
# Validate template
|
|
if request.template_name not in AVAILABLE_TEMPLATES:
|
|
return GeneratePDFResponse(
|
|
success=False,
|
|
message="Invalid template selected",
|
|
error="Template not found"
|
|
)
|
|
|
|
# Validate property data
|
|
if not request.property_data.propertyName or not request.property_data.propertyType:
|
|
return GeneratePDFResponse(
|
|
success=False,
|
|
message="Missing required property information",
|
|
error="Property name and type are required"
|
|
)
|
|
|
|
# Create temporary file for PDF
|
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp_file:
|
|
pdf_path = tmp_file.name
|
|
|
|
try:
|
|
# Generate PDF
|
|
result_path = pdf_generator.generate_property_pdf(
|
|
request.property_data.dict(),
|
|
request.template_name,
|
|
pdf_path
|
|
)
|
|
|
|
# Extract filename from the result path
|
|
filename = os.path.basename(result_path)
|
|
|
|
# Store the generated PDF path
|
|
generated_pdfs[filename] = pdf_path
|
|
|
|
# Return the download URL (fix: add /api/ prefix to match the endpoint)
|
|
pdf_url = f"/api/download-pdf/{filename}"
|
|
|
|
return GeneratePDFResponse(
|
|
success=True,
|
|
message="PDF generated successfully",
|
|
pdf_url=pdf_url,
|
|
error=None
|
|
)
|
|
|
|
except Exception as e:
|
|
# Clean up temporary file on error
|
|
if os.path.exists(pdf_path):
|
|
os.unlink(pdf_path)
|
|
raise e
|
|
|
|
except Exception as e:
|
|
return GeneratePDFResponse(
|
|
success=False,
|
|
message="Error generating PDF",
|
|
error=str(e)
|
|
)
|
|
|
|
@app.get("/api/download-pdf/{filename}")
|
|
async def download_pdf(filename: str):
|
|
"""Download generated PDF file"""
|
|
try:
|
|
# Check if the PDF exists in our storage
|
|
if filename in generated_pdfs and os.path.exists(generated_pdfs[filename]):
|
|
pdf_path = generated_pdfs[filename]
|
|
|
|
# Return the actual generated PDF
|
|
return FileResponse(
|
|
pdf_path,
|
|
media_type='application/pdf',
|
|
filename=f"property_brochure_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
|
|
)
|
|
else:
|
|
# Fallback: generate a demo PDF
|
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp_file:
|
|
demo_pdf_path = tmp_file.name
|
|
|
|
# Generate a demo PDF
|
|
demo_data = {
|
|
"propertyName": "Demo Property",
|
|
"propertyType": "Apartment",
|
|
"location": "Dubai",
|
|
"price": "1,500,000",
|
|
"bedrooms": "2",
|
|
"bathrooms": "2",
|
|
"area": "1,200",
|
|
"description": "This is a demo property brochure generated by the Property PDF Generator API.",
|
|
"amenities": ['Swimming Pool', 'Gym', 'Parking'],
|
|
"images": []
|
|
}
|
|
|
|
pdf_generator.generate_property_pdf(demo_data, "modern", demo_pdf_path)
|
|
|
|
# Return the demo file
|
|
return FileResponse(
|
|
demo_pdf_path,
|
|
media_type='application/pdf',
|
|
filename=f"demo_property_brochure.pdf"
|
|
)
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Error downloading PDF: {str(e)}")
|
|
|
|
def generate_preview_html(property_data: PropertyData) -> str:
|
|
"""Generate comprehensive HTML preview of the property brochure with all new data"""
|
|
template_info = AVAILABLE_TEMPLATES.get(property_data.template, {})
|
|
color = template_info.get('preview_color', '#333')
|
|
|
|
# Market trend styling
|
|
trend_color = {
|
|
'rising': '#28a745',
|
|
'stable': '#ffc107',
|
|
'declining': '#dc3545'
|
|
}.get(property_data.marketTrend, '#6c757d')
|
|
|
|
# Investment type styling
|
|
investment_color = {
|
|
'buy-to-live': '#007bff',
|
|
'buy-to-rent': '#28a745',
|
|
'buy-to-sell': '#ffc107'
|
|
}.get(property_data.investmentType, '#6c757d')
|
|
|
|
preview_html = f"""
|
|
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; background: #f8f9fa; border-radius: 15px;">
|
|
<!-- Header Section -->
|
|
<div style="text-align: center; margin-bottom: 40px; background: white; padding: 30px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1);">
|
|
<div style="background: {color}; color: white; padding: 15px; border-radius: 10px; margin-bottom: 20px;">
|
|
<h1 style="margin: 0; font-size: 2.5rem; font-weight: 700;">
|
|
{property_data.propertyName}
|
|
</h1>
|
|
<p style="margin: 10px 0 0; font-size: 1.2rem; opacity: 0.9;">
|
|
{property_data.propertyType} in {property_data.location}
|
|
</p>
|
|
</div>
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px;">
|
|
<div style="background: #e3f2fd; padding: 15px; border-radius: 10px;">
|
|
<h3 style="margin: 0; color: #1976d2;">Template</h3>
|
|
<p style="margin: 5px 0 0; font-weight: 600;">{template_info.get('display_name', 'Unknown')}</p>
|
|
</div>
|
|
<div style="background: #e8f5e8; padding: 15px; border-radius: 10px;">
|
|
<h3 style="margin: 0; color: #388e3c;">Pages</h3>
|
|
<p style="margin: 5px 0 0; font-weight: 600;">{template_info.get('pages', 'N/A')}</p>
|
|
</div>
|
|
<div style="background: #fce4ec; padding: 15px; border-radius: 10px;">
|
|
<h3 style="margin: 0; color: #c2185b;">Layout</h3>
|
|
<p style="margin: 5px 0 0; font-weight: 600;">{property_data.layout or template_info.get('image_grid', 'Default')}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Property Details Grid -->
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 30px;">
|
|
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1);">
|
|
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Property Details</h2>
|
|
<div style="display: grid; gap: 12px;">
|
|
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
|
|
<strong>Type:</strong> <span>{property_data.propertyType}</span>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
|
|
<strong>Location:</strong> <span>{property_data.location}</span>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
|
|
<strong>Price:</strong> <span style="color: {color}; font-weight: 700;">AED {property_data.price}</span>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
|
|
<strong>Bedrooms:</strong> <span>{property_data.bedrooms}</span>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
|
|
<strong>Bathrooms:</strong> <span>{property_data.bathrooms}</span>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between; padding: 8px 0;">
|
|
<strong>Area:</strong> <span>{property_data.area} sq ft</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{f'''
|
|
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1);">
|
|
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Market Analytics</h2>
|
|
<div style="display: grid; gap: 12px;">
|
|
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
|
|
<strong>Market Trend:</strong>
|
|
<span style="color: {trend_color}; font-weight: 600;">
|
|
{property_data.marketTrend.title() if property_data.marketTrend else 'N/A'}
|
|
</span>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
|
|
<strong>ROI Potential:</strong> <span>{property_data.roiPotential}%</span>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
|
|
<strong>Avg Price/sq ft:</strong> <span>AED {property_data.avgPricePerSqft}</span>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between; padding: 8px 0;">
|
|
<strong>Market Demand:</strong>
|
|
<span style="color: {trend_color}; font-weight: 600;">
|
|
{property_data.marketDemand.title() if property_data.marketDemand else 'N/A'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
''' if any([property_data.marketTrend, property_data.roiPotential, property_data.avgPricePerSqft, property_data.marketDemand]) else ''}
|
|
|
|
{f'''
|
|
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1);">
|
|
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Investment Details</h2>
|
|
<div style="display: grid; gap: 12px;">
|
|
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
|
|
<strong>Investment Type:</strong>
|
|
<span style="color: {investment_color}; font-weight: 600;">
|
|
{property_data.investmentType.replace('-', ' ').title() if property_data.investmentType else 'N/A'}
|
|
</span>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between; padding: 8px 0;">
|
|
<strong>Rental Yield:</strong> <span>{property_data.rentalYield}%</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
''' if any([property_data.investmentType, property_data.rentalYield]) else ''}
|
|
</div>
|
|
|
|
{f'''
|
|
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); margin-bottom: 20px;">
|
|
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Property Description</h2>
|
|
<p style="line-height: 1.6; color: #555;">{property_data.description}</p>
|
|
</div>
|
|
''' if property_data.description else ''}
|
|
|
|
{f'''
|
|
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); margin-bottom: 20px;">
|
|
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Location Advantages</h2>
|
|
<p style="line-height: 1.6; color: #555;">{property_data.locationAdvantages}</p>
|
|
</div>
|
|
''' if property_data.locationAdvantages else ''}
|
|
|
|
{f'''
|
|
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); margin-bottom: 20px;">
|
|
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Investment Highlights</h2>
|
|
<p style="line-height: 1.6; color: #555;">{property_data.investmentHighlights}</p>
|
|
</div>
|
|
''' if property_data.investmentHighlights else ''}
|
|
|
|
{f'''
|
|
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); margin-bottom: 20px;">
|
|
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Amenities</h2>
|
|
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
|
|
{''.join([f'<span style="background: {color}; color: white; padding: 8px 16px; border-radius: 20px; font-size: 14px; font-weight: 500;">{amenity}</span>' for amenity in property_data.amenities])}
|
|
</div>
|
|
</div>
|
|
''' if property_data.amenities else ''}
|
|
|
|
{f'''
|
|
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); margin-bottom: 20px;">
|
|
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Content Modules</h2>
|
|
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
|
|
{''.join([f'<span style="background: #e3f2fd; color: #1976d2; padding: 8px 16px; border-radius: 20px; font-size: 14px; font-weight: 500;">{module.replace("-", " ").title()}</span>' for module in property_data.contentModules])}
|
|
</div>
|
|
</div>
|
|
''' if property_data.contentModules else ''}
|
|
|
|
{f'''
|
|
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); margin-bottom: 20px;">
|
|
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Additional Content</h2>
|
|
<p style="line-height: 1.6; color: #555;">{property_data.additionalContent}</p>
|
|
</div>
|
|
''' if property_data.additionalContent else ''}
|
|
|
|
<!-- Footer -->
|
|
<div style="text-align: center; color: #999; font-style: italic; margin-top: 40px; padding: 20px; background: white; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1);">
|
|
<h3 style="color: {color}; margin-bottom: 10px;">Preview Information</h3>
|
|
<p style="margin: 5px 0;">Template: {template_info.get('display_name', 'Unknown')}</p>
|
|
<p style="margin: 5px 0;">Pages: {template_info.get('pages', 'N/A')} | Layout: {property_data.layout or template_info.get('image_grid', 'Default')}</p>
|
|
<p style="margin: 15px 0 5px; font-weight: 600; color: #555;">This is a preview. The final PDF will be generated with professional styling and layouts.</p>
|
|
</div>
|
|
</div>
|
|
"""
|
|
|
|
return preview_html
|
|
|
|
@app.post("/api/upload-images")
|
|
async def upload_images(files: List[UploadFile] = File(...)):
|
|
"""Upload property images"""
|
|
try:
|
|
uploaded_images = []
|
|
|
|
for file in files:
|
|
if file.content_type.startswith('image/'):
|
|
# Read image content
|
|
content = await file.read()
|
|
|
|
# Convert to base64 for storage
|
|
import base64
|
|
base64_content = base64.b64encode(content).decode('utf-8')
|
|
|
|
uploaded_images.append({
|
|
"filename": file.filename,
|
|
"content_type": file.content_type,
|
|
"size": len(content),
|
|
"base64_data": f"data:{file.content_type};base64,{base64_content}"
|
|
})
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"Successfully uploaded {len(uploaded_images)} images",
|
|
"images": uploaded_images
|
|
}
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Error uploading images: {str(e)}")
|
|
|
|
if __name__ == "__main__":
|
|
# Run the server
|
|
uvicorn.run(
|
|
"api_server:app",
|
|
host="0.0.0.0",
|
|
port=8000,
|
|
reload=True,
|
|
log_level="info"
|
|
) |