v1.0.0-alpha
This commit is contained in:
commit
e284e4ace6
64
.gitignore
vendored
Normal file
64
.gitignore
vendored
Normal file
@ -0,0 +1,64 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Virtual Environment
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Generated PDFs
|
||||
generated_pdfs/
|
||||
*.pdf
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
temp/
|
||||
tmp/
|
||||
|
||||
# Salesforce
|
||||
.sf/
|
||||
.sfdx/
|
||||
*.zip
|
||||
122
DEPLOYMENT.md
Normal file
122
DEPLOYMENT.md
Normal file
@ -0,0 +1,122 @@
|
||||
# 🚀 Production Deployment Guide
|
||||
|
||||
## 📋 **Prerequisites**
|
||||
|
||||
- Salesforce CLI (sf) installed
|
||||
- Python 3.8+ on your server
|
||||
- Access to your Salesforce sandbox
|
||||
|
||||
## 🎯 **Step 1: Deploy LWC to Salesforce**
|
||||
|
||||
```bash
|
||||
# Make script executable
|
||||
chmod +x deploy-lwc-production.sh
|
||||
|
||||
# Run deployment
|
||||
./deploy-lwc-production.sh
|
||||
```
|
||||
|
||||
**Expected Output:**
|
||||
```
|
||||
✅ LWC deployment successful!
|
||||
✅ Custom objects deployed!
|
||||
✅ Permission set created!
|
||||
✅ Lightning App Page created!
|
||||
```
|
||||
|
||||
## 🌐 **Step 2: Deploy PDF API to Your Server**
|
||||
|
||||
```bash
|
||||
# On your server
|
||||
cd python-pdf-generator
|
||||
|
||||
# Create virtual environment
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Start API server
|
||||
python3 api_server.py
|
||||
```
|
||||
|
||||
**Server will start on:** `http://0.0.0.0:8000`
|
||||
|
||||
## 🔧 **Step 3: Configure LWC with Your API URL**
|
||||
|
||||
Edit `force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js`:
|
||||
|
||||
```javascript
|
||||
// Change this line
|
||||
pdfApiBaseUrl = 'https://YOUR-ACTUAL-IP:8000/api';
|
||||
```
|
||||
|
||||
**Replace `YOUR-ACTUAL-IP` with your server's IP address.**
|
||||
|
||||
## 🔒 **Step 4: Security Configuration**
|
||||
|
||||
### **Firewall Setup:**
|
||||
```bash
|
||||
# Open port 8000
|
||||
sudo ufw allow 8000
|
||||
```
|
||||
|
||||
### **CORS Configuration (if needed):**
|
||||
Edit `python-pdf-generator/api_server.py`:
|
||||
```python
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[
|
||||
"https://tso3--r1.sandbox.lightning.force.com",
|
||||
"https://your-salesforce-domain.com"
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
```
|
||||
|
||||
## 🧪 **Step 5: Test Your Deployment**
|
||||
|
||||
1. **Open Salesforce Sandbox:** `https://tso3--r1.sandbox.lightning.force.com`
|
||||
2. **Login:** `contact+tso3@propertycrm.ae.r1` / `Demo@123`
|
||||
3. **Search for:** "Property Brochure Generator"
|
||||
4. **Test the flow:** Template → Property → Preview → Download
|
||||
|
||||
## 📊 **Expected Results**
|
||||
|
||||
- ✅ LWC loads in Salesforce
|
||||
- ✅ Properties load from your data
|
||||
- ✅ PDF preview generates
|
||||
- ✅ PDF downloads successfully
|
||||
- ✅ All 23+ properties accessible
|
||||
|
||||
## 🚨 **Troubleshooting**
|
||||
|
||||
### **LWC Not Loading:**
|
||||
- Check deployment logs
|
||||
- Verify permission sets
|
||||
- Check user access
|
||||
|
||||
### **API Connection Failed:**
|
||||
- Verify IP address in LWC
|
||||
- Check firewall settings
|
||||
- Ensure API server is running
|
||||
|
||||
### **No Properties Found:**
|
||||
- Verify `pcrm__Property__c` object exists
|
||||
- Check field permissions
|
||||
- Verify data in sandbox
|
||||
|
||||
## 📞 **Support**
|
||||
|
||||
For deployment issues:
|
||||
1. Check Salesforce CLI logs
|
||||
2. Verify API server status
|
||||
3. Check browser console for errors
|
||||
4. Ensure all prerequisites are met
|
||||
|
||||
---
|
||||
|
||||
**🎯 Your system is now production-ready!**
|
||||
137
PRODUCTION-CHECKLIST.md
Normal file
137
PRODUCTION-CHECKLIST.md
Normal file
@ -0,0 +1,137 @@
|
||||
# 🚀 Production Deployment Checklist - Property Brochure Generator LWC
|
||||
|
||||
## 📋 **Pre-Deployment Checklist**
|
||||
|
||||
### **✅ LWC Component Ready**
|
||||
- [ ] All LLM-style emojis removed from UI
|
||||
- [ ] Professional SVG icons implemented
|
||||
- [ ] Error handling and validation implemented
|
||||
- [ ] Toast notifications configured
|
||||
- [ ] Loading states implemented
|
||||
- [ ] Responsive design tested
|
||||
|
||||
### **✅ Configuration Updated**
|
||||
- [ ] PDF API URL updated in `propertyTemplateSelector.js`
|
||||
- [ ] PDF API URL updated in `PropertyTemplateController.cls`
|
||||
- [ ] Production configuration file created
|
||||
- [ ] Error messages customized for production
|
||||
- [ ] Success messages customized for production
|
||||
|
||||
### **✅ Code Quality**
|
||||
- [ ] No console.log statements in production code
|
||||
- [ ] Error boundaries implemented
|
||||
- [ ] Performance optimizations applied
|
||||
- [ ] Accessibility features implemented
|
||||
- [ ] Cross-browser compatibility tested
|
||||
|
||||
## 🌐 **PDF API Server Deployment**
|
||||
|
||||
### **✅ Server Configuration**
|
||||
- [ ] Python API server deployed to your IP
|
||||
- [ ] Port 8000 opened in firewall
|
||||
- [ ] HTTPS configured (recommended for production)
|
||||
- [ ] CORS configured for Salesforce domains
|
||||
- [ ] Environment variables set
|
||||
|
||||
### **✅ API Endpoints Working**
|
||||
- [ ] `/api/health` - Health check endpoint
|
||||
- [ ] `/api/preview` - PDF preview generation
|
||||
- [ ] `/api/generate-pdf` - PDF generation and download
|
||||
- [ ] `/api/templates` - Available templates
|
||||
|
||||
## 🔧 **Salesforce Configuration**
|
||||
|
||||
### **✅ Custom Objects Deployed**
|
||||
- [ ] `Property_Template__c` object deployed
|
||||
- [ ] `Property__c` object fields updated
|
||||
- [ ] Permission sets configured
|
||||
- [ ] User access granted
|
||||
|
||||
### **✅ LWC Deployment**
|
||||
- [ ] Component deployed to sandbox
|
||||
- [ ] Lightning App Page created
|
||||
- [ ] Component added to page layouts
|
||||
- [ ] User permissions verified
|
||||
|
||||
## 📱 **Testing Checklist**
|
||||
|
||||
### **✅ Functionality Testing**
|
||||
- [ ] Template selection working
|
||||
- [ ] Property data loading from Salesforce
|
||||
- [ ] Form validation working
|
||||
- [ ] Image upload functionality
|
||||
- [ ] PDF preview generation
|
||||
- [ ] PDF download working
|
||||
|
||||
### **✅ User Experience Testing**
|
||||
- [ ] 5-step wizard flow smooth
|
||||
- [ ] Error messages clear and helpful
|
||||
- [ ] Loading states informative
|
||||
- [ ] Responsive on mobile devices
|
||||
- [ ] Accessibility features working
|
||||
|
||||
### **✅ Integration Testing**
|
||||
- [ ] Salesforce data integration working
|
||||
- [ ] PDF API communication successful
|
||||
- [ ] Error handling graceful
|
||||
- [ ] Performance acceptable
|
||||
|
||||
## 🚨 **Production Security**
|
||||
|
||||
### **✅ Security Measures**
|
||||
- [ ] API endpoints secured
|
||||
- [ ] CORS properly configured
|
||||
- [ ] Input validation implemented
|
||||
- [ ] File upload restrictions set
|
||||
- [ ] Error messages don't expose sensitive data
|
||||
|
||||
### **✅ Monitoring & Logging**
|
||||
- [ ] Error logging configured
|
||||
- [ ] Performance monitoring enabled
|
||||
- [ ] User activity tracking
|
||||
- [ ] API usage monitoring
|
||||
|
||||
## 📊 **Performance Optimization**
|
||||
|
||||
### **✅ Performance Settings**
|
||||
- [ ] Image compression enabled
|
||||
- [ ] PDF generation optimized
|
||||
- [ ] Caching implemented
|
||||
- [ ] Database queries optimized
|
||||
- [ ] API response times acceptable
|
||||
|
||||
## 🔄 **Post-Deployment**
|
||||
|
||||
### **✅ Verification**
|
||||
- [ ] All users can access component
|
||||
- [ ] PDF generation working for all templates
|
||||
- [ ] Error handling working correctly
|
||||
- [ ] Performance meets requirements
|
||||
|
||||
### **✅ Documentation**
|
||||
- [ ] User manual created
|
||||
- [ ] Admin guide prepared
|
||||
- [ ] Troubleshooting guide available
|
||||
- [ ] Support contact information provided
|
||||
|
||||
## 📞 **Support & Maintenance**
|
||||
|
||||
### **✅ Support Plan**
|
||||
- [ ] Support team trained
|
||||
- [ ] Escalation procedures defined
|
||||
- [ ] Maintenance schedule planned
|
||||
- [ ] Backup and recovery procedures
|
||||
|
||||
---
|
||||
|
||||
## 🎯 **Final Steps Before Go-Live**
|
||||
|
||||
1. **Update PDF API URL** in both LWC and Apex controller
|
||||
2. **Test complete workflow** end-to-end
|
||||
3. **Verify user permissions** and access
|
||||
4. **Monitor system performance** for first 24 hours
|
||||
5. **Provide user training** and documentation
|
||||
|
||||
---
|
||||
|
||||
**🎉 Your Property Brochure Generator is now Production Ready!**
|
||||
93
README.md
Normal file
93
README.md
Normal file
@ -0,0 +1,93 @@
|
||||
# 🏠 Property Brochure Generator
|
||||
|
||||
Professional PDF generation system for real estate properties with Salesforce integration.
|
||||
|
||||
## 🚀 **Quick Start**
|
||||
|
||||
### **1. Deploy LWC to Salesforce**
|
||||
```bash
|
||||
chmod +x deploy-lwc-production.sh
|
||||
./deploy-lwc-production.sh
|
||||
```
|
||||
|
||||
### **2. Deploy PDF API to Your Server**
|
||||
```bash
|
||||
cd python-pdf-generator
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
python3 api_server.py
|
||||
```
|
||||
|
||||
### **3. Update LWC with Your API URL**
|
||||
Edit these files with your actual server IP:
|
||||
|
||||
**LWC JavaScript:**
|
||||
```javascript
|
||||
// force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js
|
||||
pdfApiBaseUrl = 'https://YOUR-ACTUAL-IP:8000/api';
|
||||
```
|
||||
|
||||
**Apex Controller:**
|
||||
```apex
|
||||
// force-app/main/default/classes/PropertyTemplateController.cls
|
||||
String apiEndpoint = 'https://YOUR-ACTUAL-IP:8000/api/generate-pdf';
|
||||
```
|
||||
|
||||
**Production Config:**
|
||||
```javascript
|
||||
// force-app/main/default/lwc/propertyTemplateSelector/production-config.js
|
||||
PDF_API_BASE_URL: 'https://YOUR-ACTUAL-IP:8000/api'
|
||||
```
|
||||
|
||||
## 📁 **Project Structure**
|
||||
|
||||
```
|
||||
├── force-app/ # Salesforce LWC Components
|
||||
│ └── main/default/
|
||||
│ ├── lwc/propertyTemplateSelector/ # Main LWC Component
|
||||
│ ├── classes/PropertyTemplateController.cls # Apex Controller
|
||||
│ └── objects/ # Custom Objects
|
||||
├── python-pdf-generator/ # PDF Generation API
|
||||
│ ├── api_server.py # FastAPI Server
|
||||
│ ├── property_pdf_generator.py # PDF Generation Logic
|
||||
│ └── requirements.txt # Python Dependencies
|
||||
├── deploy-lwc-production.sh # LWC Deployment Script
|
||||
└── README.md # This File
|
||||
```
|
||||
|
||||
## 🔧 **Features**
|
||||
|
||||
- **5-Step Wizard**: Template → Property → Details → Preview → Download
|
||||
- **Multiple Templates**: 1-page, 3-page, 5-page, Luxury, Modern
|
||||
- **Real-time Preview**: Instant customization updates
|
||||
- **Image Management**: Multiple images with room names
|
||||
- **Salesforce Integration**: Direct access to pcrm__Property__c data
|
||||
- **Responsive Design**: Works on all devices
|
||||
|
||||
## 📊 **API Endpoints**
|
||||
|
||||
- **Base URL**: `https://YOUR-IP:8000/api`
|
||||
- **Preview**: `POST /preview`
|
||||
- **Generate PDF**: `POST /generate-pdf`
|
||||
- **Health Check**: `GET /health`
|
||||
- **Templates**: `GET /templates`
|
||||
|
||||
## 🔒 **Security**
|
||||
|
||||
- Configure CORS for your Salesforce domain
|
||||
- Use HTTPS in production
|
||||
- Implement authentication if needed
|
||||
- Configure firewall for port 8000
|
||||
|
||||
## 📞 **Support**
|
||||
|
||||
For issues or questions, check the deployment logs and ensure:
|
||||
- Salesforce CLI is installed
|
||||
- Python dependencies are installed
|
||||
- API server is running on your IP
|
||||
- LWC has correct API URL
|
||||
|
||||
---
|
||||
|
||||
**🎯 Production-ready system for generating professional property brochures!**
|
||||
121
deploy-lwc-production.sh
Normal file
121
deploy-lwc-production.sh
Normal file
@ -0,0 +1,121 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🚀 LWC Production Deployment to Salesforce Sandbox"
|
||||
echo "=================================================="
|
||||
echo ""
|
||||
|
||||
# Configuration
|
||||
SANDBOX_URL="https://tso3--r1.sandbox.lightning.force.com"
|
||||
USERNAME="contact+tso3@propertycrm.ae.r1"
|
||||
PASSWORD="Demo@123"
|
||||
|
||||
# Check if Salesforce CLI is installed
|
||||
if ! command -v sf &> /dev/null; then
|
||||
echo "❌ Salesforce CLI (sf) is not installed!"
|
||||
echo ""
|
||||
echo "📋 Please install Salesforce CLI first:"
|
||||
echo " 1. Visit: https://developer.salesforce.com/tools/sfdxcli"
|
||||
echo " 2. Install the CLI for your operating system"
|
||||
echo " 3. Run this script again"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Salesforce CLI found"
|
||||
echo ""
|
||||
|
||||
# Check if already authenticated
|
||||
echo "🔐 Checking authentication status..."
|
||||
if sf org list --json | grep -q "tso3--r1"; then
|
||||
echo "✅ Already authenticated to sandbox: tso3--r1"
|
||||
ORG_ALIAS="tso3--r1"
|
||||
else
|
||||
echo "⚠️ Not authenticated to sandbox"
|
||||
echo ""
|
||||
echo "🔑 Authenticating to sandbox..."
|
||||
|
||||
# Authenticate to sandbox
|
||||
sf org login web --instance-url "$SANDBOX_URL" --alias "tso3--r1" --set-default-dev-hub
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Authentication successful!"
|
||||
ORG_ALIAS="tso3--r1"
|
||||
else
|
||||
echo "❌ Authentication failed!"
|
||||
echo "Please check your credentials and try again."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🏗️ Deploying LWC components to sandbox..."
|
||||
|
||||
# Deploy the source code
|
||||
echo "📦 Deploying source code..."
|
||||
sf project deploy start --source-dir force-app --target-org "$ORG_ALIAS"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ LWC deployment successful!"
|
||||
else
|
||||
echo "❌ LWC deployment failed!"
|
||||
echo "Please check the error messages above."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🔧 Setting up custom objects and fields..."
|
||||
|
||||
# Deploy custom objects
|
||||
echo "📊 Deploying Property Template object..."
|
||||
sf project deploy start --source-dir force-app/main/default/objects --target-org "$ORG_ALIAS"
|
||||
|
||||
echo "📋 Deploying Property object fields..."
|
||||
sf project deploy start --source-dir force-app/main/default/objects/Property__c --target-org "$ORG_ALIAS"
|
||||
|
||||
echo "📋 Deploying Property Template object fields..."
|
||||
sf project deploy start --source-dir force-app/main/default/objects/Property_Template__c --target-org "$ORG_ALIAS"
|
||||
|
||||
echo ""
|
||||
echo "🎯 Setting up permission sets..."
|
||||
|
||||
# Create permission set for the LWC
|
||||
echo "🔐 Creating permission set..."
|
||||
sf data upsert --sobjecttype PermissionSet --sobjectid Id --external-id Name --values "Name='Property_Brochure_Generator_Access' Label='Property Brochure Generator Access' Description='Access to Property Brochure Generator LWC'" --target-org "$ORG_ALIAS"
|
||||
|
||||
# Assign permissions to the permission set
|
||||
echo "🔑 Assigning permissions..."
|
||||
sf data upsert --sobjecttype PermissionSet --sobjectid Id --external-id Name --values "Name='Property_Brochure_Generator_Access' pcrm__Property__c=true pcrm__Property_Template__c=true" --target-org "$ORG_ALIAS"
|
||||
|
||||
echo ""
|
||||
echo "📱 Setting up Lightning App Page..."
|
||||
|
||||
# Create a Lightning App Page
|
||||
echo "📄 Creating Lightning App Page..."
|
||||
sf data upsert --sobjecttype LightningPage --sobjectid Id --external-id DeveloperName --values "DeveloperName='Property_Brochure_Generator' MasterLabel='Property Brochure Generator' LightningComponent__c='propertyTemplateSelector' IsAvailableInTouch=true IsAvailableInLightning=true IsAvailableInClassic=true" --target-org "$ORG_ALIAS"
|
||||
|
||||
echo ""
|
||||
echo "🎉 Deployment completed successfully!"
|
||||
echo "===================================="
|
||||
echo ""
|
||||
echo "📋 What was deployed:"
|
||||
echo " ✅ LWC Component: propertyTemplateSelector"
|
||||
echo " ✅ Apex Controller: PropertyTemplateController"
|
||||
echo " ✅ Custom Objects: Property__c, Property_Template__c"
|
||||
echo " ✅ Permission Set: Property_Brochure_Generator_Access"
|
||||
echo " ✅ Lightning App Page: Property Brochure Generator"
|
||||
echo ""
|
||||
echo "🚀 Next Steps:"
|
||||
echo " 1. Open your sandbox: $SANDBOX_URL"
|
||||
echo " 2. Login with: $USERNAME / $PASSWORD"
|
||||
echo " 3. Go to Setup → App Manager → Property Brochure Generator"
|
||||
echo " 4. Or search for 'Property Brochure Generator' in the app launcher"
|
||||
echo ""
|
||||
echo "🔧 Important Configuration:"
|
||||
echo " ⚠️ Update the PDF API URL in the LWC JavaScript file:"
|
||||
echo " - Open: force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js"
|
||||
echo " - Change: pdfApiBaseUrl = 'https://your-ip-address:8000/api'"
|
||||
echo " - Replace 'your-ip-address' with your actual server IP"
|
||||
echo ""
|
||||
echo "📖 For PDF generation API setup, see: README.md"
|
||||
echo ""
|
||||
echo "🎯 Your LWC is now ready for production use!"
|
||||
153
force-app/main/default/classes/PropertyTemplateController.cls
Normal file
153
force-app/main/default/classes/PropertyTemplateController.cls
Normal file
@ -0,0 +1,153 @@
|
||||
public with sharing class PropertyTemplateController {
|
||||
|
||||
@AuraEnabled(cacheable=true)
|
||||
public static List<Property_Template__c> getPropertyTemplates() {
|
||||
try {
|
||||
return [SELECT Id, Name, Description__c, Preview_Image_URL__c, Tags__c,
|
||||
Is_Active__c, Template_Definition__c
|
||||
FROM Property_Template__c
|
||||
WHERE Is_Active__c = true
|
||||
ORDER BY Name];
|
||||
} catch (Exception e) {
|
||||
throw new AuraHandledException('Error fetching templates: ' + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@AuraEnabled(cacheable=true)
|
||||
public static Property_Template__c getPropertyTemplateById(String templateId) {
|
||||
try {
|
||||
return [SELECT Id, Name, Description__c, Preview_Image_URL__c, Tags__c,
|
||||
Is_Active__c, Template_Definition__c
|
||||
FROM Property_Template__c
|
||||
WHERE Id = :templateId AND Is_Active__c = true
|
||||
LIMIT 1];
|
||||
} catch (Exception e) {
|
||||
throw new AuraHandledException('Error fetching template: ' + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@AuraEnabled(cacheable=true)
|
||||
public static List<pcrm__Property__c> getPropertyData() {
|
||||
try {
|
||||
return [SELECT Id, Name, pcrm__Property_Type__c, pcrm__Sub_Locality_Bayut_Dubizzle__c,
|
||||
pcrm__Sale_Price_max__c, pcrm__Rent_Price_max__c, pcrm__Bedrooms__c,
|
||||
pcrm__Bathrooms__c, pcrm__Size__c, pcrm__Description_English__c,
|
||||
pcrm__Title_English__c, pcrm__Unit_Number__c, pcrm__Completion_Status__c,
|
||||
pcrm__Furnished__c, pcrm__View__c, pcrm__Tower_Bayut_Dubizzle__c,
|
||||
pcrm__Community_Propertyfinder__c, pcrm__Sub_Community_Propertyfinder__c,
|
||||
pcrm__City_Propertyfinder__c, pcrm__City_Bayut_Dubizzle__c
|
||||
FROM pcrm__Property__c
|
||||
ORDER BY Name];
|
||||
} catch (Exception e) {
|
||||
throw new AuraHandledException('Error fetching properties: ' + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@AuraEnabled(cacheable=true)
|
||||
public static pcrm__Property__c getPropertyById(String propertyId) {
|
||||
try {
|
||||
return [SELECT Id, Name, pcrm__Property_Type__c, pcrm__Sub_Locality_Bayut_Dubizzle__c,
|
||||
pcrm__Sale_Price_max__c, pcrm__Rent_Price_max__c, pcrm__Bedrooms__c,
|
||||
pcrm__Bathrooms__c, pcrm__Size__c, pcrm__Description_English__c,
|
||||
pcrm__Title_English__c, pcrm__Unit_Number__c, pcrm__Completion_Status__c,
|
||||
pcrm__Furnished__c, pcrm__View__c, pcrm__Tower_Bayut_Dubizzle__c,
|
||||
pcrm__Community_Propertyfinder__c, pcrm__Sub_Community_Propertyfinder__c,
|
||||
pcrm__City_Propertyfinder__c, pcrm__City_Bayut_Dubizzle__c,
|
||||
pcrm__Private_Amenities__c, pcrm__Commercial_Amenities__c,
|
||||
pcrm__Coordinates__c, pcrm__Build_Year__c, pcrm__Stories__c,
|
||||
pcrm__Parking_Spaces__c, pcrm__Lot_Size__c, pcrm__Service_Charge__c
|
||||
FROM pcrm__Property__c
|
||||
WHERE Id = :propertyId
|
||||
LIMIT 1];
|
||||
} catch (Exception e) {
|
||||
throw new AuraHandledException('Error fetching property: ' + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@AuraEnabled(cacheable=true)
|
||||
public static Map<String, Object> getMarketData() {
|
||||
try {
|
||||
// For now, return default market data since you might not have Market_Analytics__c
|
||||
Map<String, Object> result = new Map<String, Object>();
|
||||
|
||||
// Default Dubai market data
|
||||
result.put('marketTrend', 'Rising');
|
||||
result.put('roiPotential', '8.5');
|
||||
result.put('avgPricePerSqft', '1200');
|
||||
result.put('marketDemand', 'High');
|
||||
result.put('investmentType', 'Buy-to-Rent');
|
||||
result.put('rentalYield', '6.2');
|
||||
result.put('investmentHighlights', 'Prime location with high rental demand and capital appreciation potential');
|
||||
result.put('locationAdvantages', 'Excellent connectivity, premium amenities, and strong infrastructure');
|
||||
result.put('contentModules', ['Market Analysis', 'Investment Overview', 'Location Highlights']);
|
||||
result.put('additionalContent', 'Dubai real estate market shows strong growth potential with government initiatives and Expo 2020 legacy');
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
throw new AuraHandledException('Error fetching market data: ' + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@AuraEnabled
|
||||
public static Map<String, Object> generatePropertyPDF(String propertyData, String templateName, Boolean generatePDF) {
|
||||
try {
|
||||
// Parse property data
|
||||
Map<String, Object> propertyMap = (Map<String, Object>) JSON.deserializeUntyped(propertyData);
|
||||
|
||||
// Call external Python API for PDF generation
|
||||
String apiEndpoint = 'https://YOUR-ACTUAL-IP:8000/api/generate-pdf'; // TODO: Replace with your actual server IP
|
||||
|
||||
// Prepare request body
|
||||
Map<String, Object> requestBody = new Map<String, Object>();
|
||||
requestBody.put('property_data', propertyMap);
|
||||
requestBody.put('template_name', templateName);
|
||||
|
||||
// Make HTTP callout to Python API
|
||||
Http http = new Http();
|
||||
HttpRequest request = new HttpRequest();
|
||||
request.setEndpoint(apiEndpoint);
|
||||
request.setMethod('POST');
|
||||
request.setHeader('Content-Type', 'application/json');
|
||||
request.setBody(JSON.serialize(requestBody));
|
||||
|
||||
HttpResponse response = http.send(request);
|
||||
|
||||
if (response.getStatusCode() == 200) {
|
||||
Map<String, Object> responseMap = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
|
||||
|
||||
Map<String, Object> result = new Map<String, Object>();
|
||||
result.put('success', true);
|
||||
result.put('pdfUrl', responseMap.get('pdf_url'));
|
||||
result.put('message', 'PDF generated successfully');
|
||||
|
||||
return result;
|
||||
} else {
|
||||
Map<String, Object> result = new Map<String, Object>();
|
||||
result.put('success', false);
|
||||
result.put('message', 'Failed to generate PDF: ' + response.getStatus());
|
||||
return result;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Map<String, Object> result = new Map<String, Object>();
|
||||
result.put('success', false);
|
||||
result.put('message', 'Error: ' + e.getMessage());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@AuraEnabled
|
||||
public static List<Property_Template__c> searchPropertyTemplates(String searchTerm) {
|
||||
try {
|
||||
String searchQuery = '%' + searchTerm + '%';
|
||||
return [SELECT Id, Name, Description__c, Preview_Image_URL__c, Tags__c,
|
||||
Is_Active__c, Template_Definition__c
|
||||
FROM Property_Template__c
|
||||
WHERE Is_Active__c = true
|
||||
AND (Name LIKE :searchQuery OR Description__c LIKE :searchQuery OR Tags__c LIKE :searchQuery)
|
||||
ORDER BY Name];
|
||||
} catch (Exception e) {
|
||||
throw new AuraHandledException('Error searching templates: ' + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
50
force-app/main/default/data/Property_Template__c.json
Normal file
50
force-app/main/default/data/Property_Template__c.json
Normal file
@ -0,0 +1,50 @@
|
||||
[
|
||||
{
|
||||
"Name": "Modern Apartment",
|
||||
"Description__c": "Contemporary apartment template with clean lines and modern aesthetics. Perfect for showcasing urban living spaces in Dubai's vibrant neighborhoods.",
|
||||
"Preview_Image_URL__c": "https://images.unsplash.com/photo-1545324418-cc1a3fa10c00?w=400&h=300&fit=crop",
|
||||
"Tags__c": "Modern, Apartment, Urban, Contemporary",
|
||||
"Is_Active__c": true,
|
||||
"Template_Definition__c": "{\"layout\":\"modern\",\"colorScheme\":\"blue\",\"fontFamily\":\"Arial\"}"
|
||||
},
|
||||
{
|
||||
"Name": "Luxury Villa",
|
||||
"Description__c": "Premium villa template featuring elegant design elements and spacious layouts. Ideal for high-end properties in exclusive Dubai communities.",
|
||||
"Preview_Image_URL__c": "https://images.unsplash.com/photo-1613490493576-7fde63acd811?w=400&h=300&fit=crop",
|
||||
"Tags__c": "Luxury, Villa, Premium, Spacious",
|
||||
"Is_Active__c": true,
|
||||
"Template_Definition__c": "{\"layout\":\"luxury\",\"colorScheme\":\"gold\",\"fontFamily\":\"Times New Roman\"}"
|
||||
},
|
||||
{
|
||||
"Name": "Premium Penthouse",
|
||||
"Description__c": "Exclusive penthouse template with sophisticated styling and panoramic views emphasis. Perfect for Dubai's most prestigious properties.",
|
||||
"Preview_Image_URL__c": "https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?w=400&h=300&fit=crop",
|
||||
"Tags__c": "Premium, Penthouse, Exclusive, Sophisticated",
|
||||
"Is_Active__c": true,
|
||||
"Template_Definition__c": "{\"layout\":\"premium\",\"colorScheme\":\"black\",\"fontFamily\":\"Helvetica\"}"
|
||||
},
|
||||
{
|
||||
"Name": "Family Townhouse",
|
||||
"Description__c": "Family-friendly townhouse template with warm, inviting design. Great for suburban Dubai communities and family-oriented properties.",
|
||||
"Preview_Image_URL__c": "https://images.unsplash.com/photo-1564013799919-ab600027ffc6?w=400&h=300&fit=crop",
|
||||
"Tags__c": "Family, Townhouse, Warm, Inviting",
|
||||
"Is_Active__c": true,
|
||||
"Template_Definition__c": "{\"layout\":\"family\",\"colorScheme\":\"warm\",\"fontFamily\":\"Georgia\"}"
|
||||
},
|
||||
{
|
||||
"Name": "Business Office",
|
||||
"Description__c": "Professional office template with corporate styling and business-focused layout. Perfect for commercial properties in Dubai's business districts.",
|
||||
"Preview_Image_URL__c": "https://images.unsplash.com/photo-1497366216548-37526070297c?w=400&h=300&fit=crop",
|
||||
"Tags__c": "Business, Office, Corporate, Professional",
|
||||
"Is_Active__c": true,
|
||||
"Template_Definition__c": "{\"layout\":\"business\",\"colorScheme\":\"gray\",\"fontFamily\":\"Arial\"}"
|
||||
},
|
||||
{
|
||||
"Name": "Retail Space",
|
||||
"Description__c": "Retail property template designed for commercial spaces and shopping areas. Ideal for Dubai's retail and entertainment districts.",
|
||||
"Preview_Image_URL__c": "https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=400&h=300&fit=crop",
|
||||
"Tags__c": "Retail, Commercial, Shopping, Entertainment",
|
||||
"Is_Active__c": true,
|
||||
"Template_Definition__c": "{\"layout\":\"retail\",\"colorScheme\":\"vibrant\",\"fontFamily\":\"Arial\"}"
|
||||
}
|
||||
]
|
||||
@ -0,0 +1,69 @@
|
||||
// Production Configuration for Property Brochure Generator LWC
|
||||
// Update these values before deploying to production
|
||||
|
||||
export const PRODUCTION_CONFIG = {
|
||||
// PDF Generation API Configuration
|
||||
PDF_API_BASE_URL: 'https://YOUR-ACTUAL-IP:8000/api', // Replace with your actual server IP
|
||||
|
||||
// Salesforce API Configuration
|
||||
SALESFORCE_API_VERSION: 'v59.0',
|
||||
|
||||
// PDF Generation Settings
|
||||
PDF_SETTINGS: {
|
||||
maxImageSize: 5 * 1024 * 1024, // 5MB
|
||||
supportedImageFormats: ['jpg', 'jpeg', 'png', 'webp'],
|
||||
maxImagesPerBrochure: 20,
|
||||
defaultPageSize: 'A4',
|
||||
defaultOrientation: 'portrait'
|
||||
},
|
||||
|
||||
// Error Handling
|
||||
ERROR_MESSAGES: {
|
||||
API_CONNECTION_FAILED: 'Unable to connect to PDF generation service. Please try again later.',
|
||||
PROPERTY_LOAD_FAILED: 'Failed to load property data. Please refresh and try again.',
|
||||
TEMPLATE_LOAD_FAILED: 'Failed to load templates. Please refresh and try again.',
|
||||
PDF_GENERATION_FAILED: 'PDF generation failed. Please check your data and try again.',
|
||||
IMAGE_UPLOAD_FAILED: 'Image upload failed. Please check file format and size.',
|
||||
VALIDATION_FAILED: 'Please complete all required fields before proceeding.'
|
||||
},
|
||||
|
||||
// Success Messages
|
||||
SUCCESS_MESSAGES: {
|
||||
PROPERTY_LOADED: 'Property data loaded successfully',
|
||||
TEMPLATE_SELECTED: 'Template selected successfully',
|
||||
PREVIEW_GENERATED: 'Preview generated successfully',
|
||||
PDF_DOWNLOADED: 'PDF downloaded successfully',
|
||||
BROCHURE_CREATED: 'Property brochure created successfully'
|
||||
},
|
||||
|
||||
// Validation Rules
|
||||
VALIDATION: {
|
||||
requiredFields: ['propertyName', 'propertyType', 'location', 'price', 'description'],
|
||||
minPrice: 100000, // Minimum price in AED
|
||||
maxPrice: 100000000, // Maximum price in AED
|
||||
minArea: 100, // Minimum area in sq ft
|
||||
maxArea: 50000 // Maximum area in sq ft
|
||||
},
|
||||
|
||||
// Performance Settings
|
||||
PERFORMANCE: {
|
||||
debounceDelay: 300, // ms
|
||||
maxRetries: 3,
|
||||
timeout: 30000 // 30 seconds
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get configuration value
|
||||
export function getConfig(key) {
|
||||
return PRODUCTION_CONFIG[key] || null;
|
||||
}
|
||||
|
||||
// Helper function to get error message
|
||||
export function getErrorMessage(key) {
|
||||
return PRODUCTION_CONFIG.ERROR_MESSAGES[key] || 'An unexpected error occurred.';
|
||||
}
|
||||
|
||||
// Helper function to get success message
|
||||
export function getSuccessMessage(key) {
|
||||
return PRODUCTION_CONFIG.SUCCESS_MESSAGES[key] || 'Operation completed successfully.';
|
||||
}
|
||||
@ -0,0 +1,321 @@
|
||||
.progress-container {
|
||||
margin-bottom: 2rem;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: #e9ecef;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #007bff, #0056b3);
|
||||
transition: width 0.3s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.step-indicators {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.step {
|
||||
padding: 0.5rem 1rem;
|
||||
background: #fff;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 20px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #6c757d;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.step.active {
|
||||
background: #007bff;
|
||||
border-color: #007bff;
|
||||
color: #fff;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.step-content {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.step-content h2 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.step-content p {
|
||||
color: #6c757d;
|
||||
margin-bottom: 2rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Template Grid */
|
||||
.template-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.template-card {
|
||||
background: #fff;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.template-card:hover {
|
||||
border-color: #007bff;
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 25px rgba(0, 123, 255, 0.15);
|
||||
}
|
||||
|
||||
.template-preview {
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.template-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.template-card:hover .template-preview img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.template-info {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.template-info h3 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.template-info p {
|
||||
color: #6c757d;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.template-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Form Styling */
|
||||
.form-container {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
background: #fff;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-section h3 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.125rem;
|
||||
border-bottom: 2px solid #f8f9fa;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Amenities Grid */
|
||||
.amenities-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.amenity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Image Upload */
|
||||
.image-upload {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.image-item img {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
background: #dc3545;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.remove-btn:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
/* Preview and Download */
|
||||
.preview-actions,
|
||||
.download-actions {
|
||||
text-align: center;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.pdf-preview {
|
||||
margin-top: 2rem;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pdf-preview h3 {
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.pdf-preview iframe {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
.loading-spinner {
|
||||
text-align: center;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
/* Navigation Buttons */
|
||||
.navigation-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.step-indicators {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.step {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
}
|
||||
|
||||
.template-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.amenities-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.navigation-buttons {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom Lightning Component Overrides */
|
||||
:host {
|
||||
--lwc-colorTextLabel: #2c3e50;
|
||||
--lwc-colorTextPlaceholder: #6c757d;
|
||||
--lwc-colorBorderInput: #e9ecef;
|
||||
--lwc-colorBorderInputFocus: #007bff;
|
||||
}
|
||||
|
||||
/* Animation for step transitions */
|
||||
.step-content {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,544 @@
|
||||
<template>
|
||||
<div class="property-brochure-generator">
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<div class="header-content">
|
||||
<h1>Property Brochure Generator</h1>
|
||||
<p>Advanced Template System with Multi-Page Layouts & Market Analytics</p>
|
||||
<div class="header-features">
|
||||
<span class="feature-badge">Professional Templates</span>
|
||||
<span class="feature-badge">Market Analytics</span>
|
||||
<span class="feature-badge">Multi-Format Support</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step Navigation -->
|
||||
<div class="step-navigation">
|
||||
<div class="step-item active">
|
||||
<div class="step-number">1</div>
|
||||
<div class="step-label">Choose Template</div>
|
||||
</div>
|
||||
<div class="step-item">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-label">Select Property & Details</div>
|
||||
</div>
|
||||
<div class="step-item">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-label">Additional Information</div>
|
||||
</div>
|
||||
<div class="step-item">
|
||||
<div class="step-number">4</div>
|
||||
<div class="step-label">Preview & Generate</div>
|
||||
</div>
|
||||
<div class="step-item">
|
||||
<div class="step-number">5</div>
|
||||
<div class="step-label">Download PDF</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Template Selection -->
|
||||
<div class="step-content" data-step="1" class={isStep1}>
|
||||
<div class="step-header">
|
||||
<h2>Choose Your Template</h2>
|
||||
<p>Select from our professional templates designed for real estate marketing</p>
|
||||
</div>
|
||||
|
||||
<div class="template-grid" id="all-templates">
|
||||
<!-- Custom Template -->
|
||||
<div class="template-card" data-template-id="custom" onclick={handleTemplateSelect}>
|
||||
<div class="template-preview custom-template">
|
||||
<div class="template-placeholder">
|
||||
<span class="template-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.232 5.232L18.768 8.768M16.732 3.732C17.2009 3.26331 17.8369 2.99988 18.5 2.99988C19.1631 2.99988 19.7991 3.26331 20.268 3.732C20.7367 4.20087 21.0001 4.83687 21.0001 5.5C21.0001 6.16313 20.7367 6.79913 20.268 7.268L18.5 9.036L15 12.5L12.5 15L9.036 18.5L7.268 20.268C6.79913 20.7367 6.16313 21.0001 5.5 21.0001C4.83687 21.0001 4.20087 20.7367 3.732 20.268C3.26331 19.7991 2.99988 19.1631 2.99988 18.5C2.99988 17.8369 3.26331 17.2009 3.732 16.732L15.232 5.232Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="template-text">Custom</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="template-info">
|
||||
<h3>Custom Template</h3>
|
||||
<p>Create your own layout with complete customization options</p>
|
||||
<div class="template-meta">
|
||||
<span class="template-pages">1-5 Pages</span>
|
||||
<span class="template-type">Custom</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Professional 1-Pager -->
|
||||
<div class="template-card" data-template-id="professional-1pager" onclick={handleTemplateSelect}>
|
||||
<div class="template-preview professional-1pager">
|
||||
<div class="template-layout">
|
||||
<div class="layout-header"></div>
|
||||
<div class="layout-content">
|
||||
<div class="layout-image"></div>
|
||||
<div class="layout-details"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="template-info">
|
||||
<h3>Professional 1-Pager</h3>
|
||||
<p>Compact single-page brochure with essential property highlights</p>
|
||||
<div class="template-meta">
|
||||
<span class="template-pages">1 Page</span>
|
||||
<span class="template-type">Professional</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Professional 3-Pager -->
|
||||
<div class="template-card" data-template-id="professional-3pager" onclick={handleTemplateSelect}>
|
||||
<div class="template-preview professional-3pager">
|
||||
<div class="layout-3pager">
|
||||
<div class="page-1"></div>
|
||||
<div class="page-2"></div>
|
||||
<div class="page-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="template-info">
|
||||
<h3>Professional 3-Pager</h3>
|
||||
<p>Comprehensive three-page brochure with detailed property analysis, market insights, and comprehensive property showcase.</p>
|
||||
<div class="template-meta">
|
||||
<span class="template-pages">3 Pages</span>
|
||||
<span class="template-type">Professional</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Professional 5-Pager -->
|
||||
<div class="template-card" data-template-id="professional-5pager" onclick={handleTemplateSelect}>
|
||||
<div class="template-preview professional-5pager">
|
||||
<div class="layout-5pager">
|
||||
<div class="page-1"></div>
|
||||
<div class="page-2"></div>
|
||||
<div class="page-3"></div>
|
||||
<div class="page-4"></div>
|
||||
<div class="page-5"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="template-info">
|
||||
<h3>Professional 5-Pager</h3>
|
||||
<p>Comprehensive five-page brochure with detailed market analysis and investment insights</p>
|
||||
<div class="template-meta">
|
||||
<span class="template-pages">5 Pages</span>
|
||||
<span class="template-type">Professional</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Luxury Villa -->
|
||||
<div class="template-card" data-template-id="luxury-villa" onclick={handleTemplateSelect}>
|
||||
<div class="template-preview luxury-villa">
|
||||
<div class="layout-luxury">
|
||||
<div class="luxury-header"></div>
|
||||
<div class="luxury-content">
|
||||
<div class="luxury-image"></div>
|
||||
<div class="luxury-details"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="template-info">
|
||||
<h3>Luxury Villa</h3>
|
||||
<p>Premium villa brochure with elegant design and comprehensive property showcase</p>
|
||||
<div class="template-meta">
|
||||
<span class="template-pages">3 Pages</span>
|
||||
<span class="template-type">Luxury</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modern Apartment -->
|
||||
<div class="template-card" data-template-id="modern-apartment" onclick={handleTemplateSelect}>
|
||||
<div class="template-preview modern-apartment">
|
||||
<div class="layout-modern">
|
||||
<div class="modern-header"></div>
|
||||
<div class="modern-content">
|
||||
<div class="modern-image"></div>
|
||||
<div class="modern-details"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="template-info">
|
||||
<h3>Modern Apartment</h3>
|
||||
<p>Contemporary apartment brochure with modern design elements</p>
|
||||
<div class="template-meta">
|
||||
<span class="template-pages">2 Pages</span>
|
||||
<span class="template-type">Modern</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Investment Property -->
|
||||
<div class="template-card" data-template-id="investment-property" onclick={handleTemplateSelect}>
|
||||
<div class="template-preview investment-property">
|
||||
<div class="layout-investment">
|
||||
<div class="investment-header"></div>
|
||||
<div class="investment-content">
|
||||
<div class="investment-chart"></div>
|
||||
<div class="investment-details"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="template-info">
|
||||
<h3>Investment Property</h3>
|
||||
<p>Investment-focused brochure with ROI analysis and market data</p>
|
||||
<div class="template-meta">
|
||||
<span class="template-pages">3 Pages</span>
|
||||
<span class="template-type">Investment</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-actions">
|
||||
<button class="btn btn-primary" disabled={!canProceed} onclick={nextStep}>
|
||||
Next: Property Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Property Selection and Details -->
|
||||
<div class="step-content" data-step="2" class={isStep2}>
|
||||
<div class="step-header">
|
||||
<h2>Select Property & Details</h2>
|
||||
<p>Choose a property from your Salesforce system or enter details manually</p>
|
||||
</div>
|
||||
|
||||
<!-- Property Selection Section -->
|
||||
<div class="property-selection-section">
|
||||
<h3>Select Existing Property</h3>
|
||||
<div class="form-group">
|
||||
<label for="property-select">Choose Property:</label>
|
||||
<lightning-combobox
|
||||
id="property-select"
|
||||
name="property-select"
|
||||
label="Select Property"
|
||||
placeholder="Choose a property from your system"
|
||||
options={availableProperties}
|
||||
value={selectedPropertyId}
|
||||
onchange={handlePropertySelection}
|
||||
disabled={isLoading}>
|
||||
</lightning-combobox>
|
||||
<small class="form-help">Select a property to auto-populate all fields, or fill manually below</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Property Details Form -->
|
||||
<div class="property-details-form">
|
||||
<h3>Property Information</h3>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="propertyName">Property Name *</label>
|
||||
<input type="text" id="propertyName" name="propertyName" value={propertyData.propertyName} onchange={handleInputChange} required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="propertyType">Property Type *</label>
|
||||
<select id="propertyType" name="propertyType" value={propertyData.propertyType} onchange={handleInputChange} required>
|
||||
<option value="">Select Property Type</option>
|
||||
<template for:each={propertyTypes} for:item="type">
|
||||
<option key={type} value={type}>{type}</option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="location">Location *</label>
|
||||
<select id="location" name="location" value={propertyData.location} onchange={handleLocationChange} required>
|
||||
<option value="">Select Location</option>
|
||||
<template for:each={dubaiLocations} for:item="loc">
|
||||
<option key={loc} value={loc}>{loc}</option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="price">Price (AED) *</label>
|
||||
<input type="number" id="price" name="price" value={propertyData.price} onchange={handleInputChange} placeholder="e.g., 5000000" required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="bedrooms">Bedrooms</label>
|
||||
<input type="number" id="bedrooms" name="bedrooms" value={propertyData.bedrooms} onchange={handleInputChange} min="0" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="bathrooms">Bathrooms</label>
|
||||
<input type="number" id="bathrooms" name="bathrooms" value={propertyData.bathrooms} onchange={handleInputChange} min="0" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="area">Area (sq ft)</label>
|
||||
<input type="number" id="area" name="area" value={propertyData.area} onchange={handleInputChange} min="0" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="titleEnglish">Property Title (English)</label>
|
||||
<input type="text" id="titleEnglish" name="titleEnglish" value={propertyData.titleEnglish} onchange={handleInputChange} placeholder="e.g., Brand New | Furnished | Canal View" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description *</label>
|
||||
<textarea id="description" name="description" value={propertyData.description} onchange={handleInputChange} rows="4" placeholder="Detailed property description..." required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="comments">Additional Comments</label>
|
||||
<textarea id="comments" name="comments" value={propertyData.comments} onchange={handleInputChange} rows="3" placeholder="Any additional notes or comments..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Additional Property Fields -->
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="externalId">External ID</label>
|
||||
<input type="text" id="externalId" name="externalId" value={propertyData.externalId} onchange={handleInputChange} placeholder="e.g., RO-R-21-3985" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="unitNumber">Unit Number</label>
|
||||
<input type="text" id="unitNumber" name="unitNumber" value={propertyData.unitNumber} onchange={handleInputChange} placeholder="e.g., 905" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="geopoint">Geopoint</label>
|
||||
<input type="text" id="geopoint" name="geopoint" value={propertyData.geopoint} onchange={handleInputChange} placeholder="e.g., 25.18580055,55.27030182" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="owner">Owner</label>
|
||||
<input type="text" id="owner" name="owner" value={propertyData.owner} onchange={handleInputChange} placeholder="e.g., Property Master" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="amenities">Amenities</label>
|
||||
<div class="amenities-grid">
|
||||
<template for:each={availableAmenities} for:item="amenity">
|
||||
<label class="amenity-checkbox">
|
||||
<input type="checkbox" value={amenity} onchange={handleAmenityChange} />
|
||||
<span class="amenity-label">{amenity}</span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="images">Property Images</label>
|
||||
<input type="file" id="images" name="images" onchange={handleImageUpload} multiple accept="image/*" />
|
||||
<small class="form-help">Upload multiple images (JPG, PNG, GIF). Max 6 images recommended.</small>
|
||||
|
||||
<div class="image-preview" if:true={propertyData.images}>
|
||||
<template for:each={propertyData.images} for:index="index">
|
||||
<div class="image-item">
|
||||
<img src={item} alt="Property Image" />
|
||||
<button type="button" class="remove-image" onclick={removeImage} data-index={index}>×</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-actions">
|
||||
<button class="btn btn-secondary" onclick={previousStep}>Previous</button>
|
||||
<button class="btn btn-primary" disabled={!canProceed} onclick={nextStep}>
|
||||
Next: Additional Information
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Additional Information -->
|
||||
<div class="step-content" data-step="3" class={isStep3}>
|
||||
<div class="step-header">
|
||||
<h2>Additional Information</h2>
|
||||
<p>Add market data, investment insights, and content modules</p>
|
||||
</div>
|
||||
|
||||
<div class="additional-info-form">
|
||||
<h3>Market Analytics</h3>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="marketTrend">Market Trend</label>
|
||||
<select id="marketTrend" name="marketTrend" value={propertyData.marketTrend} onchange={handleInputChange}>
|
||||
<option value="">Select Market Trend</option>
|
||||
<option value="Rising">Rising</option>
|
||||
<option value="Stable">Stable</option>
|
||||
<option value="Declining">Declining</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="roiPotential">ROI Potential (%)</label>
|
||||
<input type="number" id="roiPotential" name="roiPotential" value={propertyData.roiPotential} onchange={handleInputChange} step="0.1" min="0" max="100" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="avgPricePerSqft">Average Price per Sq Ft (AED)</label>
|
||||
<input type="number" id="avgPricePerSqft" name="avgPricePerSqft" value={propertyData.avgPricePerSqft} onchange={handleInputChange} min="0" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="marketDemand">Market Demand</label>
|
||||
<select id="marketDemand" name="marketDemand" value={propertyData.marketDemand} onchange={handleInputChange}>
|
||||
<option value="">Select Market Demand</option>
|
||||
<option value="High">High</option>
|
||||
<option value="Medium">Medium</option>
|
||||
<option value="Low">Low</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="investmentType">Investment Type</label>
|
||||
<select id="investmentType" name="investmentType" value={propertyData.investmentType} onchange={handleInputChange}>
|
||||
<option value="">Select Investment Type</option>
|
||||
<option value="Buy-to-Live">Buy-to-Live</option>
|
||||
<option value="Buy-to-Rent">Buy-to-Rent</option>
|
||||
<option value="Investment">Investment</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="rentalYield">Rental Yield (%)</label>
|
||||
<input type="number" id="rentalYield" name="rentalYield" value={propertyData.rentalYield} onchange={handleInputChange} step="0.1" min="0" max="100" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="investmentHighlights">Investment Highlights</label>
|
||||
<textarea id="investmentHighlights" name="investmentHighlights" value={propertyData.investmentHighlights} onchange={handleInputChange} rows="3" placeholder="Key investment benefits and highlights..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="locationAdvantages">Location Advantages</label>
|
||||
<textarea id="locationAdvantages" name="locationAdvantages" value={propertyData.locationAdvantages} onchange={handleInputChange} rows="3" placeholder="Location benefits, connectivity, amenities..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="additionalContent">Additional Content</label>
|
||||
<textarea id="additionalContent" name="additionalContent" value={propertyData.additionalContent} onchange={handleInputChange} rows="4" placeholder="Any additional content, notes, or special features..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-actions">
|
||||
<button class="btn btn-secondary" onclick={previousStep}>Previous</button>
|
||||
<button class="btn btn-primary" onclick={nextStep}>
|
||||
Next: Preview & Generate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Preview and Generate -->
|
||||
<div class="step-content" data-step="4" class={isStep4}>
|
||||
<div class="step-header">
|
||||
<h2>Preview & Generate</h2>
|
||||
<p>Review your brochure and generate the final PDF</p>
|
||||
</div>
|
||||
|
||||
<div class="preview-section">
|
||||
<div class="preview-header">
|
||||
<h3>Brochure Preview</h3>
|
||||
<div class="preview-actions">
|
||||
<button class="btn btn-secondary" onclick={handlePreview} disabled={isLoading}>
|
||||
{isLoading ? 'Generating...' : 'Generate Preview'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-content" if:true={showPreview}>
|
||||
<div class="preview-frame">
|
||||
<iframe src={pdfPreviewUrl} width="100%" height="600" frameborder="0"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-placeholder" if:false={showPreview}>
|
||||
<div class="placeholder-content">
|
||||
<span class="placeholder-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 12H15M9 16H15M17 21H7C5.89543 21 5 20.1046 5 19V5C5 3.89543 5.89543 3 7 3H12.5858C12.851 3 13.1054 3.10536 13.2929 3.29289L19.7071 9.70711C19.8946 9.89464 20 10.149 20 10.4142V19C20 20.1046 19.1046 21 18 21H17ZM17 21V10H12V5H7V19H17Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
<p>Click "Generate Preview" to see your brochure</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-actions">
|
||||
<button class="btn btn-secondary" onclick={previousStep}>Previous</button>
|
||||
<button class="btn btn-primary" onclick={nextStep} disabled={!showPreview}>
|
||||
Next: Download PDF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 5: Download PDF -->
|
||||
<div class="step-content" data-step="5" class={isStep5}>
|
||||
<div class="step-header">
|
||||
<h2>Download Your PDF</h2>
|
||||
<p>Your professional property brochure is ready!</p>
|
||||
</div>
|
||||
|
||||
<div class="download-section">
|
||||
<div class="download-success">
|
||||
<div class="success-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 12L11 14L15 10M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>PDF Generated Successfully!</h3>
|
||||
<p>Your professional property brochure has been created with the selected template and all your property data.</p>
|
||||
</div>
|
||||
|
||||
<div class="download-actions">
|
||||
<button class="btn btn-primary btn-large" onclick={handleDownload} disabled={isLoading}>
|
||||
{isLoading ? 'Downloading...' : 'Download PDF'}
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick={handlePreview}>
|
||||
View Preview Again
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="brochure-details">
|
||||
<h4>Brochure Details</h4>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Template:</span>
|
||||
<span class="detail-value">{selectedTemplateData.Name}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Property:</span>
|
||||
<span class="detail-value">{propertyData.propertyName}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Location:</span>
|
||||
<span class="detail-value">{propertyData.location}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Generated:</span>
|
||||
<span class="detail-value">{new Date().toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-actions">
|
||||
<button class="btn btn-secondary" onclick={previousStep}>Previous</button>
|
||||
<button class="btn btn-success" onclick={handleCreateNew}>
|
||||
Create New Brochure
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<div class="loading-overlay" if:true={isLoading}>
|
||||
<div class="loading-content">
|
||||
<div class="spinner"></div>
|
||||
<p>Processing your request...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1,494 @@
|
||||
import { LightningElement, track, wire, api } from 'lwc';
|
||||
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
|
||||
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
|
||||
import getPropertyTemplates from '@salesforce/apex/PropertyTemplateController.getPropertyTemplates';
|
||||
import generatePropertyPDF from '@salesforce/apex/PropertyTemplateController.generatePropertyPDF';
|
||||
import getPropertyData from '@salesforce/apex/PropertyTemplateController.getPropertyData';
|
||||
import getPropertyById from '@salesforce/apex/PropertyTemplateController.getPropertyById';
|
||||
import getMarketData from '@salesforce/apex/PropertyTemplateController.getMarketData';
|
||||
|
||||
export default class PropertyTemplateSelector extends LightningElement {
|
||||
@api recordId; // Current record ID if used in record page
|
||||
@track currentStep = 1;
|
||||
@track selectedTemplate = null;
|
||||
@track selectedLayout = null;
|
||||
@track propertyData = {};
|
||||
@track templates = [];
|
||||
@track isLoading = false;
|
||||
@track pdfPreviewUrl = '';
|
||||
@track showPreview = false;
|
||||
@track salesforceData = null;
|
||||
@track availableProperties = [];
|
||||
@track selectedPropertyId = null;
|
||||
@track showPropertySelector = false;
|
||||
@track uploadedImages = [];
|
||||
@track customizationOptions = {
|
||||
headerStyle: 'modern',
|
||||
colorScheme: 'professional',
|
||||
fontStyle: 'clean'
|
||||
};
|
||||
|
||||
// PDF Generation API Configuration
|
||||
pdfApiBaseUrl = 'https://YOUR-ACTUAL-IP:8000/api'; // TODO: Replace with your actual server IP
|
||||
|
||||
// Property types and locations for Dubai
|
||||
propertyTypes = [
|
||||
'AP', 'VI', 'BU', 'BW', 'OF', 'SH', 'WH'
|
||||
];
|
||||
|
||||
dubaiLocations = [
|
||||
'Astron Towers', 'Downtown Dubai', 'Palm Jumeirah', 'Dubai Marina', 'JBR', 'Business Bay',
|
||||
'Dubai Hills Estate', 'Emirates Hills', 'Arabian Ranches', 'Meadows', 'Springs'
|
||||
];
|
||||
|
||||
availableAmenities = [
|
||||
'Swimming Pool', 'Gym', 'Parking', 'Security', 'Garden', 'Balcony', 'Terrace',
|
||||
'Concierge', 'Maintenance', '24/7 Support', 'High-Speed Internet', 'Smart Home System'
|
||||
];
|
||||
|
||||
// Wire service to get property templates
|
||||
@wire(getPropertyTemplates)
|
||||
wiredTemplates({ error, data }) {
|
||||
if (data) {
|
||||
this.templates = data;
|
||||
this.error = undefined;
|
||||
} else if (error) {
|
||||
this.error = error;
|
||||
this.templates = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Wire service to get current property data if recordId is provided
|
||||
@wire(getRecord, {
|
||||
recordId: '$recordId',
|
||||
fields: ['pcrm__Property__c.Name', 'pcrm__Property__c.pcrm__Property_Type__c', 'pcrm__Property__c.pcrm__Sub_Locality_Bayut_Dubizzle__c', 'pcrm__Property__c.pcrm__Sale_Price_max__c', 'pcrm__Property__c.pcrm__Bedrooms__c', 'pcrm__Property__c.pcrm__Bathrooms__c', 'pcrm__Property__c.pcrm__Size__c', 'pcrm__Property__c.pcrm__Description_English__c', 'pcrm__Property__c.pcrm__Title_English__c']
|
||||
})
|
||||
wiredProperty({ error, data }) {
|
||||
if (data) {
|
||||
this.populatePropertyDataFromRecord(data);
|
||||
} else if (error) {
|
||||
console.error('Error loading property data:', error);
|
||||
this.showToast('Error', 'Failed to load property data. Please try again.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Wire service to get available properties for selection
|
||||
@wire(getPropertyData)
|
||||
wiredProperties({ error, data }) {
|
||||
if (data) {
|
||||
this.availableProperties = data.map(prop => ({
|
||||
label: `${prop.Name} - ${prop.pcrm__Title_English__c || 'No Title'} - ${prop.pcrm__Sub_Locality_Bayut_Dubizzle__c || 'No Location'}`,
|
||||
value: prop.Id,
|
||||
property: prop
|
||||
}));
|
||||
} else if (error) {
|
||||
console.error('Error loading properties:', error);
|
||||
this.showToast('Error', 'Failed to load available properties. Please try again.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Wire service to get market data
|
||||
@wire(getMarketData)
|
||||
wiredMarketData({ error, data }) {
|
||||
if (data) {
|
||||
this.salesforceData = data;
|
||||
this.populateMarketData();
|
||||
} else if (error) {
|
||||
console.error('Error loading market data:', error);
|
||||
this.showToast('Error', 'Failed to load market data. Please try again.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Populate property data from Salesforce record
|
||||
populatePropertyDataFromRecord(record) {
|
||||
this.propertyData = {
|
||||
propertyName: getFieldValue(record, 'pcrm__Property__c.Name') || '',
|
||||
propertyType: getFieldValue(record, 'pcrm__Property__c.pcrm__Property_Type__c') || '',
|
||||
location: getFieldValue(record, 'pcrm__Property__c.pcrm__Sub_Locality_Bayut_Dubizzle__c') || '',
|
||||
price: getFieldValue(record, 'pcrm__Property__c.pcrm__Sale_Price_max__c') || getFieldValue(record, 'pcrm__Property__c.pcrm__Rent_Price_max__c') || '',
|
||||
bedrooms: getFieldValue(record, 'pcrm__Property__c.pcrm__Bedrooms__c') || '',
|
||||
bathrooms: getFieldValue(record, 'pcrm__Property__c.pcrm__Bathrooms__c') || '',
|
||||
area: getFieldValue(record, 'pcrm__Property__c.pcrm__Size__c') || '',
|
||||
description: getFieldValue(record, 'pcrm__Property__c.pcrm__Description_English__c') || '',
|
||||
titleEnglish: getFieldValue(record, 'pcrm__Property__c.pcrm__Title_English__c') || '',
|
||||
externalId: getFieldValue(record, 'pcrm__Property__c.pcrm__External_ID__c') || '',
|
||||
unitNumber: getFieldValue(record, 'pcrm__Property__c.pcrm__Unit_Number__c') || '',
|
||||
completionStatus: getFieldValue(record, 'pcrm__Property__c.pcrm__Completion_Status__c') || '',
|
||||
furnished: getFieldValue(record, 'pcrm__Property__c.pcrm__Furnished__c') || '',
|
||||
view: getFieldValue(record, 'pcrm__Property__c.pcrm__View__c') || '',
|
||||
tower: getFieldValue(record, 'pcrm__Property__c.pcrm__Tower_Bayut_Dubizzle__c') || '',
|
||||
city: getFieldValue(record, 'pcrm__Property__c.pcrm__City_Bayut_Dubizzle__c') || '',
|
||||
subCommunity: getFieldValue(record, 'pcrm__Property__c.pcrm__Sub_Community_Propertyfinder__c') || '',
|
||||
buildYear: getFieldValue(record, 'pcrm__Property__c.pcrm__Build_Year__c') || '',
|
||||
stories: getFieldValue(record, 'pcrm__Property__c.pcrm__Stories__c') || '',
|
||||
parkingSpaces: getFieldValue(record, 'pcrm__Property__c.pcrm__Parking_Spaces__c') || '',
|
||||
lotSize: getFieldValue(record, 'pcrm__Property__c.pcrm__Lot_Size__c') || '',
|
||||
serviceCharge: getFieldValue(record, 'pcrm__Property__c.pcrm__Service_Charge__c') || '',
|
||||
privateAmenities: getFieldValue(record, 'pcrm__Property__c.pcrm__Private_Amenities__c') || '',
|
||||
commercialAmenities: getFieldValue(record, 'pcrm__Property__c.pcrm__Commercial_Amenities__c') || '',
|
||||
coordinates: getFieldValue(record, 'pcrm__Property__c.pcrm__Coordinates__c') || '',
|
||||
// Market and Investment Data
|
||||
marketTrend: '',
|
||||
roiPotential: '',
|
||||
avgPricePerSqft: '',
|
||||
marketDemand: '',
|
||||
investmentType: '',
|
||||
rentalYield: '',
|
||||
investmentHighlights: '',
|
||||
locationAdvantages: '',
|
||||
additionalContent: '',
|
||||
// Content Modules
|
||||
contentModules: [],
|
||||
// Customization Options
|
||||
headerStyle: 'modern',
|
||||
colorScheme: 'professional',
|
||||
fontStyle: 'clean',
|
||||
// Images and Amenities
|
||||
amenities: [],
|
||||
images: [],
|
||||
imageNames: []
|
||||
};
|
||||
}
|
||||
|
||||
// Populate market data from Salesforce
|
||||
populateMarketData() {
|
||||
if (this.salesforceData) {
|
||||
// Auto-populate market data fields
|
||||
this.propertyData = {
|
||||
...this.propertyData,
|
||||
marketTrend: this.salesforceData.marketTrend || '',
|
||||
roiPotential: this.salesforceData.roiPotential || '',
|
||||
avgPricePerSqft: this.salesforceData.avgPricePerSqft || '',
|
||||
marketDemand: this.salesforceData.marketDemand || '',
|
||||
investmentType: this.salesforceData.investmentType || '',
|
||||
rentalYield: this.salesforceData.rentalYield || '',
|
||||
investmentHighlights: this.salesforceData.investmentHighlights || '',
|
||||
locationAdvantages: this.salesforceData.locationAdvantages || '',
|
||||
contentModules: this.salesforceData.contentModules || [],
|
||||
additionalContent: this.salesforceData.additionalContent || ''
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle property selection from dropdown
|
||||
async handlePropertySelection(event) {
|
||||
const selectedPropertyId = event.detail.value;
|
||||
if (selectedPropertyId) {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const selectedProperty = await getPropertyById({ propertyId: selectedPropertyId });
|
||||
if (selectedProperty) {
|
||||
this.populatePropertyDataFromSelectedProperty(selectedProperty);
|
||||
this.selectedPropertyId = selectedPropertyId;
|
||||
this.showToast('Success', 'Property data loaded successfully', 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading property:', error);
|
||||
this.showToast('Error', 'Failed to load property data', 'error');
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate form with selected property data
|
||||
populatePropertyDataFromSelectedProperty(property) {
|
||||
this.propertyData = {
|
||||
propertyName: property.Name || '',
|
||||
propertyType: property.pcrm__Property_Type__c || '',
|
||||
location: property.pcrm__Sub_Locality_Bayut_Dubizzle__c || property.pcrm__Community_Propertyfinder__c || '',
|
||||
price: property.pcrm__Sale_Price_max__c || property.pcrm__Rent_Price_max__c || '',
|
||||
bedrooms: property.pcrm__Bedrooms__c || '',
|
||||
bathrooms: property.pcrm__Bathrooms__c || '',
|
||||
area: property.pcrm__Size__c || '',
|
||||
description: property.pcrm__Description_English__c || '',
|
||||
titleEnglish: property.pcrm__Title_English__c || '',
|
||||
unitNumber: property.pcrm__Unit_Number__c || '',
|
||||
completionStatus: property.pcrm__Completion_Status__c || '',
|
||||
furnished: property.pcrm__Furnished__c || '',
|
||||
view: property.pcrm__View__c || '',
|
||||
tower: property.pcrm__Tower_Bayut_Dubizzle__c || '',
|
||||
city: property.pcrm__City_Bayut_Dubizzle__c || property.pcrm__City_Propertyfinder__c || '',
|
||||
subCommunity: property.pcrm__Sub_Community_Propertyfinder__c || '',
|
||||
buildYear: property.pcrm__Build_Year__c || '',
|
||||
stories: property.pcrm__Stories__c || '',
|
||||
parkingSpaces: property.pcrm__Parking_Spaces__c || '',
|
||||
lotSize: property.pcrm__Lot_Size__c || '',
|
||||
serviceCharge: property.pcrm__Service_Charge__c || '',
|
||||
privateAmenities: property.pcrm__Private_Amenities__c || '',
|
||||
commercialAmenities: property.pcrm__Commercial_Amenities__c || '',
|
||||
coordinates: property.pcrm__Coordinates__c || '',
|
||||
amenities: [],
|
||||
images: []
|
||||
};
|
||||
}
|
||||
|
||||
// Navigation methods
|
||||
nextStep() {
|
||||
if (this.currentStep < 5 && this.validateCurrentStep()) {
|
||||
this.currentStep++;
|
||||
this.updateStepDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
previousStep() {
|
||||
if (this.currentStep > 1) {
|
||||
this.currentStep--;
|
||||
this.updateStepDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
// Template selection
|
||||
handleTemplateSelect(event) {
|
||||
const templateId = event.currentTarget.dataset.templateId;
|
||||
this.selectedTemplate = templateId;
|
||||
|
||||
// Set default layout based on template
|
||||
if (templateId === 'professional-1pager') {
|
||||
this.selectedLayout = '1-page';
|
||||
} else if (templateId === 'professional-3pager') {
|
||||
this.selectedLayout = '3-page';
|
||||
} else if (templateId === 'professional-5pager') {
|
||||
this.selectedLayout = '5-page';
|
||||
} else {
|
||||
this.selectedLayout = 'custom';
|
||||
}
|
||||
|
||||
this.updateStepDisplay();
|
||||
}
|
||||
|
||||
// Input change handlers
|
||||
handleInputChange(event) {
|
||||
const { name, value } = event.target;
|
||||
this.propertyData[name] = value;
|
||||
}
|
||||
|
||||
handleLocationChange(event) {
|
||||
this.propertyData.location = event.detail.value;
|
||||
}
|
||||
|
||||
handlePropertyTypeChange(event) {
|
||||
this.propertyData.propertyType = event.detail.value;
|
||||
}
|
||||
|
||||
handleAmenityChange(event) {
|
||||
const amenity = event.target.value;
|
||||
if (event.target.checked) {
|
||||
if (!this.propertyData.amenities) this.propertyData.amenities = [];
|
||||
this.propertyData.amenities.push(amenity);
|
||||
} else {
|
||||
this.propertyData.amenities = this.propertyData.amenities.filter(a => a !== amenity);
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced Image handling with room names
|
||||
handleImageUpload(event) {
|
||||
const files = event.target.files;
|
||||
if (files.length > 0) {
|
||||
Array.from(files).forEach((file, index) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
if (!this.propertyData.images) this.propertyData.images = [];
|
||||
if (!this.propertyData.imageNames) this.propertyData.imageNames = [];
|
||||
|
||||
this.propertyData.images.push(e.target.result);
|
||||
this.propertyData.imageNames.push(`Room ${index + 1}`);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
removeImage(index) {
|
||||
this.propertyData.images.splice(index, 1);
|
||||
this.propertyData.imageNames.splice(index, 1);
|
||||
}
|
||||
|
||||
// Update image name
|
||||
handleImageNameChange(event) {
|
||||
const index = parseInt(event.target.dataset.index);
|
||||
const newName = event.target.value;
|
||||
this.propertyData.imageNames[index] = newName;
|
||||
}
|
||||
|
||||
// Customization options
|
||||
handleCustomizationChange(event) {
|
||||
const { name, value } = event.target;
|
||||
this.customizationOptions[name] = value;
|
||||
this.propertyData[name] = value;
|
||||
this.updateCustomizationPreview();
|
||||
}
|
||||
|
||||
updateCustomizationPreview() {
|
||||
// Real-time customization preview updates
|
||||
const previewContainer = this.template.querySelector('.preview-content');
|
||||
if (previewContainer) {
|
||||
// Update preview styling based on customization options
|
||||
previewContainer.style.setProperty('--header-style', this.customizationOptions.headerStyle);
|
||||
previewContainer.style.setProperty('--color-scheme', this.customizationOptions.colorScheme);
|
||||
previewContainer.style.setProperty('--font-style', this.customizationOptions.fontStyle);
|
||||
}
|
||||
}
|
||||
|
||||
// Preview and PDF generation using external API
|
||||
async handlePreview() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
// Prepare data for external PDF API
|
||||
const pdfRequestData = {
|
||||
template: this.selectedTemplate,
|
||||
layout: this.selectedLayout || 'standard',
|
||||
propertyData: this.propertyData,
|
||||
customizationOptions: this.customizationOptions,
|
||||
generatePreview: true
|
||||
};
|
||||
|
||||
// Call external PDF generation API
|
||||
const response = await fetch(`${this.pdfApiBaseUrl}/preview`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(pdfRequestData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.pdfPreviewUrl = result.previewUrl;
|
||||
this.showPreview = true;
|
||||
this.currentStep = 4;
|
||||
this.updateStepDisplay();
|
||||
this.showToast('Success', 'Preview generated successfully', 'success');
|
||||
} else {
|
||||
this.showToast('Error', result.message || 'Failed to generate preview', 'error');
|
||||
}
|
||||
} else {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating preview:', error);
|
||||
this.showToast('Error', 'Failed to generate preview. Please check your API connection.', 'error');
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async handleDownload() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
// Prepare data for external PDF API
|
||||
const pdfRequestData = {
|
||||
template: this.selectedTemplate,
|
||||
layout: this.selectedLayout || 'standard',
|
||||
propertyData: this.propertyData,
|
||||
customizationOptions: this.customizationOptions,
|
||||
generatePDF: true
|
||||
};
|
||||
|
||||
// Call external PDF generation API
|
||||
const response = await fetch(`${this.pdfApiBaseUrl}/generate-pdf`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(pdfRequestData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
// Trigger download
|
||||
const link = document.createElement('a');
|
||||
link.href = result.pdfUrl;
|
||||
link.download = `${this.propertyData.propertyName || 'Property'}_Brochure.pdf`;
|
||||
link.click();
|
||||
this.showToast('Success', 'PDF downloaded successfully', 'success');
|
||||
} else {
|
||||
this.showToast('Error', result.message || 'Failed to generate PDF', 'error');
|
||||
}
|
||||
} else {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error downloading PDF:', error);
|
||||
this.showToast('Error', 'Failed to download PDF. Please check your API connection.', 'error');
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validation
|
||||
validateCurrentStep() {
|
||||
switch (this.currentStep) {
|
||||
case 1:
|
||||
return this.selectedTemplate;
|
||||
case 2:
|
||||
return this.propertyData.propertyName && this.propertyData.propertyType && this.propertyData.location;
|
||||
case 3:
|
||||
return this.propertyData.description;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
updateStepDisplay() {
|
||||
// Update step navigation
|
||||
const stepItems = this.template.querySelectorAll('.step-item');
|
||||
stepItems.forEach((item, index) => {
|
||||
if (index + 1 === this.currentStep) {
|
||||
item.classList.add('active');
|
||||
} else if (index + 1 < this.currentStep) {
|
||||
item.classList.remove('active', 'completed');
|
||||
} else {
|
||||
item.classList.remove('active', 'completed');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showToast(title, message, variant) {
|
||||
this.dispatchEvent(new ShowToastEvent({
|
||||
title,
|
||||
message,
|
||||
variant
|
||||
}));
|
||||
}
|
||||
|
||||
// Production-ready error handling
|
||||
handleError(error, context) {
|
||||
console.error(`Error in ${context}:`, error);
|
||||
this.showToast('Error', `An error occurred: ${error.message || 'Unknown error'}`, 'error');
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
// Production-ready success handling
|
||||
handleSuccess(message, context) {
|
||||
console.log(`Success in ${context}:`, message);
|
||||
this.showToast('Success', message, 'success');
|
||||
}
|
||||
|
||||
// Getter for computed properties
|
||||
get isStep1() { return this.currentStep === 1; }
|
||||
get isStep2() { return this.currentStep === 2; }
|
||||
get isStep3() { return this.currentStep === 3; }
|
||||
get isStep4() { return this.currentStep === 4; }
|
||||
get isStep5() { return this.currentStep === 5; }
|
||||
|
||||
get canProceed() {
|
||||
return this.validateCurrentStep();
|
||||
}
|
||||
|
||||
get selectedTemplateData() {
|
||||
return this.templates.find(t => t.Id === this.selectedTemplate);
|
||||
}
|
||||
|
||||
get hasProperties() {
|
||||
return this.availableProperties && this.availableProperties.length > 0;
|
||||
}
|
||||
|
||||
// Handle create new brochure
|
||||
handleCreateNew() {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,188 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<actionOverrides>
|
||||
<actionName>Accept</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Property_Template__cAccept</content>
|
||||
<formFactor>Large</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>Accept</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Property_Template__cAccept</content>
|
||||
<formFactor>Small</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>CancelEdit</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Property_Template__cCancelEdit</content>
|
||||
<formFactor>Large</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>CancelEdit</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Property_Template__cCancelEdit</content>
|
||||
<formFactor>Small</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>Clone</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Property_Template__cClone</content>
|
||||
<formFactor>Large</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>Clone</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Property_Template__cClone</content>
|
||||
<formFactor>Small</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>Delete</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Property_Template__cDelete</content>
|
||||
<formFactor>Large</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>Delete</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Property_Template__cDelete</content>
|
||||
<formFactor>Small</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>Edit</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Property_Template__cEdit</content>
|
||||
<formFactor>Large</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>Edit</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Property_Template__cEdit</content>
|
||||
<formFactor>Small</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>List</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Property_Template__cList</content>
|
||||
<formFactor>Large</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>List</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Property_Template__cList</content>
|
||||
<formFactor>Small</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>New</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Property_Template__cNew</content>
|
||||
<formFactor>Large</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>New</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Property_Template__cNew</content>
|
||||
<formFactor>Small</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>SaveEdit</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Property_Template__cSaveEdit</content>
|
||||
<formFactor>Large</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>SaveEdit</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Property_Template__cSaveEdit</content>
|
||||
<formFactor>Small</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>Tab</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Property_Template__cTab</content>
|
||||
<formFactor>Large</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>Tab</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Property_Template__cTab</content>
|
||||
<formFactor>Small</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>View</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Property_Template__cView</content>
|
||||
<formFactor>Large</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>View</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Property_Template__cView</content>
|
||||
<formFactor>Small</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<allowInChatterGroups>false</allowInChatterGroups>
|
||||
<allowMru>true</allowMru>
|
||||
<compactLayoutAssignment>SYSTEM</compactLayoutAssignment>
|
||||
<deploymentStatus>Deployed</deploymentStatus>
|
||||
<enableActivities>false</enableActivities>
|
||||
<enableBulkApi>true</enableBulkApi>
|
||||
<enableFeeds>false</enableFeeds>
|
||||
<enableHistory>false</enableHistory>
|
||||
<enableLicensing>false</enableLicensing>
|
||||
<enableReports>true</enableReports>
|
||||
<enableSearch>true</enableSearch>
|
||||
<enableSharing>true</enableSharing>
|
||||
<enableStreamingApi>true</enableStreamingApi>
|
||||
<externalSharingModel>Private</externalSharingModel>
|
||||
<label>Property Template</label>
|
||||
<nameField>
|
||||
<displayFormat>T-{0000}</displayFormat>
|
||||
<label>Template Name</label>
|
||||
<trackHistory>false</trackHistory>
|
||||
<type>AutoNumber</type>
|
||||
</nameField>
|
||||
<pluralLabel>Property Templates</pluralLabel>
|
||||
<searchLayouts/>
|
||||
<sharingModel>ReadWrite</sharingModel>
|
||||
<visibility>Public</visibility>
|
||||
</CustomObject>
|
||||
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Description__c</fullName>
|
||||
<description>Detailed description of the property template</description>
|
||||
<externalId>false</externalId>
|
||||
<label>Description</label>
|
||||
<length>32768</length>
|
||||
<trackHistory>false</trackHistory>
|
||||
<trackTrending>false</trackTrending>
|
||||
<type>LongTextArea</type>
|
||||
<visibleLines>3</visibleLines>
|
||||
</CustomField>
|
||||
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/type">
|
||||
<fullName>Is_Active__c</fullName>
|
||||
<defaultValue>true</defaultValue>
|
||||
<description>Indicates if this template is active and available for use</description>
|
||||
<externalId>false</externalId>
|
||||
<label>Is Active</label>
|
||||
<trackHistory>false</trackHistory>
|
||||
<trackTrending>false</trackTrending>
|
||||
<type>Checkbox</type>
|
||||
</CustomField>
|
||||
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Preview_Image_URL__c</fullName>
|
||||
<description>URL to the preview image for this template</description>
|
||||
<externalId>false</externalId>
|
||||
<label>Preview Image URL</label>
|
||||
<length>255</length>
|
||||
<trackHistory>false</trackHistory>
|
||||
<trackTrending>false</trackTrending>
|
||||
<type>Url</type>
|
||||
</CustomField>
|
||||
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Tags__c</fullName>
|
||||
<description>Comma-separated tags for categorizing this template</description>
|
||||
<externalId>false</externalId>
|
||||
<label>Tags</label>
|
||||
<length>255</length>
|
||||
<trackHistory>false</trackHistory>
|
||||
<trackTrending>false</trackTrending>
|
||||
<type>Text</type>
|
||||
</CustomField>
|
||||
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Template_Definition__c</fullName>
|
||||
<description>JSON definition for the pdfmake template</description>
|
||||
<externalId>false</externalId>
|
||||
<label>Template Definition</label>
|
||||
<length>131072</length>
|
||||
<trackHistory>false</trackHistory>
|
||||
<trackTrending>false</trackTrending>
|
||||
<type>LongTextArea</type>
|
||||
<visibleLines>10</visibleLines>
|
||||
</CustomField>
|
||||
@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<actionOverrides>
|
||||
<actionName>Accept</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Accept</content>
|
||||
<formFactor>Large</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>CancelEdit</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>CancelEdit</content>
|
||||
<formFactor>Large</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>Clone</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Clone</content>
|
||||
<formFactor>Large</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>Delete</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Delete</content>
|
||||
<formFactor>Large</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>Edit</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Edit</content>
|
||||
<formFactor>Large</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>List</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>List</content>
|
||||
<formFactor>Large</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>New</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>New</content>
|
||||
<formFactor>Large</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>SaveEdit</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>SaveEdit</content>
|
||||
<formFactor>Large</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>Tab</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>Tab</content>
|
||||
<formFactor>Large</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<actionOverrides>
|
||||
<actionName>View</actionName>
|
||||
<comment>Action override created by Lightning App Builder during activation.</comment>
|
||||
<content>View</content>
|
||||
<formFactor>Large</formFactor>
|
||||
<skipRecordTypeSelect>false</skipRecordTypeSelect>
|
||||
<type>Default</type>
|
||||
</actionOverrides>
|
||||
<allowInChatterGroups>false</allowInChatterGroups>
|
||||
<compactLayoutAssignment>SYSTEM</compactLayoutAssignment>
|
||||
<deploymentStatus>Deployed</deploymentStatus>
|
||||
<enableActivities>true</enableActivities>
|
||||
<enableBulkApi>true</enableBulkApi>
|
||||
<enableFeeds>false</enableFeeds>
|
||||
<enableHistory>false</enableHistory>
|
||||
<enableLicensing>false</enableLicensing>
|
||||
<enableReports>true</enableReports>
|
||||
<enableSearch>true</enableSearch>
|
||||
<enableSharing>true</enableSharing>
|
||||
<enableStreamingApi>true</enableStreamingApi>
|
||||
<externalSharingModel>Private</externalSharingModel>
|
||||
<label>Property</label>
|
||||
<nameField>
|
||||
<displayFormat>P-{0000}</displayFormat>
|
||||
<label>Property Number</label>
|
||||
<type>AutoNumber</type>
|
||||
</nameField>
|
||||
<pluralLabel>Properties</pluralLabel>
|
||||
<searchLayouts/>
|
||||
<sharingModel>ReadWrite</sharingModel>
|
||||
<visibility>Public</visibility>
|
||||
</CustomObject>
|
||||
@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Location__c</fullName>
|
||||
<externalId>false</externalId>
|
||||
<label>Location</label>
|
||||
<required>false</required>
|
||||
<trackTrending>false</trackTrending>
|
||||
<type>Picklist</type>
|
||||
<valueSet>
|
||||
<restricted>true</restricted>
|
||||
<valueSetDefinition>
|
||||
<sorted>false</sorted>
|
||||
<value>
|
||||
<fullName>Downtown Dubai</fullName>
|
||||
<default>false</default>
|
||||
<label>Downtown Dubai</label>
|
||||
</value>
|
||||
<value>
|
||||
<fullName>Palm Jumeirah</fullName>
|
||||
<default>false</default>
|
||||
<label>Palm Jumeirah</label>
|
||||
</value>
|
||||
<value>
|
||||
<fullName>Dubai Marina</fullName>
|
||||
<default>false</default>
|
||||
<label>Dubai Marina</label>
|
||||
</value>
|
||||
<value>
|
||||
<fullName>JBR</fullName>
|
||||
<default>false</default>
|
||||
<label>JBR</label>
|
||||
</value>
|
||||
<value>
|
||||
<fullName>Business Bay</fullName>
|
||||
<default>false</default>
|
||||
<label>Business Bay</label>
|
||||
</value>
|
||||
<value>
|
||||
<fullName>Dubai Hills Estate</fullName>
|
||||
<default>false</default>
|
||||
<label>Dubai Hills Estate</label>
|
||||
</value>
|
||||
<value>
|
||||
<fullName>Emirates Hills</fullName>
|
||||
<default>false</default>
|
||||
<label>Emirates Hills</label>
|
||||
</value>
|
||||
<value>
|
||||
<fullName>Arabian Ranches</fullName>
|
||||
<default>false</default>
|
||||
<label>Arabian Ranches</label>
|
||||
</value>
|
||||
<value>
|
||||
<fullName>Meadows</fullName>
|
||||
<default>false</default>
|
||||
<label>Meadows</label>
|
||||
</value>
|
||||
<value>
|
||||
<fullName>Springs</fullName>
|
||||
<default>false</default>
|
||||
<label>Springs</label>
|
||||
</value>
|
||||
</valueSetDefinition>
|
||||
</valueSet>
|
||||
</CustomField>
|
||||
@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Property_Type__c</fullName>
|
||||
<externalId>false</externalId>
|
||||
<label>Property Type</label>
|
||||
<required>false</required>
|
||||
<trackTrending>false</trackTrending>
|
||||
<type>Picklist</type>
|
||||
<valueSet>
|
||||
<restricted>true</restricted>
|
||||
<valueSetDefinition>
|
||||
<sorted>false</sorted>
|
||||
<value>
|
||||
<fullName>Apartment</fullName>
|
||||
<default>false</default>
|
||||
<label>Apartment</label>
|
||||
</value>
|
||||
<value>
|
||||
<fullName>Villa</fullName>
|
||||
<default>false</default>
|
||||
<label>Villa</label>
|
||||
</value>
|
||||
<value>
|
||||
<fullName>Penthouse</fullName>
|
||||
<default>false</default>
|
||||
<label>Penthouse</label>
|
||||
</value>
|
||||
<value>
|
||||
<fullName>Townhouse</fullName>
|
||||
<default>false</default>
|
||||
<label>Townhouse</label>
|
||||
</value>
|
||||
<value>
|
||||
<fullName>Office</fullName>
|
||||
<default>false</default>
|
||||
<label>Office</label>
|
||||
</value>
|
||||
<value>
|
||||
<fullName>Retail Space</fullName>
|
||||
<default>false</default>
|
||||
<label>Retail Space</label>
|
||||
</value>
|
||||
<value>
|
||||
<fullName>Warehouse</fullName>
|
||||
<default>false</default>
|
||||
<label>Warehouse</label>
|
||||
</value>
|
||||
</valueSetDefinition>
|
||||
</valueSet>
|
||||
</CustomField>
|
||||
24
package.xml
Normal file
24
package.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<types>
|
||||
<members>Property_Template__c</members>
|
||||
<name>CustomObject</name>
|
||||
</types>
|
||||
<types>
|
||||
<members>Property_Template__c.Description__c</members>
|
||||
<members>Property_Template__c.Preview_Image_URL__c</members>
|
||||
<members>Property_Template__c.Tags__c</members>
|
||||
<members>Property_Template__c.Template_Definition__c</members>
|
||||
<members>Property_Template__c.Is_Active__c</members>
|
||||
<name>CustomField</name>
|
||||
</types>
|
||||
<types>
|
||||
<members>PropertyTemplateController</members>
|
||||
<name>ApexClass</name>
|
||||
</types>
|
||||
<types>
|
||||
<members>propertyTemplateSelector</members>
|
||||
<name>LightningComponentBundle</name>
|
||||
</types>
|
||||
<version>58.0</version>
|
||||
</Package>
|
||||
608
python-pdf-generator/advanced_templates.py
Normal file
608
python-pdf-generator/advanced_templates.py
Normal file
@ -0,0 +1,608 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Advanced Property PDF Templates
|
||||
Highly sophisticated templates with real estate images and professional layouts
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.styles import ParagraphStyle
|
||||
from reportlab.lib.units import inch, cm
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT, TA_JUSTIFY
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image, Table, TableStyle, PageBreak
|
||||
from reportlab.platypus.flowables import KeepTogether
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.colors import HexColor
|
||||
from PIL import Image as PILImage
|
||||
import io
|
||||
import base64
|
||||
|
||||
class AdvancedPropertyTemplates:
|
||||
"""Advanced property PDF templates with sophisticated designs"""
|
||||
|
||||
def __init__(self):
|
||||
self.setup_advanced_styles()
|
||||
|
||||
def setup_advanced_styles(self):
|
||||
"""Setup advanced paragraph styles"""
|
||||
self.styles = {}
|
||||
|
||||
# Ultra Premium Title
|
||||
self.styles['UltraTitle'] = ParagraphStyle(
|
||||
name='UltraTitle',
|
||||
fontSize=48,
|
||||
textColor=HexColor('#1a1a1a'),
|
||||
alignment=TA_CENTER,
|
||||
spaceAfter=35,
|
||||
fontName='Helvetica-Bold',
|
||||
leading=56
|
||||
)
|
||||
|
||||
# Premium Subtitle
|
||||
self.styles['PremiumSubtitle'] = ParagraphStyle(
|
||||
name='PremiumSubtitle',
|
||||
fontSize=22,
|
||||
textColor=HexColor('#666666'),
|
||||
alignment=TA_CENTER,
|
||||
spaceAfter=30,
|
||||
fontName='Helvetica',
|
||||
leading=26
|
||||
)
|
||||
|
||||
# Section Headers
|
||||
self.styles['SectionHeader'] = ParagraphStyle(
|
||||
name='SectionHeader',
|
||||
fontSize=28,
|
||||
textColor=HexColor('#1f2937'),
|
||||
alignment=TA_LEFT,
|
||||
spaceAfter=20,
|
||||
fontName='Helvetica-Bold',
|
||||
leading=32
|
||||
)
|
||||
|
||||
# Content Text
|
||||
self.styles['ContentText'] = ParagraphStyle(
|
||||
name='ContentText',
|
||||
fontSize=13,
|
||||
textColor=HexColor('#374151'),
|
||||
alignment=TA_JUSTIFY,
|
||||
spaceAfter=15,
|
||||
fontName='Helvetica',
|
||||
leading=18
|
||||
)
|
||||
|
||||
# Feature Text
|
||||
self.styles['FeatureText'] = ParagraphStyle(
|
||||
name='FeatureText',
|
||||
fontSize=14,
|
||||
textColor=HexColor('#1f2937'),
|
||||
alignment=TA_LEFT,
|
||||
spaceAfter=12,
|
||||
fontName='Helvetica-Bold',
|
||||
leading=18
|
||||
)
|
||||
|
||||
# Price Display
|
||||
self.styles['PriceDisplay'] = ParagraphStyle(
|
||||
name='PriceDisplay',
|
||||
fontSize=36,
|
||||
textColor=HexColor('#dc2626'),
|
||||
alignment=TA_CENTER,
|
||||
spaceAfter=30,
|
||||
fontName='Helvetica-Bold',
|
||||
leading=42
|
||||
)
|
||||
|
||||
# Amenity Item
|
||||
self.styles['AmenityItem'] = ParagraphStyle(
|
||||
name='AmenityItem',
|
||||
fontSize=13,
|
||||
textColor=HexColor('#4b5563'),
|
||||
alignment=TA_LEFT,
|
||||
spaceAfter=10,
|
||||
fontName='Helvetica',
|
||||
leading=17
|
||||
)
|
||||
|
||||
def create_luxury_villa_template(self, data: Dict[str, Any], output_path: str) -> str:
|
||||
"""Create ultra-luxury villa template with sophisticated design"""
|
||||
doc = SimpleDocTemplate(
|
||||
output_path,
|
||||
pagesize=A4,
|
||||
rightMargin=0.3*cm,
|
||||
leftMargin=0.3*cm,
|
||||
topMargin=0.3*cm,
|
||||
bottomMargin=0.3*cm
|
||||
)
|
||||
|
||||
story = []
|
||||
|
||||
# Page 1: Cover Page
|
||||
story.extend(self._create_cover_page(data))
|
||||
story.append(PageBreak())
|
||||
|
||||
# Page 2: Property Overview
|
||||
story.extend(self._create_property_overview(data))
|
||||
story.append(PageBreak())
|
||||
|
||||
# Page 3: Features & Amenities
|
||||
story.extend(self._create_features_page(data))
|
||||
story.append(PageBreak())
|
||||
|
||||
# Page 4: Location & Investment
|
||||
story.extend(self._create_investment_page(data))
|
||||
|
||||
# Build PDF with custom header/footer
|
||||
doc.build(story, onFirstPage=lambda c, d: self._create_luxury_header_footer(c, 1, "LUXURY VILLA"),
|
||||
onLaterPages=lambda c, d: self._create_luxury_header_footer(c, d.page, "LUXURY VILLA"))
|
||||
|
||||
return output_path
|
||||
|
||||
def _create_cover_page(self, data: Dict[str, Any]) -> List:
|
||||
"""Create sophisticated cover page"""
|
||||
story = []
|
||||
|
||||
# Main Title
|
||||
story.append(Paragraph("LUXURY VILLA COLLECTION", self.styles['UltraTitle']))
|
||||
story.append(Spacer(1, 40))
|
||||
|
||||
# Property Name
|
||||
story.append(Paragraph(f"{data.get('propertyName', 'Exclusive Villa')}", self.styles['UltraTitle']))
|
||||
story.append(Spacer(1, 30))
|
||||
|
||||
# Location
|
||||
story.append(Paragraph(f"Located in {data.get('location', 'Dubai')}", self.styles['PremiumSubtitle']))
|
||||
story.append(Spacer(1, 50))
|
||||
|
||||
# Price Highlight
|
||||
story.append(Paragraph("INVESTMENT VALUE", self.styles['SectionHeader']))
|
||||
story.append(Paragraph(f"AED {data.get('price', 'N/A')}", self.styles['PriceDisplay']))
|
||||
story.append(Spacer(1, 60))
|
||||
|
||||
# Property Stats
|
||||
stats_data = [
|
||||
['BEDROOMS', 'BATHROOMS', 'AREA', 'LOCATION'],
|
||||
[
|
||||
data.get('bedrooms', 'N/A'),
|
||||
data.get('bathrooms', 'N/A'),
|
||||
f"{data.get('area', 'N/A')} sq ft",
|
||||
data.get('location', 'N/A')
|
||||
]
|
||||
]
|
||||
|
||||
stats_table = Table(stats_data, colWidths=[1.8*inch, 1.8*inch, 1.8*inch, 1.8*inch])
|
||||
stats_table.setStyle(TableStyle([
|
||||
('BACKGROUND', (0, 0), (-1, 0), HexColor('#2c1810')),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||
('FONTNAME', (0, 0), (-1, -1), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, 0), 16),
|
||||
('FONTSIZE', (0, 1), (-1, 1), 18),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 20),
|
||||
('GRID', (0, 0), (-1, -1), 1, HexColor('#2c1810')),
|
||||
('ROUNDEDCORNERS', [15, 15, 15, 15])
|
||||
]))
|
||||
|
||||
story.append(stats_table)
|
||||
story.append(Spacer(1, 60))
|
||||
|
||||
# Footer Text
|
||||
story.append(Paragraph("EXCLUSIVE • PRESTIGIOUS • SOPHISTICATED", self.styles['PremiumSubtitle']))
|
||||
story.append(Paragraph("Where luxury meets lifestyle", self.styles['ContentText']))
|
||||
|
||||
return story
|
||||
|
||||
def _create_property_overview(self, data: Dict[str, Any]) -> List:
|
||||
"""Create property overview page"""
|
||||
story = []
|
||||
|
||||
# Page Title
|
||||
story.append(Paragraph("PROPERTY OVERVIEW", self.styles['SectionHeader']))
|
||||
story.append(Spacer(1, 30))
|
||||
|
||||
# Description
|
||||
if data.get('description'):
|
||||
story.append(Paragraph("ABOUT THIS PROPERTY", self.styles['FeatureText']))
|
||||
story.append(Paragraph(data['description'], self.styles['ContentText']))
|
||||
story.append(Spacer(1, 30))
|
||||
|
||||
# Property Highlights
|
||||
story.append(Paragraph("PROPERTY HIGHLIGHTS", self.styles['FeatureText']))
|
||||
highlights = [
|
||||
"• Premium finishes throughout",
|
||||
"• High-end appliances and fixtures",
|
||||
"• Smart home technology integration",
|
||||
"• Energy-efficient design",
|
||||
"• Premium security systems",
|
||||
"• Landscaped gardens and outdoor spaces"
|
||||
]
|
||||
|
||||
for highlight in highlights:
|
||||
story.append(Paragraph(highlight, self.styles['ContentText']))
|
||||
|
||||
story.append(Spacer(1, 30))
|
||||
|
||||
# Additional Features
|
||||
story.append(Paragraph("ADDITIONAL FEATURES", self.styles['FeatureText']))
|
||||
additional_features = [
|
||||
"• Premium finishes throughout",
|
||||
"• Premium flooring materials",
|
||||
"• Designer lighting fixtures",
|
||||
"• High-quality windows and doors",
|
||||
"• Advanced HVAC systems",
|
||||
"• Premium insulation and soundproofing"
|
||||
]
|
||||
|
||||
for feature in additional_features:
|
||||
story.append(Paragraph(feature, self.styles['ContentText']))
|
||||
|
||||
return story
|
||||
|
||||
def _create_features_page(self, data: Dict[str, Any]) -> List:
|
||||
"""Create features and amenities page"""
|
||||
story = []
|
||||
|
||||
# Page Title
|
||||
story.append(Paragraph("FEATURES & AMENITIES", self.styles['SectionHeader']))
|
||||
story.append(Spacer(1, 30))
|
||||
|
||||
# Interior Features
|
||||
story.append(Paragraph("INTERIOR FEATURES", self.styles['FeatureText']))
|
||||
story.append(Spacer(1, 15))
|
||||
|
||||
interior_features = [
|
||||
"• Master suite with walk-in closet",
|
||||
"• En-suite bathrooms with premium fixtures",
|
||||
"• Open-concept living areas",
|
||||
"• Gourmet kitchen with island",
|
||||
"• Formal dining room",
|
||||
"• Home office/study",
|
||||
"• Media room/home theater",
|
||||
"• Wine cellar/storage"
|
||||
]
|
||||
|
||||
for feature in interior_features:
|
||||
story.append(Paragraph(feature, self.styles['ContentText']))
|
||||
|
||||
story.append(Spacer(1, 30))
|
||||
|
||||
# Exterior Features
|
||||
story.append(Paragraph("EXTERIOR FEATURES", self.styles['FeatureText']))
|
||||
story.append(Spacer(1, 15))
|
||||
|
||||
exterior_features = [
|
||||
"• Private swimming pool",
|
||||
"• Outdoor kitchen and dining area",
|
||||
"• Landscaped gardens",
|
||||
"• Private parking/garage",
|
||||
"• Security gate and fencing",
|
||||
"• Outdoor entertainment areas",
|
||||
"• Garden sheds/storage",
|
||||
"• Professional landscaping"
|
||||
]
|
||||
|
||||
for feature in exterior_features:
|
||||
story.append(Paragraph(feature, self.styles['ContentText']))
|
||||
|
||||
return story
|
||||
|
||||
def _create_investment_page(self, data: Dict[str, Any]) -> List:
|
||||
"""Create investment and location page"""
|
||||
story = []
|
||||
|
||||
# Page Title
|
||||
story.append(Paragraph("INVESTMENT & LOCATION", self.styles['SectionHeader']))
|
||||
story.append(Spacer(1, 30))
|
||||
|
||||
# Location Benefits
|
||||
story.append(Paragraph("LOCATION BENEFITS", self.styles['FeatureText']))
|
||||
story.append(Spacer(1, 15))
|
||||
|
||||
location_benefits = [
|
||||
"• Prime location in prestigious area",
|
||||
"• Easy access to major highways",
|
||||
"• Close to shopping and dining",
|
||||
"• Excellent schools nearby",
|
||||
"• Public transportation access",
|
||||
"• Healthcare facilities nearby",
|
||||
"• Recreational facilities close by",
|
||||
"• High appreciation potential"
|
||||
]
|
||||
|
||||
for benefit in location_benefits:
|
||||
story.append(Paragraph(benefit, self.styles['ContentText']))
|
||||
|
||||
story.append(Spacer(1, 30))
|
||||
|
||||
# Investment Highlights
|
||||
story.append(Paragraph("INVESTMENT HIGHLIGHTS", self.styles['FeatureText']))
|
||||
story.append(Spacer(1, 15))
|
||||
|
||||
investment_highlights = [
|
||||
"• Strong rental yield potential",
|
||||
"• High capital appreciation",
|
||||
"• Low maintenance costs",
|
||||
"• Premium tenant attraction",
|
||||
"• Stable market conditions",
|
||||
"• Excellent resale value",
|
||||
"• Tax benefits available",
|
||||
"• Professional property management"
|
||||
]
|
||||
|
||||
for highlight in investment_highlights:
|
||||
story.append(Paragraph(highlight, self.styles['ContentText']))
|
||||
|
||||
story.append(Spacer(1, 40))
|
||||
|
||||
# Contact Information
|
||||
story.append(Paragraph("CONTACT US", self.styles['FeatureText']))
|
||||
story.append(Paragraph("For more information about this exclusive property,", self.styles['ContentText']))
|
||||
story.append(Paragraph("please contact our luxury property specialists.", self.styles['ContentText']))
|
||||
story.append(Spacer(1, 20))
|
||||
story.append(Paragraph("LUXURY REAL ESTATE", self.styles['FeatureText']))
|
||||
story.append(Paragraph("Premium Property Solutions", self.styles['ContentText']))
|
||||
|
||||
return story
|
||||
|
||||
def _create_luxury_header_footer(self, canvas_obj, page_num: int, template_name: str):
|
||||
"""Create luxury header and footer"""
|
||||
# Header
|
||||
canvas_obj.setFillColor(HexColor('#2c1810'))
|
||||
canvas_obj.setFont("Helvetica-Bold", 18)
|
||||
canvas_obj.drawString(50, A4[1] - 40, "LUXURY REAL ESTATE")
|
||||
|
||||
canvas_obj.setFont("Helvetica", 14)
|
||||
canvas_obj.drawString(50, A4[1] - 60, "Premium Property Brochure")
|
||||
|
||||
# Template indicator
|
||||
canvas_obj.setFillColor(HexColor('#8b4513'))
|
||||
canvas_obj.setFont("Helvetica-Bold", 16)
|
||||
canvas_obj.drawRightString(A4[0] - 50, A4[1] - 40, template_name)
|
||||
|
||||
# Footer
|
||||
canvas_obj.setFillColor(HexColor('#8b4513'))
|
||||
canvas_obj.setFont("Helvetica", 12)
|
||||
canvas_obj.drawCentredString(A4[0]/2, 35, f"Generated on {datetime.now().strftime('%B %d, %Y')}")
|
||||
canvas_obj.drawCentredString(A4[0]/2, 20, "Luxury Real Estate - Premium Property Solutions")
|
||||
|
||||
# Page number
|
||||
canvas_obj.drawRightString(A4[0] - 50, 20, f"Page {page_num}")
|
||||
|
||||
def create_modern_apartment_template(self, data: Dict[str, Any], output_path: str) -> str:
|
||||
"""Create modern apartment template with contemporary design"""
|
||||
doc = SimpleDocTemplate(
|
||||
output_path,
|
||||
pagesize=A4,
|
||||
rightMargin=0.4*cm,
|
||||
leftMargin=0.4*cm,
|
||||
topMargin=0.4*cm,
|
||||
bottomMargin=0.4*cm
|
||||
)
|
||||
|
||||
story = []
|
||||
|
||||
# Page 1: Modern Cover
|
||||
story.extend(self._create_modern_cover(data))
|
||||
story.append(PageBreak())
|
||||
|
||||
# Page 2: Modern Features
|
||||
story.extend(self._create_modern_features(data))
|
||||
story.append(PageBreak())
|
||||
|
||||
# Page 3: Modern Amenities
|
||||
story.extend(self._create_modern_amenities(data))
|
||||
|
||||
# Build PDF
|
||||
doc.build(story, onFirstPage=lambda c, d: self._create_modern_header_footer(c, 1, "MODERN APARTMENT"),
|
||||
onLaterPages=lambda c, d: self._create_modern_header_footer(c, d.page, "MODERN APARTMENT"))
|
||||
|
||||
return output_path
|
||||
|
||||
def _create_modern_cover(self, data: Dict[str, Any]) -> List:
|
||||
"""Create modern cover page"""
|
||||
story = []
|
||||
|
||||
# Main Title
|
||||
story.append(Paragraph("THE MODERN COLLECTION", self.styles['UltraTitle']))
|
||||
story.append(Spacer(1, 35))
|
||||
|
||||
# Property Name
|
||||
story.append(Paragraph(f"{data.get('propertyName', 'Modern Apartment')}", self.styles['UltraTitle']))
|
||||
story.append(Spacer(1, 25))
|
||||
|
||||
# Location
|
||||
story.append(Paragraph(f"Located in {data.get('location', 'Dubai')}", self.styles['PremiumSubtitle']))
|
||||
story.append(Spacer(1, 45))
|
||||
|
||||
# Price
|
||||
story.append(Paragraph("INVESTMENT VALUE", self.styles['SectionHeader']))
|
||||
story.append(Paragraph(f"AED {data.get('price', 'N/A')}", self.styles['PriceDisplay']))
|
||||
story.append(Spacer(1, 50))
|
||||
|
||||
# Modern Stats
|
||||
stats_data = [
|
||||
['BEDROOMS', 'BATHROOMS', 'AREA', 'LOCATION'],
|
||||
[
|
||||
data.get('bedrooms', 'N/A'),
|
||||
data.get('bathrooms', 'N/A'),
|
||||
f"{data.get('area', 'N/A')} sq ft",
|
||||
data.get('location', 'N/A')
|
||||
]
|
||||
]
|
||||
|
||||
stats_table = Table(stats_data, colWidths=[1.8*inch, 1.8*inch, 1.8*inch, 1.8*inch])
|
||||
stats_table.setStyle(TableStyle([
|
||||
('BACKGROUND', (0, 0), (-1, 0), HexColor('#1e3a8a')),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||
('FONTNAME', (0, 0), (-1, -1), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, 0), 16),
|
||||
('FONTSIZE', (0, 1), (-1, 1), 18),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 20),
|
||||
('GRID', (0, 0), (-1, -1), 1, HexColor('#1e3a8a')),
|
||||
('ROUNDEDCORNERS', [15, 15, 15, 15])
|
||||
]))
|
||||
|
||||
story.append(stats_table)
|
||||
story.append(Spacer(1, 50))
|
||||
|
||||
# Footer
|
||||
story.append(Paragraph("CONTEMPORARY • ELEGANT • URBAN", self.styles['PremiumSubtitle']))
|
||||
story.append(Paragraph("Experience the future of urban living", self.styles['ContentText']))
|
||||
|
||||
return story
|
||||
|
||||
def _create_modern_features(self, data: Dict[str, Any]) -> List:
|
||||
"""Create modern features page"""
|
||||
story = []
|
||||
|
||||
story.append(Paragraph("MODERN FEATURES", self.styles['SectionHeader']))
|
||||
story.append(Spacer(1, 30))
|
||||
|
||||
# Design Features
|
||||
story.append(Paragraph("DESIGN FEATURES", self.styles['FeatureText']))
|
||||
story.append(Spacer(1, 15))
|
||||
|
||||
design_features = [
|
||||
"• Open-concept floor plan",
|
||||
"• Floor-to-ceiling windows",
|
||||
"• High ceilings",
|
||||
"• Modern minimalist design",
|
||||
"• Smart home integration",
|
||||
"• Energy-efficient appliances",
|
||||
"• Premium materials and finishes",
|
||||
"• Custom lighting design"
|
||||
]
|
||||
|
||||
for feature in design_features:
|
||||
story.append(Paragraph(feature, self.styles['ContentText']))
|
||||
|
||||
story.append(Spacer(1, 30))
|
||||
|
||||
# Technology Features
|
||||
story.append(Paragraph("TECHNOLOGY FEATURES", self.styles['FeatureText']))
|
||||
story.append(Spacer(1, 15))
|
||||
|
||||
tech_features = [
|
||||
"• Smart home automation",
|
||||
"• High-speed internet",
|
||||
"• Security camera systems",
|
||||
"• Digital door locks",
|
||||
"• Climate control systems",
|
||||
"• Entertainment systems",
|
||||
"• Mobile app control",
|
||||
"• Energy monitoring"
|
||||
]
|
||||
|
||||
for feature in tech_features:
|
||||
story.append(Paragraph(feature, self.styles['ContentText']))
|
||||
|
||||
return story
|
||||
|
||||
def _create_modern_amenities(self, data: Dict[str, Any]) -> List:
|
||||
"""Create modern amenities page"""
|
||||
story = []
|
||||
|
||||
story.append(Paragraph("MODERN AMENITIES", self.styles['SectionHeader']))
|
||||
story.append(Spacer(1, 30))
|
||||
|
||||
# Building Amenities
|
||||
story.append(Paragraph("BUILDING AMENITIES", self.styles['FeatureText']))
|
||||
story.append(Spacer(1, 15))
|
||||
|
||||
building_amenities = [
|
||||
"• Rooftop swimming pool",
|
||||
"• Fitness center with latest equipment",
|
||||
"• Co-working spaces",
|
||||
"• Rooftop terrace and gardens",
|
||||
"• Concierge services",
|
||||
"• Package delivery lockers",
|
||||
"• Bike storage",
|
||||
"• Electric vehicle charging"
|
||||
]
|
||||
|
||||
for amenity in building_amenities:
|
||||
story.append(Paragraph(f"🏢 {amenity}", self.styles['ContentText']))
|
||||
|
||||
story.append(Spacer(1, 30))
|
||||
|
||||
# Lifestyle Amenities
|
||||
story.append(Paragraph("LIFESTYLE AMENITIES", self.styles['FeatureText']))
|
||||
story.append(Spacer(1, 15))
|
||||
|
||||
lifestyle_amenities = [
|
||||
"• Community lounge areas",
|
||||
"• Outdoor dining spaces",
|
||||
"• Children's play areas",
|
||||
"• Pet-friendly facilities",
|
||||
"• Guest parking",
|
||||
"• 24/7 security",
|
||||
"• Maintenance services",
|
||||
"• Community events"
|
||||
]
|
||||
|
||||
for amenity in lifestyle_amenities:
|
||||
story.append(Paragraph(f"🌟 {amenity}", self.styles['ContentText']))
|
||||
|
||||
return story
|
||||
|
||||
def _create_modern_header_footer(self, canvas_obj, page_num: int, template_name: str):
|
||||
"""Create modern header and footer"""
|
||||
# Header
|
||||
canvas_obj.setFillColor(HexColor('#1e3a8a'))
|
||||
canvas_obj.setFont("Helvetica-Bold", 18)
|
||||
canvas_obj.drawString(50, A4[1] - 40, "MODERN REAL ESTATE")
|
||||
|
||||
canvas_obj.setFont("Helvetica", 14)
|
||||
canvas_obj.drawString(50, A4[1] - 60, "Contemporary Property Solutions")
|
||||
|
||||
# Template indicator
|
||||
canvas_obj.setFillColor(HexColor('#3b82f6'))
|
||||
canvas_obj.setFont("Helvetica-Bold", 16)
|
||||
canvas_obj.drawRightString(A4[0] - 50, A4[1] - 40, template_name)
|
||||
|
||||
# Footer
|
||||
canvas_obj.setFillColor(HexColor('#475569'))
|
||||
canvas_obj.setFont("Helvetica", 12)
|
||||
canvas_obj.drawCentredString(A4[0]/2, 35, f"Generated on {datetime.now().strftime('%B %d, %Y')}")
|
||||
canvas_obj.drawCentredString(A4[0]/2, 20, "Modern Real Estate - Contemporary Living")
|
||||
|
||||
# Page number
|
||||
canvas_obj.drawRightString(A4[0] - 50, 20, f"Page {page_num}")
|
||||
|
||||
def main():
|
||||
"""Test the advanced templates"""
|
||||
templates = AdvancedPropertyTemplates()
|
||||
|
||||
# Sample data
|
||||
sample_data = {
|
||||
'propertyName': 'Luxury Marina Villa',
|
||||
'propertyType': 'Villa',
|
||||
'location': 'Dubai Marina',
|
||||
'price': '5,500,000',
|
||||
'bedrooms': '5',
|
||||
'bathrooms': '6',
|
||||
'area': '4,200',
|
||||
'description': 'Stunning luxury villa with panoramic marina views, premium finishes, and exclusive amenities.',
|
||||
'amenities': ['Private Pool', 'Gym', 'Security', 'Garden', 'Garage', 'Smart Home']
|
||||
}
|
||||
|
||||
# Test luxury villa template
|
||||
try:
|
||||
result = templates.create_luxury_villa_template(sample_data, 'luxury_villa_brochure.pdf')
|
||||
print(f"Luxury villa PDF generated: {result}")
|
||||
except Exception as e:
|
||||
print(f"Error: {str(e)}")
|
||||
|
||||
# Test modern apartment template
|
||||
try:
|
||||
result = templates.create_modern_apartment_template(sample_data, 'modern_apartment_brochure.pdf')
|
||||
print(f"Modern apartment PDF generated: {result}")
|
||||
except Exception as e:
|
||||
print(f"Error: {str(e)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
626
python-pdf-generator/api_server.py
Normal file
626
python-pdf-generator/api_server.py
Normal file
@ -0,0 +1,626 @@
|
||||
#!/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)}")
|
||||
|
||||
@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
|
||||
)
|
||||
|
||||
# Store the generated PDF for later download
|
||||
filename = os.path.basename(result_path)
|
||||
generated_pdfs[filename] = result_path
|
||||
|
||||
# Generate download URL
|
||||
pdf_url = f"/download-pdf/{filename}"
|
||||
|
||||
return GeneratePDFResponse(
|
||||
success=True,
|
||||
message="PDF generated successfully",
|
||||
pdf_url=pdf_url
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
1561
python-pdf-generator/property_pdf_generator.py
Normal file
1561
python-pdf-generator/property_pdf_generator.py
Normal file
File diff suppressed because it is too large
Load Diff
18
python-pdf-generator/requirements.txt
Normal file
18
python-pdf-generator/requirements.txt
Normal file
@ -0,0 +1,18 @@
|
||||
# Core PDF generation
|
||||
reportlab>=4.0.0
|
||||
Pillow>=10.0.0
|
||||
|
||||
# HTTP and data handling
|
||||
requests>=2.31.0
|
||||
python-multipart>=0.0.6
|
||||
|
||||
# Data validation and serialization
|
||||
pydantic>=2.0.0
|
||||
|
||||
# Web framework
|
||||
fastapi>=0.100.0
|
||||
uvicorn>=0.20.0
|
||||
|
||||
# Testing
|
||||
pytest>=7.0.0
|
||||
pytest-cov>=4.0.0
|
||||
11
sfdx-project.json
Normal file
11
sfdx-project.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"packageDirectories": [
|
||||
{
|
||||
"path": "force-app",
|
||||
"default": true
|
||||
}
|
||||
],
|
||||
"namespace": "",
|
||||
"sfdcLoginUrl": "https://test.salesforce.com",
|
||||
"sourceApiVersion": "64.0"
|
||||
}
|
||||
2876
test-environment/index.html
Normal file
2876
test-environment/index.html
Normal file
File diff suppressed because it is too large
Load Diff
626
test-environment/test-property-api.html
Normal file
626
test-environment/test-property-api.html
Normal file
@ -0,0 +1,626 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Property Brochure Generator - API Test</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
|
||||
color: white;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 1.2rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.test-section {
|
||||
padding: 40px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.test-section h2 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.api-info {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.api-info h3 {
|
||||
color: #495057;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.api-endpoint {
|
||||
background: #e9ecef;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
font-family: monospace;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.property-selector {
|
||||
background: #f8f9fa;
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.form-group select,
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-group select:focus,
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.property-details {
|
||||
background: #fff;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 15px;
|
||||
padding: 30px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.property-details h3 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #3498db;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #3498db;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #2c3e50;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #3498db;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #f5c6cb;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #c3e6cb;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.api-status {
|
||||
display: inline-block;
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-online {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-offline {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Property Brochure Generator</h1>
|
||||
<p>Test API Integration - Property Selection & Data Population</p>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>🔌 API Information</h2>
|
||||
<div class="api-info">
|
||||
<h3>Test API Endpoints</h3>
|
||||
<div class="api-endpoint">GET /api/properties - Get all properties list</div>
|
||||
<div class="api-endpoint">GET /api/properties/{name} - Get property by name (e.g., PR-00036)</div>
|
||||
<div class="api-endpoint">POST /api/generate-pdf - Generate PDF brochure</div>
|
||||
|
||||
<div style="margin-top: 20px;">
|
||||
<strong>API Status:</strong>
|
||||
<span id="api-status" class="api-status status-offline">Checking...</span>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px;">
|
||||
<strong>Base URL:</strong>
|
||||
<span class="api-endpoint">http://localhost:8001</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Property Selection Test</h2>
|
||||
<div class="property-selector">
|
||||
<div class="form-group">
|
||||
<label for="property-select">Select Property:</label>
|
||||
<select id="property-select" onchange="handlePropertySelection()">
|
||||
<option value="">Choose a property to test...</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn" onclick="loadProperties()">Load Properties</button>
|
||||
<button class="btn btn-secondary" onclick="testPropertyByName()">Test Property by Name</button>
|
||||
<button class="btn btn-success" onclick="testPDFGeneration()">Test PDF Generation</button>
|
||||
</div>
|
||||
|
||||
<div id="loading" class="loading" style="display: none;">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
|
||||
<div id="error" class="error" style="display: none;"></div>
|
||||
<div id="success" class="success" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="property-details" class="property-details" style="display: none;">
|
||||
<h3>Selected Property Details</h3>
|
||||
<div id="details-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE_URL = 'http://localhost:8001';
|
||||
let properties = [];
|
||||
|
||||
// Check API status on page load
|
||||
window.onload = function() {
|
||||
checkAPIStatus();
|
||||
};
|
||||
|
||||
// Check if API is running
|
||||
async function checkAPIStatus() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/health`);
|
||||
if (response.ok) {
|
||||
document.getElementById('api-status').textContent = 'Online';
|
||||
document.getElementById('api-status').className = 'api-status status-online';
|
||||
loadProperties(); // Auto-load properties if API is online
|
||||
} else {
|
||||
document.getElementById('api-status').textContent = 'Offline';
|
||||
document.getElementById('api-status').className = 'api-status status-offline';
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('api-status').textContent = 'Offline';
|
||||
document.getElementById('api-status').className = 'api-status status-offline';
|
||||
showError('API is not running. Please start the test server: python test_api_server.py');
|
||||
}
|
||||
}
|
||||
|
||||
// Load all properties
|
||||
async function loadProperties() {
|
||||
showLoading(true);
|
||||
hideMessages();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/properties`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
properties = result.data;
|
||||
populatePropertyDropdown();
|
||||
showSuccess(`Loaded ${result.count} properties successfully!`);
|
||||
} else {
|
||||
throw new Error(result.message || 'Failed to load properties');
|
||||
}
|
||||
} catch (error) {
|
||||
showError(`Error loading properties: ${error.message}`);
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Populate property dropdown
|
||||
function populatePropertyDropdown() {
|
||||
const select = document.getElementById('property-select');
|
||||
select.innerHTML = '<option value="">Choose a property to test...</option>';
|
||||
|
||||
properties.forEach(prop => {
|
||||
const option = document.createElement('option');
|
||||
option.value = prop.value;
|
||||
option.textContent = prop.label;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle property selection
|
||||
async function handlePropertySelection() {
|
||||
const select = document.getElementById('property-select');
|
||||
const selectedId = select.value;
|
||||
|
||||
if (!selectedId) {
|
||||
hidePropertyDetails();
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedProperty = properties.find(p => p.value === selectedId);
|
||||
if (selectedProperty) {
|
||||
displayPropertyDetails(selectedProperty.property);
|
||||
}
|
||||
}
|
||||
|
||||
// Display property details
|
||||
function displayPropertyDetails(property) {
|
||||
const detailsDiv = document.getElementById('property-details');
|
||||
const contentDiv = document.getElementById('details-content');
|
||||
|
||||
contentDiv.innerHTML = `
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Property ID</span>
|
||||
<span class="detail-value">${property.Name}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Property Type</span>
|
||||
<span class="detail-value">${property.pcrm__Property_Type__c || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Location</span>
|
||||
<span class="detail-value">${property.pcrm__Sub_Locality_Bayut_Dubizzle__c || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Sale Price</span>
|
||||
<span class="detail-value">${property.pcrm__Sale_Price_max__c ? `AED ${property.pcrm__Sale_Price_max__c.toLocaleString()}` : 'N/A'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Rent Price</span>
|
||||
<span class="detail-value">${property.pcrm__Rent_Price_max__c ? `AED ${property.pcrm__Rent_Price_max__c.toLocaleString()}` : 'N/A'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Bedrooms</span>
|
||||
<span class="detail-value">${property.pcrm__Bedrooms__c || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Bathrooms</span>
|
||||
<span class="detail-value">${property.pcrm__Bathrooms__c || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Size (sq ft)</span>
|
||||
<span class="detail-value">${property.pcrm__Size__c || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Unit Number</span>
|
||||
<span class="detail-value">${property.pcrm__Unit_Number__c || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Completion Status</span>
|
||||
<span class="detail-value">${property.pcrm__Completion_Status__c || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Furnished</span>
|
||||
<span class="detail-value">${property.pcrm__Furnished__c || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">View</span>
|
||||
<span class="detail-value">${property.pcrm__View__c || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Tower</span>
|
||||
<span class="detail-value">${property.pcrm__Tower_Bayut_Dubizzle__c || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">City</span>
|
||||
<span class="detail-value">${property.pcrm__City_Bayut_Dubizzle__c || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Build Year</span>
|
||||
<span class="detail-value">${property.pcrm__Build_Year__c || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Stories</span>
|
||||
<span class="detail-value">${property.pcrm__Stories__c || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Parking Spaces</span>
|
||||
<span class="detail-value">${property.pcrm__Parking_Spaces__c || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Service Charge</span>
|
||||
<span class="detail-value">${property.pcrm__Service_Charge__c ? `AED ${property.pcrm__Service_Charge__c.toLocaleString()}` : 'N/A'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Private Amenities</span>
|
||||
<span class="detail-value">${property.pcrm__Private_Amenities__c || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Commercial Amenities</span>
|
||||
<span class="detail-value">${property.pcrm__Commercial_Amenities__c || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Coordinates</span>
|
||||
<span class="detail-value">${property.pcrm__Coordinates__c || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Description</span>
|
||||
<span class="detail-value">${property.pcrm__Description_English__c || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Title (English)</span>
|
||||
<span class="detail-value">${property.pcrm__Title_English__c || 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
detailsDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
// Test property by name
|
||||
async function testPropertyByName() {
|
||||
const propertyName = prompt('Enter property name (e.g., PR-00036):');
|
||||
if (!propertyName) return;
|
||||
|
||||
showLoading(true);
|
||||
hideMessages();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/properties/${propertyName}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
displayPropertyDetails(result.data);
|
||||
showSuccess(`Property ${propertyName} loaded successfully!`);
|
||||
} else {
|
||||
throw new Error(result.message || 'Failed to load property');
|
||||
}
|
||||
} catch (error) {
|
||||
showError(`Error loading property ${propertyName}: ${error.message}`);
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Test PDF generation
|
||||
async function testPDFGeneration() {
|
||||
const select = document.getElementById('property-select');
|
||||
const selectedId = select.value;
|
||||
|
||||
if (!selectedId) {
|
||||
showError('Please select a property first');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedProperty = properties.find(p => p.value === selectedId);
|
||||
if (!selectedProperty) {
|
||||
showError('Selected property not found');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading(true);
|
||||
hideMessages();
|
||||
|
||||
try {
|
||||
const propertyData = {
|
||||
propertyName: selectedProperty.property.Name,
|
||||
propertyType: selectedProperty.property.pcrm__Property_Type__c || '',
|
||||
location: selectedProperty.property.pcrm__Sub_Locality_Bayut_Dubizzle__c || '',
|
||||
price: selectedProperty.property.pcrm__Sale_Price_max__c || selectedProperty.property.pcrm__Rent_Price_max__c || 0,
|
||||
bedrooms: selectedProperty.property.pcrm__Bedrooms__c || 0,
|
||||
bathrooms: selectedProperty.property.pcrm__Bathrooms__c || 0,
|
||||
area: selectedProperty.property.pcrm__Size__c || 0,
|
||||
description: selectedProperty.property.pcrm__Description_English__c || '',
|
||||
titleEnglish: selectedProperty.property.pcrm__Title_English__c || '',
|
||||
unitNumber: selectedProperty.property.pcrm__Unit_Number__c || '',
|
||||
completionStatus: selectedProperty.property.pcrm__Completion_Status__c || '',
|
||||
furnished: selectedProperty.property.pcrm__Furnished__c || '',
|
||||
view: selectedProperty.property.pcrm__View__c || '',
|
||||
tower: selectedProperty.property.pcrm__Tower_Bayut_Dubizzle__c || '',
|
||||
city: selectedProperty.property.pcrm__City_Bayut_Dubizzle__c || '',
|
||||
subCommunity: selectedProperty.property.pcrm__Sub_Community_Propertyfinder__c || '',
|
||||
buildYear: selectedProperty.property.pcrm__Build_Year__c || '',
|
||||
stories: selectedProperty.property.pcrm__Stories__c || 0,
|
||||
parkingSpaces: selectedProperty.property.pcrm__Parking_Spaces__c || 0,
|
||||
lotSize: selectedProperty.property.pcrm__Lot_Size__c || 0,
|
||||
serviceCharge: selectedProperty.property.pcrm__Service_Charge__c || 0,
|
||||
privateAmenities: selectedProperty.property.pcrm__Private_Amenities__c || '',
|
||||
commercialAmenities: selectedProperty.property.pcrm__Commercial_Amenities__c || '',
|
||||
coordinates: selectedProperty.property.pcrm__Coordinates__c || '',
|
||||
amenities: [],
|
||||
images: []
|
||||
};
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/generate-pdf`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
property_data: propertyData,
|
||||
template_name: 'professional-3pager'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showSuccess(`PDF generated successfully! Mock URL: ${result.pdf_url}`);
|
||||
} else {
|
||||
throw new Error(result.message || 'Failed to generate PDF');
|
||||
}
|
||||
} catch (error) {
|
||||
showError(`Error generating PDF: ${error.message}`);
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function showLoading(show) {
|
||||
document.getElementById('loading').style.display = show ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const errorDiv = document.getElementById('error');
|
||||
errorDiv.textContent = message;
|
||||
errorDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
const successDiv = document.getElementById('success');
|
||||
successDiv.textContent = message;
|
||||
successDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
function hideMessages() {
|
||||
document.getElementById('error').style.display = 'none';
|
||||
document.getElementById('success').style.display = 'none';
|
||||
}
|
||||
|
||||
function hidePropertyDetails() {
|
||||
document.getElementById('property-details').style.display = 'none';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user