From 9a33862445cd1995867316aced7442be5fcf8442 Mon Sep 17 00:00:00 2001
From: Ubuntu
Date: Sat, 13 Sep 2025 20:34:53 +0530
Subject: [PATCH] v1.0.5-bet
---
.../classes/PropertyDataController.cls | 142 +
.../propertyTemplateSelector copy 2.js | 12248 ++++++++++++++++
.../propertyTemplateSelector.css | 2356 ++-
.../propertyTemplateSelector.html | 260 +-
.../propertyTemplateSelector.js | 5463 ++++++-
.../CompanyLogo.resource-meta.xml | 5 +
.../PropertyLogo.resource-meta.xml | 5 +
.../default/staticresources/PropertyLogo.svg | 3 +
.../default/staticresources/companyLogo.jpeg | Bin 0 -> 21904 bytes
template samples/grand-oak.html | 0
10 files changed, 19338 insertions(+), 1144 deletions(-)
create mode 100644 force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector copy 2.js
create mode 100644 force-app/main/default/staticresources/CompanyLogo.resource-meta.xml
create mode 100644 force-app/main/default/staticresources/PropertyLogo.resource-meta.xml
create mode 100644 force-app/main/default/staticresources/PropertyLogo.svg
create mode 100644 force-app/main/default/staticresources/companyLogo.jpeg
create mode 100644 template samples/grand-oak.html
diff --git a/force-app/main/default/classes/PropertyDataController.cls b/force-app/main/default/classes/PropertyDataController.cls
index 7f8a29a..e81e642 100644
--- a/force-app/main/default/classes/PropertyDataController.cls
+++ b/force-app/main/default/classes/PropertyDataController.cls
@@ -20,6 +20,7 @@ public with sharing class PropertyDataController {
'pcrm__Sub_Community_Propertyfinder__c, pcrm__Property_Name_Propertyfinder__c, ' +
'pcrm__City_Propertyfinder__c, ' +
'pcrm__Rent_Available_From__c, pcrm__Rent_Available_To__c, ' +
+ 'Private_Amenities__c, ' +
'Contact__c, Contact__r.FirstName, Contact__r.LastName, Contact__r.Email, Contact__r.Phone, ' +
'Email__c, Phone__c, ' +
'CreatedBy.Name, LastModifiedBy.Name, Owner.Name, ' +
@@ -55,6 +56,7 @@ public with sharing class PropertyDataController {
System.debug('Build Year: ' + firstProp.pcrm__Build_Year__c);
System.debug('Parking Spaces: ' + firstProp.pcrm__Parking_Spaces__c);
System.debug('Offering Type: ' + firstProp.pcrm__Offering_Type__c);
+ System.debug('Private Amenities: ' + firstProp.Private_Amenities__c);
}
return properties;
@@ -87,6 +89,7 @@ public with sharing class PropertyDataController {
'pcrm__Sub_Community_Propertyfinder__c, pcrm__Property_Name_Propertyfinder__c, ' +
'pcrm__City_Propertyfinder__c, ' +
'pcrm__Rent_Available_From__c, pcrm__Rent_Available_To__c, ' +
+ 'Private_Amenities__c, ' +
'Contact__c, Contact__r.FirstName, Contact__r.LastName, Contact__r.Email, Contact__r.Phone, ' +
'Email__c, Phone__c, ' +
'CreatedBy.Name, LastModifiedBy.Name, Owner.Name, ' +
@@ -117,6 +120,7 @@ public with sharing class PropertyDataController {
System.debug('Build Year: ' + property.pcrm__Build_Year__c);
System.debug('Parking Spaces: ' + property.pcrm__Parking_Spaces__c);
System.debug('Offering Type: ' + property.pcrm__Offering_Type__c);
+ System.debug('Private Amenities: ' + property.Private_Amenities__c);
}
return property;
@@ -175,4 +179,142 @@ public with sharing class PropertyDataController {
throw new AuraHandledException('Failed to fetch property images: ' + e.getMessage());
}
}
+
+ @AuraEnabled(cacheable=true)
+ public static User getAgentData(String propertyId) {
+ try {
+ System.debug('=== FETCHING AGENT DATA FOR PROPERTY ===');
+ System.debug('Property ID: ' + propertyId);
+
+ // First, get the property to find the related agent/owner
+ pcrm__Property__c property = [
+ SELECT Id, OwnerId, CreatedById, LastModifiedById, Contact__c, Contact__r.OwnerId
+ FROM pcrm__Property__c
+ WHERE Id = :propertyId
+ LIMIT 1
+ ];
+
+ if (property == null) {
+ System.debug('Property not found for ID: ' + propertyId);
+ return null;
+ }
+
+ // Try to get agent data from different sources in priority order
+ User agentUser = null;
+
+ // Priority 1: Contact's owner (if contact exists)
+ if (property.Contact__c != null && property.Contact__r.OwnerId != null) {
+ try {
+ agentUser = [
+ SELECT Id, Name, FirstName, LastName, Email, Phone, MobilePhone,
+ Title, Department, CompanyName, SmallPhotoUrl, FullPhotoUrl,
+ Profile.Name, UserRole.Name
+ FROM User
+ WHERE Id = :property.Contact__r.OwnerId
+ AND IsActive = true
+ LIMIT 1
+ ];
+ System.debug('Found agent from Contact Owner: ' + agentUser?.Name);
+ } catch (Exception e) {
+ System.debug('Error fetching Contact Owner: ' + e.getMessage());
+ }
+ }
+
+ // Priority 2: Property Owner (if not found above)
+ if (agentUser == null && property.OwnerId != null) {
+ try {
+ agentUser = [
+ SELECT Id, Name, FirstName, LastName, Email, Phone, MobilePhone,
+ Title, Department, CompanyName, SmallPhotoUrl, FullPhotoUrl,
+ Profile.Name, UserRole.Name
+ FROM User
+ WHERE Id = :property.OwnerId
+ AND IsActive = true
+ LIMIT 1
+ ];
+ System.debug('Found agent from Property Owner: ' + agentUser?.Name);
+ } catch (Exception e) {
+ System.debug('Error fetching Property Owner: ' + e.getMessage());
+ }
+ }
+
+ // Priority 3: Property Creator (if not found above)
+ if (agentUser == null && property.CreatedById != null) {
+ try {
+ agentUser = [
+ SELECT Id, Name, FirstName, LastName, Email, Phone, MobilePhone,
+ Title, Department, CompanyName, SmallPhotoUrl, FullPhotoUrl,
+ Profile.Name, UserRole.Name
+ FROM User
+ WHERE Id = :property.CreatedById
+ AND IsActive = true
+ LIMIT 1
+ ];
+ System.debug('Found agent from Property Creator: ' + agentUser?.Name);
+ } catch (Exception e) {
+ System.debug('Error fetching Property Creator: ' + e.getMessage());
+ }
+ }
+
+ if (agentUser != null) {
+ System.debug('=== AGENT DATA FETCHED ===');
+ System.debug('Agent Name: ' + agentUser.Name);
+ System.debug('Agent Email: ' + agentUser.Email);
+ System.debug('Agent Phone: ' + agentUser.Phone);
+ System.debug('Agent Title: ' + agentUser.Title);
+ System.debug('Agent Department: ' + agentUser.Department);
+ } else {
+ System.debug('No agent found for property: ' + propertyId);
+ }
+
+ return agentUser;
+
+ } catch (Exception e) {
+ System.debug('Error fetching agent data: ' + e.getMessage());
+ System.debug('Stack trace: ' + e.getStackTraceString());
+ throw new AuraHandledException('Failed to fetch agent data: ' + e.getMessage());
+ }
+ }
+
+ @AuraEnabled(cacheable=true)
+ public static pcrm__Listing__c getListingData(String propertyId) {
+ try {
+ System.debug('=== FETCHING LISTING DATA BY PROPERTY ===');
+ System.debug('Property ID: ' + propertyId);
+
+ // Query listing with related property and agent data using property ID
+ String query = 'SELECT Id, Name, ' +
+ 'Property__r.Id, Property__r.Name, ' +
+ 'Listing_Agent__r.Id, Listing_Agent__r.Name, Listing_Agent__r.Email, Listing_Agent__r.Phone, Listing_Agent__r.Title, ' +
+ 'Select_Agent__r.Id, Select_Agent__r.Name, Select_Agent__r.Email, Select_Agent__r.Phone, Select_Agent__r.Title ' +
+ 'FROM pcrm__Listing__c ' +
+ 'WHERE Property__c = :propertyId ' +
+ 'ORDER BY CreatedDate DESC LIMIT 1';
+
+ List listings = Database.query(query);
+ pcrm__Listing__c listing = listings.isEmpty() ? null : listings[0];
+
+ if (listing != null) {
+ System.debug('=== LISTING DATA FETCHED ===');
+ System.debug('Listing Name: ' + listing.Name);
+ System.debug('Property ID: ' + listing.Property__r?.Id);
+ System.debug('Property Name: ' + listing.Property__r?.Name);
+ System.debug('Listing Agent ID: ' + listing.Listing_Agent__r?.Id);
+ System.debug('Listing Agent Name: ' + listing.Listing_Agent__r?.Name);
+ System.debug('Listing Agent Email: ' + listing.Listing_Agent__r?.Email);
+ System.debug('Listing Agent Phone: ' + listing.Listing_Agent__r?.Phone);
+ System.debug('Select Agent ID: ' + listing.Select_Agent__r?.Id);
+ System.debug('Select Agent Name: ' + listing.Select_Agent__r?.Name);
+ } else {
+ System.debug('No listing found for Property ID: ' + propertyId);
+ }
+
+ return listing;
+
+ } catch (Exception e) {
+ System.debug('Error fetching listing data: ' + e.getMessage());
+ System.debug('Stack trace: ' + e.getStackTraceString());
+ throw new AuraHandledException('Failed to fetch listing data: ' + e.getMessage());
+ }
+ }
}
diff --git a/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector copy 2.js b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector copy 2.js
new file mode 100644
index 0000000..4a6e8b3
--- /dev/null
+++ b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector copy 2.js
@@ -0,0 +1,12248 @@
+import { LightningElement, track, wire } from "lwc";
+import { CurrentPageReference } from "lightning/navigation";
+import getProperties from "@salesforce/apex/PropertyDataController.getProperties";
+import generatePDFFromHTML from "@salesforce/apex/PDFGenerationProxy.generatePDFFromHTML";
+import generateCompressedPDF from "@salesforce/apex/PDFGenerationProxy.generateCompressedPDF";
+import getPropertyImages from "@salesforce/apex/PropertyDataController.getPropertyImages";
+import logoUrl from '@salesforce/resourceUrl/PropertyLogo';
+
+export default class PropertyTemplateSelector extends LightningElement {
+ @track currentStep = 1;
+ htmlContent = ""; // Remove @track to prevent reactive updates
+
+ // Getter for logo URL
+ get logoUrl() {
+ return logoUrl;
+ }
+
+ // Lifecycle method - called when component is rendered
+ renderedCallback() {
+ // If we're on step 3 and have template/property selected, load the template
+ if (
+ this.currentStep === 3 &&
+ this.selectedTemplateId &&
+ this.selectedPropertyId
+ ) {
+ this.loadTemplateInStep3();
+ }
+ }
+ @track properties = [];
+ @track selectedPropertyId = "";
+ @track propertyData = {};
+ @track isLoading = false;
+ @track error = "";
+
+ // Search and filter properties
+ @track propertySearchTerm = "";
+ @track propertyFilterType = "all";
+ @track propertyFilterCity = "all";
+ @track marketAnalysis = {
+ includeMarketData: true,
+ includeROIAnalysis: true,
+ includeComparableSales: true,
+ includeRentalYield: true,
+ includeGrowthProjection: true,
+ };
+
+ // PDF generation properties
+ @track exportPdfButtonText = "📄 Generate PDF";
+ @track showPdfPreview = false;
+ @track editorContent = "";
+ @track pageCount = 0;
+ @track progressMessage = "";
+ @track selectedPageSize = "A4"; // Default page size
+ @track zoom = 1.0; // Step 3 viewport zoom
+ @track isGeneratingPdf = false; // Loading state for PDF generation
+ @track previewPages = []; // Array of pages for viewport display
+ cachedTemplateContent = null; // Cache template content to prevent regeneration
+
+ // Image review properties
+ @track showImageReview = false;
+ @track selectedCategory = "Interior"; // Will be updated when images load
+ @track currentImageIndex = 0;
+ @track totalImages = 0;
+ @track currentImage = null;
+
+ // Real property images from Salesforce
+ @track realPropertyImages = [];
+ @track propertyImages = [];
+ @track imagesByCategory = {
+ Interior: [
+ {
+ url: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Interior View 1",
+ category: "Interior",
+ },
+ {
+ url: "https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800",
+ title: "Interior View 2",
+ category: "Interior",
+ },
+ {
+ url: "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Interior View 3",
+ category: "Interior",
+ },
+ ],
+ Exterior: [
+ {
+ url: "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Exterior View 1",
+ category: "Exterior",
+ },
+ {
+ url: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Exterior View 2",
+ category: "Exterior",
+ },
+ ],
+ Kitchen: [
+ {
+ url: "https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Kitchen View 1",
+ category: "Kitchen",
+ },
+ {
+ url: "https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Kitchen View 2",
+ category: "Kitchen",
+ },
+ ],
+ Bedroom: [
+ {
+ url: "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Bedroom View 1",
+ category: "Bedroom",
+ },
+ {
+ url: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Bedroom View 2",
+ category: "Bedroom",
+ },
+ ],
+ "Living Area": [
+ {
+ url: "https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800",
+ title: "Living Area View 1",
+ category: "Living Area",
+ },
+ {
+ url: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Living Area View 2",
+ category: "Living Area",
+ },
+ ],
+ Parking: [
+ {
+ url: "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Parking View 1",
+ category: "Parking",
+ },
+ ],
+ Anchor: [
+ {
+ url: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Anchor View 1",
+ category: "Anchor",
+ },
+ ],
+ Maps: [
+ {
+ url: "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Map View 1",
+ category: "Maps",
+ },
+ ],
+ };
+
+ // Capture URL param c__propertyId and hydrate selection
+ @wire(CurrentPageReference)
+ setPageRef(ref) {
+ try {
+ const pid = ref && ref.state && ref.state.c__propertyId;
+ if (pid && pid !== this.selectedPropertyId) {
+ this.selectedPropertyId = pid;
+ const hydrate = () => {
+ this.loadPropertyData();
+ this.loadPropertyImages();
+ };
+ if (this.properties && this.properties.length > 0) {
+ hydrate();
+ } else {
+ this._deferHydrateFromUrl = hydrate;
+ }
+ }
+ } catch (e) {
+ // ignore
+ }
+ }
+
+ // Template selection states - simplified approach
+ @track selectedTemplateId = "";
+
+ // Image Replacement Variables
+ @track showImageReplacement = false;
+ @track selectedImageElement = null;
+ @track replacementActiveTab = "property"; // 'property' or 'upload'
+ @track replacementSelectedCategory = "Interior"; // Will be updated when images load
+ @track filteredReplacementImages = [];
+ @track uploadedImagePreview = null;
+
+ // Triple click detection for image replacement
+ @track imageClickCount = 0;
+ @track lastClickedImage = null;
+ @track clickTimeout = null;
+
+ // Undo/Redo functionality
+ @track undoStack = [];
+ @track redoStack = [];
+ @track maxUndoSteps = 20;
+
+ // Category selection tracking
+ @track initialCategorySelected = false;
+ // Template Save/Load Variables
+ @track showSaveDialog = false;
+ @track showLoadDialog = false;
+ @track savedTemplates = [];
+ @track saveTemplateName = "";
+ @track showHtmlDialog = false;
+ @track exportedHtml = "";
+ // Table Dialog Variables
+ @track showTableDialog = false;
+ @track tableRows = 3;
+ @track tableCols = 3;
+ @track includeHeader = true;
+
+ // Image insertion modal properties
+ @track showImageModal = false;
+ @track imageSource = "property"; // 'property' or 'local'
+ @track selectedImageCategory = "all";
+ @track selectedImageUrl = "";
+ @track selectedImageName = "";
+ @track uploadedImageData = "";
+ @track renderKey = 0; // For forcing re-renders
+ @track insertButtonDisabled = true; // Explicit button state
+
+ // Table Drag and Drop Variables
+ @track isDraggingTable = false;
+ @track draggedTableData = null;
+ @track selectorMode = false;
+ @track showDownloadModal = false;
+ @track downloadInfo = {};
+ @track selectedElement = null;
+ // z-index controls removed per request
+
+ // Undo functionality
+ @track undoStack = [];
+ @track redoStack = [];
+ maxUndoSteps = 50;
+
+ // Computed properties for image replacement tabs
+ get propertyImagesTabClass() {
+ return this.replacementActiveTab === "property"
+ ? "source-tab active"
+ : "source-tab";
+ }
+
+ // Unified gallery section used across templates
+ generateUnifiedGallerySectionHTML() {
+ const imagesHTML = this.generatePropertyGalleryHTML();
+ return `
+
+
Property Gallery
+
+ ${imagesHTML}
+
+
`;
+ }
+
+ // z-index functions removed
+
+ get localUploadTabClass() {
+ return this.replacementActiveTab === "upload"
+ ? "source-tab active"
+ : "source-tab";
+ }
+
+ get showPropertyImagesTab() {
+ return this.replacementActiveTab === "property";
+ }
+
+ get showLocalUploadTab() {
+ return this.replacementActiveTab === "upload";
+ }
+
+ // Image insertion modal getters
+ get isImageModalOpen() {
+ return this.showImageModal;
+ }
+
+ get imageCategories() {
+ const categories = [
+ { label: "All Images", value: "all" },
+ { label: "Exterior", value: "exterior" },
+ { label: "Interior", value: "interior" },
+ { label: "Kitchen", value: "kitchen" },
+ { label: "Bedroom", value: "bedroom" },
+ { label: "Bathroom", value: "bathroom" },
+ { label: "Living Room", value: "living" },
+ { label: "Maps", value: "maps" },
+ { label: "None", value: "none" },
+ ];
+ return categories;
+ }
+ get filteredPropertyImages() {
+ if (!this.propertyImages || this.propertyImages.length === 0) {
+ return [];
+ }
+
+ if (this.selectedImageCategory === "all") {
+ return this.propertyImages;
+ }
+
+ const filtered = this.propertyImages.filter((image) => {
+ const category = image.category ? image.category.toLowerCase() : "none";
+ return category === this.selectedImageCategory;
+ });
+
+ return filtered;
+ }
+
+ get propertyTabClass() {
+ return this.imageSource === "property" ? "tab-btn active" : "tab-btn";
+ }
+
+ get localTabClass() {
+ return this.imageSource === "local" ? "tab-btn active" : "tab-btn";
+ }
+
+ getCategoryButtonClass(categoryValue) {
+ return this.selectedImageCategory === categoryValue
+ ? "category-btn active"
+ : "category-btn";
+ }
+
+ get showPropertyImagesSection() {
+ return this.imageSource === "property";
+ }
+
+ get showLocalUploadSection() {
+ return this.imageSource === "local";
+ }
+
+ get isInsertButtonDisabled() {
+ const disabled = !this.selectedImageUrl || this.selectedImageUrl === "";
+ return disabled;
+ }
+
+ // Computed properties for template selection
+ get isBlankTemplateSelected() {
+ return this.selectedTemplateId === "blank-template";
+ }
+
+ get isEverkindTemplateSelected() {
+ return this.selectedTemplateId === "everkind-template";
+ }
+
+ get isShiftTemplateSelected() {
+ return this.selectedTemplateId === "shift-template";
+ }
+
+ get isSaintbartsTemplateSelected() {
+ return this.selectedTemplateId === "saintbarts-template";
+ }
+
+ get isLearnoyTemplateSelected() {
+ return this.selectedTemplateId === "learnoy-template";
+ }
+
+ get isLeafampTemplateSelected() {
+ return this.selectedTemplateId === "leafamp-template";
+ }
+
+ get isCoreshiftTemplateSelected() {
+ return this.selectedTemplateId === "coreshift-template";
+ }
+
+ get isModernHomeTemplateSelected() {
+ return this.selectedTemplateId === "modern-home-template";
+ }
+
+ get isGrandOakVillaTemplateSelected() {
+ return this.selectedTemplateId === "grand-oak-villa-template";
+ }
+
+ get isSampleTemplateSelected() {
+ return this.selectedTemplateId === "sample-template";
+ }
+
+ get isSerenityHouseTemplateSelected() {
+ return this.selectedTemplateId === "serenity-house-template";
+ }
+
+ get isLuxuryMansionTemplateSelected() {
+ return this.selectedTemplateId === "luxury-mansion-template";
+ }
+
+ // Image review computed properties
+ get isFirstImage() {
+ return this.currentImageIndex === 0;
+ }
+
+ get isLastImage() {
+ return this.currentImageIndex === this.totalImages - 1;
+ }
+
+ get displayImageIndex() {
+ return this.currentImageIndex + 1;
+ }
+
+ // Computed properties for step visibility
+ get step1Class() {
+ return this.currentStep === 1 ? "step-content active" : "step-content";
+ }
+ get step2Class() {
+ return this.currentStep === 2 ? "step-content active" : "step-content";
+ }
+ get step3Class() {
+ return this.currentStep === 3 ? "step-content active" : "step-content";
+ }
+
+ // Step navigation classes (for header stepper circles only)
+ get step1NavClass() {
+ return this.currentStep === 1 ? "active-circle active" : "";
+ }
+
+ get step2NavClass() {
+ return this.currentStep === 2 ? "active-circle active" : "";
+ }
+
+ get step3NavClass() {
+ return this.currentStep === 3 ? "active-circle active" : "";
+ }
+
+ // Inline styles to hard-force blue fill for active circles
+ get step1NavStyle() {
+ return this.currentStep === 1
+ ? "background:#1e88e5;border-color:#1e88e5;color:#ffffff;"
+ : "";
+ }
+ get step2NavStyle() {
+ return this.currentStep === 2
+ ? "background:#1e88e5;border-color:#1e88e5;color:#ffffff;"
+ : "";
+ }
+ get step3NavStyle() {
+ return this.currentStep === 3
+ ? "background:#1e88e5;border-color:#1e88e5;color:#ffffff;"
+ : "";
+ }
+
+ // Image availability flag
+ get hasPropertyImages() {
+ return (
+ Array.isArray(this.realPropertyImages) &&
+ this.realPropertyImages.length > 0
+ );
+ }
+
+ // Dynamic class for pdf viewport to enable placeholder styling when no images
+ get pdfViewportClass() {
+ return this.hasPropertyImages ? "pdf-viewport" : "pdf-viewport no-images";
+ }
+
+ originalTemplateGridHTML = null;
+ originalStylesText = null;
+
+ renderedCallback() {
+ if (this.currentStep === 1) {
+ const gridLive = this.template.querySelector("#all-templates");
+ if (gridLive) {
+ // Always keep a clean snapshot from the DOM the first time we hit step 1 in a render pass
+ if (
+ this.originalTemplateGridHTML === null ||
+ this.originalTemplateGridHTML.length < 50
+ ) {
+ this.originalTemplateGridHTML = gridLive.innerHTML;
+ }
+ }
+ }
+ if (this.originalTemplateGridHTML === null) {
+ const grid = this.template.querySelector("#all-templates");
+ if (grid) {
+ this.originalTemplateGridHTML = grid.innerHTML;
+ }
+ }
+ if (this.originalStylesText === null) {
+ const styles = this.template.querySelectorAll("style");
+ this.originalStylesText = Array.from(styles).map((s) => s.textContent);
+ }
+ }
+
+ resetStep1Grid() {
+ const grid = this.template.querySelector("#all-templates");
+ if (!grid) return;
+ // Clear any selected states on cards
+ const cards = grid.querySelectorAll(".template-card");
+ cards.forEach((card) => {
+ card.classList.remove("selected");
+ });
+ // DON'T clear selectedTemplateId - preserve the selection
+ // this.selectedTemplateId = '';
+ }
+
+ restoreComponentStyles() {
+ if (!this.originalStylesText) return;
+ const styles = this.template.querySelectorAll("style");
+ const snapshots = this.originalStylesText;
+ styles.forEach((styleEl, idx) => {
+ if (
+ snapshots[idx] !== undefined &&
+ styleEl.textContent !== snapshots[idx]
+ ) {
+ styleEl.textContent = snapshots[idx];
+ }
+ });
+ }
+ get isNextButtonDisabled() {
+ return !this.selectedTemplateId;
+ }
+
+ get isNextButtonDisabledStep2() {
+ return !this.selectedPropertyId;
+ }
+
+ // Wire method to get properties
+ @wire(getProperties)
+ wiredProperties({ error, data }) {
+ if (data) {
+ this.properties = data;
+ if (this._deferHydrateFromUrl) {
+ try {
+ this._deferHydrateFromUrl();
+ } catch (e) {}
+ this._deferHydrateFromUrl = null;
+ }
+ } else if (error) {
+ this.error = "Error loading properties: " + error.body.message;
+ }
+ }
+
+ // Derived list with selected flag to force dropdown selection rendering
+ get propertiesWithSelected() {
+ const selectedId = this.selectedPropertyId || "";
+ return (this.properties || []).map((p) => ({
+ ...p,
+ _selected: p.Id === selectedId,
+ }));
+ }
+
+ // Filtered properties based on search and filters
+ get filteredProperties() {
+ let filtered = this.properties || [];
+
+ // Apply search filter
+ if (this.propertySearchTerm) {
+ const searchTerm = this.propertySearchTerm.toLowerCase();
+ filtered = filtered.filter(property =>
+ (property.Name && property.Name.toLowerCase().includes(searchTerm)) ||
+ (property.pcrm__Title_English__c && property.pcrm__Title_English__c.toLowerCase().includes(searchTerm)) ||
+ (property.pcrm__Property_Type__c && property.pcrm__Property_Type__c.toLowerCase().includes(searchTerm)) ||
+ (property.pcrm__City_Bayut_Dubizzle__c && property.pcrm__City_Bayut_Dubizzle__c.toLowerCase().includes(searchTerm)) ||
+ (property.Address__c && property.Address__c.toLowerCase().includes(searchTerm))
+ );
+ }
+
+ // Apply property type filter
+ if (this.propertyFilterType !== "all") {
+ filtered = filtered.filter(property =>
+ property.pcrm__Property_Type__c === this.propertyFilterType
+ );
+ }
+
+ // Apply city filter
+ if (this.propertyFilterCity !== "all") {
+ filtered = filtered.filter(property =>
+ property.pcrm__City_Bayut_Dubizzle__c === this.propertyFilterCity
+ );
+ }
+
+ return filtered;
+ }
+
+ // Get unique property types for filter dropdown
+ get uniquePropertyTypes() {
+ const types = [...new Set((this.properties || []).map(p => p.pcrm__Property_Type__c).filter(Boolean))];
+ return types.sort();
+ }
+
+ // Get unique cities for filter dropdown
+ get uniqueCities() {
+ const cities = [...new Set((this.properties || []).map(p => p.pcrm__City_Bayut_Dubizzle__c).filter(Boolean))];
+ return cities.sort();
+ }
+
+ // Get filtered properties with selected flag
+ get filteredPropertiesWithSelected() {
+ const selectedId = this.selectedPropertyId || "";
+ return this.filteredProperties.map((p) => ({
+ ...p,
+ _selected: p.Id === selectedId,
+ }));
+ }
+
+ // Template selection handler
+ handleTemplateSelect(event) {
+ const templateId = event.currentTarget.dataset.templateId;
+
+ // Clear cached content when selecting a new template
+ this.cachedTemplateContent = null;
+
+ // Set the selected template
+ switch (templateId) {
+ case "blank-template":
+ this.selectedTemplateId = "blank-template";
+ break;
+ case "everkind-template":
+ this.selectedTemplateId = "everkind-template";
+ break;
+ case "shift-template":
+ this.selectedTemplateId = "shift-template";
+ break;
+ case "saintbarts-template":
+ this.selectedTemplateId = "saintbarts-template";
+ break;
+ case "learnoy-template":
+ this.selectedTemplateId = "learnoy-template";
+ break;
+ case "leafamp-template":
+ this.selectedTemplateId = "leafamp-template";
+ break;
+ case "coreshift-template":
+ this.selectedTemplateId = "coreshift-template";
+ break;
+ case "modern-home-template":
+ this.selectedTemplateId = "modern-home-template";
+ break;
+ case "grand-oak-villa-template":
+ this.selectedTemplateId = "grand-oak-villa-template";
+ break;
+ case "serenity-house-template":
+ this.selectedTemplateId = "serenity-house-template";
+ break;
+ case "sample-template":
+ this.selectedTemplateId = "sample-template";
+ break;
+ case "luxury-mansion-template":
+ this.selectedTemplateId = "luxury-mansion-template";
+ break;
+ default:
+ break;
+ }
+
+ // Visually mark the selected template card with a black border
+ try {
+ this.template.querySelectorAll(".template-card").forEach((card) => {
+ card.classList.remove("selected");
+ });
+ if (event.currentTarget && event.currentTarget.classList) {
+ event.currentTarget.classList.add("selected");
+ }
+ } catch (e) {}
+ }
+
+ resetTemplateSelections() {
+ this.selectedTemplateId = "";
+ }
+ // Page size change handler
+ handlePageSizeChange(event) {
+ const newPageSize = event.target.value;
+ this.selectedPageSize = newPageSize;
+
+ // Update the preview frame with new dimensions
+ this.updatePreviewFrameSize(newPageSize);
+
+ // Initialize viewport with exact PDF dimensions
+ this.initializeViewportDimensions();
+
+ // Re-fit to width when page size changes
+ setTimeout(() => this.fitToWidth(), 0);
+ }
+
+ // ===== Step 3 viewport zoom controls =====
+ get zoomPercent() {
+ try {
+ return `${Math.round((this.zoom || 1) * 100)}%`;
+ } catch (e) {
+ return "100%";
+ }
+ }
+
+ get pdfCanvasStyle() {
+ const scale = this.zoom || 1;
+ return `transform: scale(${scale}) !important; transform-origin: top center !important; margin: 0 auto;`;
+ }
+
+ zoomIn() {
+ this.zoom = Math.min((this.zoom || 1) + 0.1, 3);
+ }
+ zoomOut() {
+ this.zoom = Math.max((this.zoom || 1) - 0.1, 0.3);
+ }
+ resetZoom() {
+ this.zoom = 1;
+ }
+
+ // Handler methods for HTML onclick events
+ handleZoomIn() {
+ this.zoomIn();
+ }
+ handleZoomOut() {
+ this.zoomOut();
+ }
+ handleZoom100() {
+ this.resetZoom();
+ }
+ handleFitWidth() {
+ this.fitToWidth();
+ }
+ handleFitPage() {
+ this.fitToPage();
+ }
+
+ // Page management methods
+ addNewPage() {
+ const newPage = {
+ id: `page-${Date.now()}`,
+ content: "New page content...
",
+ };
+ this.previewPages = [...this.previewPages, newPage];
+ this.updatePageCount();
+ }
+
+ updatePageCount() {
+ const pageInfo = this.template?.querySelector(".page-info");
+ if (pageInfo) {
+ const pageCount = this.previewPages.length || 1;
+ pageInfo.innerHTML = `${
+ this.selectedPageSize
+ } - ${pageCount} page${pageCount > 1 ? "s" : ""}`;
+ }
+ }
+
+ // Split content into pages based on page breaks
+ splitContentIntoPages(content) {
+ if (!content) return [];
+
+ // Look for page break markers or split by content length
+ const pageBreakMarkers = [
+ '',
+ '',
+ ];
+ let pages = [];
+
+ // Check if content has explicit page breaks
+ let hasPageBreaks = false;
+ pageBreakMarkers.forEach((marker) => {
+ if (content.includes(marker)) {
+ hasPageBreaks = true;
+ }
+ });
+
+ if (hasPageBreaks) {
+ // Split by page breaks
+ const pageContent = content.split(/]*page-break[^>]*><\/div>/i);
+ pages = pageContent.map((pageContent, index) => ({
+ id: `page-${index + 1}`,
+ content: pageContent.trim() || "
Empty page
",
+ }));
+ } else {
+ // Single page for now - can be enhanced to auto-split based on content length
+ pages = [
+ {
+ id: "page-1",
+ content: content,
+ },
+ ];
+ }
+
+ return pages;
+ }
+
+ // Update preview pages when content changes
+ updatePreviewPages() {
+ if (this.htmlContent) {
+ this.previewPages = this.splitContentIntoPages(this.htmlContent);
+ } else {
+ this.previewPages = [];
+ }
+ this.updatePageCount();
+
+ // Force proper HTML rendering after content changes
+ setTimeout(() => {
+ this.forceHTMLRendering();
+ }, 100);
+ }
+
+ fitToWidth() {
+ const container = this.template?.querySelector(".pdf-viewport");
+ if (!container) {
+ return;
+ }
+ // Use exact PDF dimensions for perfect matching
+ const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794;
+ const baseHeight = this.selectedPageSize === "A3" ? 1587 : 1123;
+ const available = Math.max((container.clientWidth || baseWidth) - 20, 100);
+ this.zoom = Math.max(Math.min(available / baseWidth, 4), 0.2);
+
+ // Update canvas dimensions to match PDF exactly
+ const canvas = this.template?.querySelector(".pdf-canvas");
+ if (canvas) {
+ canvas.style.width = `${baseWidth}px`;
+ canvas.style.height = `${baseHeight}px`;
+ canvas.setAttribute('data-page-size', this.selectedPageSize);
+ }
+ }
+
+ fitToPage() {
+ const container = this.template?.querySelector(".pdf-viewport");
+ if (!container) {
+ return;
+ }
+ // Use exact PDF dimensions for perfect matching
+ const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794;
+ const baseHeight = this.selectedPageSize === "A3" ? 1587 : 1123;
+ const availableW = Math.max((container.clientWidth || baseWidth) - 20, 100);
+ const availableH = Math.max((container.clientHeight || baseHeight) - 20, 100);
+ const scaleW = availableW / baseWidth;
+ const scaleH = availableH / baseHeight;
+ this.zoom = Math.max(Math.min(Math.min(scaleW, scaleH), 4), 0.2);
+
+ // Update canvas dimensions to match PDF exactly
+ const canvas = this.template?.querySelector(".pdf-canvas");
+ if (canvas) {
+ canvas.style.width = `${baseWidth}px`;
+ canvas.style.height = `${baseHeight}px`;
+ canvas.setAttribute('data-page-size', this.selectedPageSize);
+ }
+ }
+ // Update preview frame size based on selected page size
+ updatePreviewFrameSize(pageSize) {
+ const previewFrame = this.template.querySelector(
+ ".enhanced-editor-content"
+ );
+ if (previewFrame) {
+ // Update the data attribute for the CSS content
+ previewFrame.setAttribute("data-page-size", pageSize);
+
+ // Set exact PDF dimensions for perfect matching
+ const baseWidth = pageSize === "A3" ? 1123 : 794;
+ const baseHeight = pageSize === "A3" ? 1587 : 1123;
+
+ // Update canvas dimensions
+ const canvas = this.template?.querySelector(".pdf-canvas");
+ if (canvas) {
+ canvas.style.width = `${baseWidth}px`;
+ canvas.style.height = `${baseHeight}px`;
+ canvas.setAttribute('data-page-size', pageSize);
+ }
+
+ // Re-render content with new page size
+ this.renderContentInPages(pageSize);
+
+ // Update page count based on new page size
+ this.updatePageCountForSize(pageSize);
+ }
+ }
+
+ // Render content in separate pages based on selected size
+ renderContentInPages(pageSize) {
+ const previewFrame = this.template.querySelector(
+ ".enhanced-editor-content"
+ );
+ if (!previewFrame) return;
+
+ // Get the current template content
+ const templateHTML = this.createTemplateHTML();
+ if (!templateHTML) return;
+
+ // Clear existing content
+ previewFrame.innerHTML = "";
+
+ // Split content into pages based on size
+ const pages = this.splitContentIntoPages(templateHTML, pageSize);
+
+ // Set exact PDF dimensions for perfect matching
+ const baseWidth = pageSize === "A3" ? 1123 : 794;
+ const baseHeight = pageSize === "A3" ? 1587 : 1123;
+
+ // Create page containers with exact PDF dimensions
+ pages.forEach((pageContent, index) => {
+ const pageContainer = document.createElement("div");
+ pageContainer.className = `preview-page page-size-${pageSize.toLowerCase()}`;
+ pageContainer.setAttribute("data-page-number", `Page ${index + 1}`);
+ pageContainer.style.width = `${baseWidth}px`;
+ pageContainer.style.minHeight = `${baseHeight}px`;
+ pageContainer.style.maxWidth = `${baseWidth}px`;
+ pageContainer.innerHTML = pageContent;
+ previewFrame.appendChild(pageContainer);
+ });
+
+ // Force proper HTML rendering after pages are created
+ setTimeout(() => {
+ this.forceHTMLRendering();
+ }, 50);
+ }
+ // Split HTML content into pages based on page size
+ splitContentIntoPages(htmlContent, pageSize) {
+ const pages = [];
+ let currentPage = "";
+ let currentHeight = 0;
+
+ // Define page heights in mm
+ const pageHeights = {
+ A4: 297,
+ A3: 420,
+ };
+
+ const maxHeight = pageHeights[pageSize] || 297;
+
+ // Create a temporary div to parse HTML
+ const tempDiv = document.createElement("div");
+ tempDiv.innerHTML = htmlContent;
+
+ // Get all direct children (brochure pages)
+ const children = Array.from(tempDiv.children);
+
+ children.forEach((child, index) => {
+ // If it's a brochure-page div, treat it as a separate page
+ if (child.classList.contains("brochure-page")) {
+ if (currentPage) {
+ pages.push(currentPage);
+ }
+ currentPage = child.outerHTML;
+ currentHeight = 0;
+ } else {
+ // For other content, check if it fits in current page
+ const estimatedHeight = this.estimateElementHeight(child, pageSize);
+
+ if (currentHeight + estimatedHeight > maxHeight && currentPage) {
+ // Start new page
+ pages.push(currentPage);
+ currentPage = child.outerHTML;
+ currentHeight = estimatedHeight;
+ } else {
+ // Add to current page
+ currentPage += child.outerHTML;
+ currentHeight += estimatedHeight;
+ }
+ }
+ });
+
+ // Add the last page
+ if (currentPage) {
+ pages.push(currentPage);
+ }
+
+ return pages;
+ }
+
+ // Estimate element height based on page size
+ estimateElementHeight(element, pageSize) {
+ // Base height estimation in mm
+ let baseHeight = 50; // Default height
+
+ // Adjust based on element type
+ if (element.tagName === "IMG") {
+ baseHeight = 100;
+ } else if (element.tagName === "H1") {
+ baseHeight = 30;
+ } else if (element.tagName === "H2") {
+ baseHeight = 25;
+ } else if (element.tagName === "P") {
+ baseHeight = 20;
+ } else if (element.tagName === "DIV") {
+ baseHeight = 80;
+ }
+
+ // Adjust for page size
+ if (pageSize === "A3") {
+ baseHeight *= 1.4; // A3 is larger
+ }
+
+ return baseHeight;
+ }
+
+ // Update page count based on selected page size
+ updatePageCountForSize(pageSize) {
+ const previewFrame = this.template.querySelector(
+ ".enhanced-editor-content"
+ );
+ if (previewFrame) {
+ let pageHeight = 297; // Default A4 height in mm
+
+ switch (pageSize) {
+ case "A3":
+ pageHeight = 420; // A3 height in mm
+ break;
+ case "A4":
+ default:
+ pageHeight = 297; // A4 height in mm
+ break;
+ }
+
+ // Calculate content height and estimate pages needed
+ const contentHeight = previewFrame.scrollHeight;
+ const contentHeightMm = contentHeight * 0.264583; // Convert px to mm
+ const pagesNeeded = Math.ceil(contentHeightMm / pageHeight);
+
+ this.pageCount = Math.max(1, Math.min(pagesNeeded, 20)); // Limit to 1-20 pages
+ }
+ }
+
+ // Navigation methods
+ nextStep() {
+ if (this.currentStep === 1 && !this.selectedTemplateId) {
+ this.showError("Please select a template first.");
+ return;
+ }
+ if (this.currentStep < 3) {
+ this.currentStep++;
+ // Reset click tracking when changing steps
+ this.resetImageClickTracking();
+ // If moving to step 3, automatically load the template
+ if (this.currentStep === 3) {
+ this.loadTemplateInStep3();
+ requestAnimationFrame(() => {
+ this.updatePreviewFrameSize(this.selectedPageSize || "A4");
+ });
+ }
+ this.scrollToTop();
+ }
+ }
+
+ previousStep() {
+ if (this.currentStep > 1) {
+ this.currentStep--;
+ // Reset click tracking when changing steps
+ this.resetImageClickTracking();
+ if (this.currentStep === 1) {
+ this.resetStep1Grid();
+ }
+ this.scrollToTop();
+ }
+ }
+
+ replaceStaticWithDynamic(content) {
+ if (!this.propertyData || !content) return content;
+
+ let result = content;
+
+ // Replace hardcoded values with actual property data
+ result = result.replace(
+ /Concorde Tower/g,
+ this.propertyData.propertyName || "Property Name"
+ );
+ result = result.replace(
+ /AED 81,999/g,
+ this.propertyData.rentPriceMin || this.propertyData.price || "Price"
+ );
+ result = result.replace(
+ /Modern Villa/g,
+ this.propertyData.propertyName || "Property Name"
+ );
+ result = result.replace(
+ /AED 2,500,000/g,
+ this.propertyData.salePriceMin || this.propertyData.price || "Price"
+ );
+ result = result.replace(/Dubai/g, this.propertyData.city || "Location");
+ result = result.replace(
+ /This beautiful property offers exceptional value and modern amenities\./g,
+ this.propertyData.descriptionEnglish || "Property description"
+ );
+
+ return result;
+ }
+ goToStep(event) {
+ const step = parseInt(event.currentTarget.dataset.step);
+ this.currentStep = step;
+ // Reset click tracking when changing steps
+ this.resetImageClickTracking();
+ if (this.currentStep === 1) {
+ this.resetStep1Grid();
+ // Also reconstruct grid HTML from original snapshot if available to fully reset content
+ if (
+ this.originalTemplateGridHTML &&
+ this.originalTemplateGridHTML.length > 50
+ ) {
+ const grid = this.template.querySelector("#all-templates");
+ if (grid) {
+ grid.innerHTML = this.originalTemplateGridHTML;
+ }
+ }
+ // Restore any styles that could have been mutated during step 3
+ this.restoreComponentStyles();
+ // Rebind click handlers for fresh grid without page reload
+ requestAnimationFrame(() => {
+ const cards = this.template.querySelectorAll(
+ "#all-templates .template-card"
+ );
+ cards.forEach((card) => {
+ card.onclick = this.handleTemplateSelect.bind(this);
+ // Restore selected state if this card matches the selected template
+ if (
+ this.selectedTemplateId &&
+ card.dataset.templateId === this.selectedTemplateId
+ ) {
+ card.classList.add("selected");
+ }
+ });
+ });
+ }
+ // If going directly to step 3, load the template only if not already loaded
+ if (
+ this.currentStep === 3 &&
+ (!this.htmlContent || this.htmlContent.trim() === "")
+ ) {
+ this.loadTemplateInStep3();
+ requestAnimationFrame(() => {
+ this.updatePreviewFrameSize(this.selectedPageSize || "A4");
+ // Auto fit width for better initial experience
+ this.fitToWidth && this.fitToWidth();
+ });
+ }
+ this.scrollToTop();
+ }
+
+ // Scroll to top of page when changing steps
+ scrollToTop() {
+ window.scrollTo({
+ top: 0,
+ behavior: "smooth",
+ });
+ }
+ // Load template content into step 3 enhanced editor
+ async loadTemplateInStep3() {
+ if (this.selectedTemplateId && this.selectedPropertyId) {
+ try {
+ // Use cached content if available to prevent regeneration
+ if (
+ this.cachedTemplateContent &&
+ this.cachedTemplateContent.templateId === this.selectedTemplateId &&
+ this.cachedTemplateContent.propertyId === this.selectedPropertyId
+ ) {
+ this.htmlContent = this.cachedTemplateContent.html;
+ this.updatePreviewPages();
+ this.updatePreviewFrameSize(this.selectedPageSize);
+ setTimeout(() => {
+ this.updatePageCountForSize(this.selectedPageSize);
+ this.updatePageCount();
+
+ // Set exact PDF dimensions for perfect matching
+ const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794;
+ const baseHeight = this.selectedPageSize === "A3" ? 1587 : 1123;
+
+ // Update canvas dimensions to match PDF exactly
+ const canvas = this.template?.querySelector(".pdf-canvas");
+ if (canvas) {
+ canvas.style.width = `${baseWidth}px`;
+ canvas.style.height = `${baseHeight}px`;
+ canvas.setAttribute('data-page-size', this.selectedPageSize);
+ }
+
+ this.fitToWidth && this.fitToWidth();
+ // Apply dynamic font sizing for cached content
+ this.applyDynamicFontSizing();
+ // Force image reload to ensure they display properly
+ this.forceImageReload();
+ // Force proper HTML rendering to match PDF exactly
+ this.forceHTMLRendering();
+ }, 100);
+ return;
+ }
+
+ // Ensure property images are loaded before creating template
+ if (this.realPropertyImages.length === 0) {
+ await this.loadPropertyImages();
+ }
+
+ const templateHTML = this.createTemplateHTML();
+
+ // Replace any hardcoded background-image URLs with property images
+ const processedTemplateHTML =
+ this.replaceBackgroundImagesInHTML(templateHTML);
+
+ // Cache the template content
+ this.cachedTemplateContent = {
+ templateId: this.selectedTemplateId,
+ propertyId: this.selectedPropertyId,
+ html: processedTemplateHTML,
+ };
+
+ // Set the HTML content for the template binding
+ this.htmlContent = processedTemplateHTML;
+
+ // Update preview pages with the new content
+ this.updatePreviewPages();
+
+ // Set initial page size class and data attribute
+ this.updatePreviewFrameSize(this.selectedPageSize);
+
+ // Update page count after template is loaded
+ setTimeout(() => {
+ this.updatePageCountForSize(this.selectedPageSize);
+ this.updatePageCount(); // Update page count display
+
+ // Set exact PDF dimensions for perfect matching
+ const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794;
+ const baseHeight = this.selectedPageSize === "A3" ? 1587 : 1123;
+
+ // Update canvas dimensions to match PDF exactly
+ const canvas = this.template?.querySelector(".pdf-canvas");
+ if (canvas) {
+ canvas.style.width = `${baseWidth}px`;
+ canvas.style.height = `${baseHeight}px`;
+ canvas.setAttribute('data-page-size', this.selectedPageSize);
+ }
+
+ // After content settles, fit viewport to width
+ this.fitToWidth && this.fitToWidth();
+ // Ensure CSS background images reflect current property images
+ this.updateCSSBackgroundImages();
+ // Apply dynamic font sizing after template loads
+ this.applyDynamicFontSizing();
+
+ // Force image reload to ensure they display properly
+ this.forceImageReload();
+ // Force proper HTML rendering to match PDF exactly
+ this.forceHTMLRendering();
+ }, 100);
+ } catch (error) {
+ this.error = "Error loading template: " + error.message;
+
+ // Show error message in preview frame
+ const previewFrame = this.template.querySelector(
+ ".enhanced-editor-content"
+ );
+ if (previewFrame) {
+ previewFrame.innerHTML = `
+
Error Loading Template
+
${error.message}
+
Please try selecting a different template or contact support.
+
`;
+ }
+ }
+ } else {
+ // Show a message if template or property is not selected
+ const previewFrame = this.template.querySelector(
+ ".enhanced-editor-content"
+ );
+ if (previewFrame) {
+ if (!this.selectedTemplateId) {
+ previewFrame.innerHTML =
+ '
No Template Selected
Please go back to step 1 and select a template.
';
+ } else if (!this.selectedPropertyId) {
+ previewFrame.innerHTML =
+ '
No Property Selected
Please go back to step 2 and select a property.
';
+ }
+ }
+ }
+ }
+
+ // Property selection handler
+ handlePropertySelection(event) {
+ // Handle both select dropdown and custom dropdown clicks
+ let propertyId;
+ if (event.target.tagName === 'SELECT') {
+ propertyId = event.target.value;
+ } else {
+ propertyId = event.currentTarget.dataset.value;
+ }
+
+ this.selectedPropertyId = propertyId;
+
+ // Clear cached content when selecting a new property
+ this.cachedTemplateContent = null;
+
+ if (this.selectedPropertyId) {
+ this.loadPropertyData();
+ // Auto-scroll to property preview section after a short delay to ensure data is loaded
+ setTimeout(() => {
+ this.scrollToPropertyPreview();
+ // Initialize image review with Interior category
+ // Auto-select category will be handled in loadPropertyImages
+ }, 300);
+ }
+ }
+
+ // Search input handler
+ handlePropertySearch(event) {
+ this.propertySearchTerm = event.target.value;
+ }
+
+ // Property type filter handler
+ handlePropertyTypeFilter(event) {
+ this.propertyFilterType = event.target.value;
+ }
+
+ // City filter handler
+ handleCityFilter(event) {
+ this.propertyFilterCity = event.target.value;
+ }
+
+ // Clear all filters
+ handleClearFilters() {
+ this.propertySearchTerm = "";
+ this.propertyFilterType = "all";
+ this.propertyFilterCity = "all";
+ }
+
+ // Scroll to property preview section
+ scrollToPropertyPreview() {
+ const propertyPreviewSection = this.template.querySelector(
+ ".property-details-layout"
+ );
+ if (propertyPreviewSection) {
+ // Get the element's position and add offset for better positioning
+ const elementTop = propertyPreviewSection.offsetTop;
+ const offset = 100; // 100px offset from the top
+
+ window.scrollTo({
+ top: elementTop - offset,
+ behavior: "smooth",
+ });
+ }
+ }
+ // Helper function to safely get property values with validation
+ getPropertyValue(property, fieldPath, defaultValue = "N/A") {
+ try {
+ if (!property) return defaultValue;
+
+ // Handle nested object paths like 'Contact__r.FirstName'
+ if (fieldPath.includes(".")) {
+ const parts = fieldPath.split(".");
+ let value = property;
+ for (const part of parts) {
+ if (value && typeof value === "object" && value[part] !== undefined) {
+ value = value[part];
+ } else {
+ return defaultValue;
+ }
+ }
+ return value || defaultValue;
+ }
+
+ // Handle direct field access
+ return property[fieldPath] || defaultValue;
+ } catch (error) {
+ return defaultValue;
+ }
+ }
+ // Load property data with comprehensive validation
+ async loadPropertyData() {
+ const selectedProperty = this.properties.find(
+ (prop) => prop.Id === this.selectedPropertyId
+ );
+ if (selectedProperty) {
+ // Set the selectedProperty for use in PDF generation
+ this.selectedProperty = selectedProperty;
+
+ // Helper function for safe property access
+ const get = (fieldPath, defaultValue = "N/A") =>
+ this.getPropertyValue(selectedProperty, fieldPath, defaultValue);
+
+ this.propertyData = {
+ // Basic Information
+ propertyName:
+ get("pcrm__Property_Name_Propertyfinder__c") !== "N/A"
+ ? get("pcrm__Property_Name_Propertyfinder__c")
+ : get("Name", "Property Name"),
+ propertyType: get("pcrm__Property_Type__c", "Property Type"),
+ status: get("pcrm__Status__c", "Available"),
+ referenceNumber: get("Name", "REF-001"),
+
+ // Location Details
+ location:
+ get("pcrm__City_Bayut_Dubizzle__c") !== "N/A"
+ ? get("pcrm__City_Bayut_Dubizzle__c")
+ : get("pcrm__City_Propertyfinder__c") !== "N/A"
+ ? get("pcrm__City_Propertyfinder__c")
+ : "Location",
+ city: get("pcrm__City_Propertyfinder__c", "City"),
+ community: get("pcrm__Community_Propertyfinder__c", "Community"),
+ subCommunity: get(
+ "pcrm__Sub_Community_Propertyfinder__c",
+ "Sub Community"
+ ),
+ locality: get("pcrm__Locality_Bayut_Dubizzle__c", "Locality"),
+ subLocality: get(
+ "pcrm__Sub_Locality_Bayut_Dubizzle__c",
+ "Sub Locality"
+ ),
+ tower: get("pcrm__Tower_Bayut_Dubizzle__c", "Tower"),
+ unitNumber: get("pcrm__Unit_Number__c", "Unit Number"),
+
+ // Additional Location Fields
+ cityBayut: get("pcrm__City_Bayut_Dubizzle__c"),
+ cityPropertyfinder: get("pcrm__City_Propertyfinder__c"),
+ communityBayut: get("pcrm__Community_Propertyfinder__c"),
+ subCommunityBayut: get("pcrm__Sub_Community_Propertyfinder__c"),
+ localityBayut: get("pcrm__Locality_Bayut_Dubizzle__c"),
+ subLocalityBayut: get("pcrm__Sub_Locality_Bayut_Dubizzle__c"),
+ towerBayut: get("pcrm__Tower_Bayut_Dubizzle__c"),
+
+ // Rent Availability
+ rentAvailableFrom: get("pcrm__Rent_Available_From__c"),
+ rentAvailableTo: get("pcrm__Rent_Available_To__c"),
+
+ // Contact Details
+ contactName: (() => {
+ const firstName = get("Contact__r.FirstName", "");
+ const lastName = get("Contact__r.LastName", "");
+ if (firstName !== "N/A" && lastName !== "N/A") {
+ return `${firstName} ${lastName}`.trim();
+ } else if (firstName !== "N/A") {
+ return firstName;
+ } else if (lastName !== "N/A") {
+ return lastName;
+ }
+ return "Contact Not Linked";
+ })(),
+ // Prefer property-level fields if populated; otherwise fall back to linked Contact
+ contactEmail: (() => {
+ const propEmail = get("Email__c");
+ if (propEmail && propEmail !== "N/A") return propEmail;
+ const contactEmail = get("Contact__r.Email");
+ return contactEmail && contactEmail !== "N/A" ? contactEmail : "N/A";
+ })(),
+ contactPhone: (() => {
+ const propPhone = get("Phone__c");
+ if (propPhone && propPhone !== "N/A") return propPhone;
+ const contactPhone = get("Contact__r.Phone");
+ return contactPhone && contactPhone !== "N/A" ? contactPhone : "N/A";
+ })(),
+
+ // Specifications
+ bedrooms: get("pcrm__Bedrooms__c"),
+ bathrooms: get("pcrm__Bathrooms__c"),
+ floor: get("pcrm__Floor__c"),
+ size: get("pcrm__Size__c"),
+ sizeUnit: "sq ft", // Default unit since field doesn't exist
+ buildYear: get("pcrm__Build_Year__c"),
+
+ // Parking & Amenities
+ parkingSpaces: get("pcrm__Parking_Spaces__c"),
+
+ // Furnishing & Details
+ furnished: get("pcrm__Furnished__c"),
+
+ // Pricing Information
+ rentPriceMin: (() => {
+ const value = get("pcrm__Rent_Price_min__c");
+ return value !== "N/A" && !isNaN(value)
+ ? `AED ${parseFloat(value).toLocaleString()}`
+ : "N/A";
+ })(),
+ rentPriceMax: (() => {
+ const value = get("pcrm__Rent_Price_max__c");
+ return value !== "N/A" && !isNaN(value)
+ ? `AED ${parseFloat(value).toLocaleString()}`
+ : "N/A";
+ })(),
+ salePriceMin: (() => {
+ const value = get("pcrm__Sale_Price_min__c");
+ return value !== "N/A" && !isNaN(value)
+ ? `AED ${parseFloat(value).toLocaleString()}`
+ : "N/A";
+ })(),
+ salePriceMax: (() => {
+ const value = get("pcrm__Sale_Price_max__c");
+ return value !== "N/A" && !isNaN(value)
+ ? `AED ${parseFloat(value).toLocaleString()}`
+ : "N/A";
+ })(),
+
+ // Description & Title
+ titleEnglish: get("pcrm__Title_English__c"),
+ descriptionEnglish: get(
+ "pcrm__Description_English__c",
+ "This beautiful property offers exceptional value and modern amenities."
+ ),
+
+ // Offering Type
+ offeringType: get("pcrm__Offering_Type__c"),
+
+ // Legacy fields for backward compatibility
+ price: (() => {
+ const value = get("pcrm__Rent_Price_min__c");
+ return value !== "N/A" && !isNaN(value)
+ ? `AED ${parseFloat(value).toLocaleString()}`
+ : "Price on Request";
+ })(),
+ area: (() => {
+ const value = get("pcrm__Size__c");
+ return value !== "N/A" && !isNaN(value) ? `${value} sq ft` : "N/A";
+ })(),
+ yearBuilt: get("pcrm__Build_Year__c"),
+ parking: (() => {
+ const value = get("pcrm__Parking_Spaces__c");
+ return value !== "N/A" && !isNaN(value)
+ ? `${value} Parking Space(s)`
+ : "N/A";
+ })(),
+ furnishing: get("pcrm__Furnished__c"),
+ };
+
+ // Load property images
+ await this.loadPropertyImages();
+ }
+ }
+ // Load property images from Image Genie
+ async loadPropertyImages() {
+ if (!this.selectedPropertyId) {
+ this.realPropertyImages = [];
+ this.initialCategorySelected = false; // Reset flag when no property selected
+ this.currentImage = null;
+ this.totalImages = 0;
+ this.currentImageIndex = 0;
+ return;
+ }
+
+ try {
+ const images = await getPropertyImages({
+ propertyId: this.selectedPropertyId,
+ });
+
+ // Transform the data to match expected format
+ this.realPropertyImages = images.map((img) => ({
+ id: img.id,
+ name: img.name,
+ title: img.name,
+ category: img.category,
+ url: img.url,
+ }));
+
+ if (this.realPropertyImages && this.realPropertyImages.length > 0) {
+ // Find the first category that has images
+ const firstAvailableCategory = this.findFirstAvailableCategory();
+ this.filterImagesByCategory(firstAvailableCategory);
+ this.selectedCategory = firstAvailableCategory;
+ this.initialCategorySelected = true;
+
+ // Update active button visually
+ setTimeout(() => {
+ const categoryButtons = this.template.querySelectorAll(
+ ".category-btn-step2"
+ );
+ categoryButtons.forEach((btn) => {
+ btn.classList.remove("active");
+ if (btn.dataset.category === firstAvailableCategory) {
+ btn.classList.add("active");
+ }
+ });
+ }, 100);
+ } else {
+ // No images found for this property
+ this.currentImage = null;
+ this.totalImages = 0;
+ this.currentImageIndex = 0;
+ this.selectedCategory = "None";
+ this.initialCategorySelected = false;
+ }
+ } catch (error) {
+ this.realPropertyImages = [];
+ this.currentImage = null;
+ this.totalImages = 0;
+ this.currentImageIndex = 0;
+ }
+ }
+
+ // Market analysis change handler
+ handleMarketAnalysisChange(event) {
+ const { name, checked } = event.target;
+ this.marketAnalysis[name] = checked;
+ }
+ // Generate template content
+ async generateTemplateContent() {
+ if (!this.selectedTemplateId || !this.selectedPropertyId) {
+ this.error = "Please select both a template and a property.";
+ return;
+ }
+
+ this.isLoading = true;
+ this.error = "";
+
+ try {
+ // Get the current HTML content from the editor
+ const editorFrame = this.template.querySelector(
+ ".enhanced-editor-content"
+ );
+ let htmlContent = "";
+
+ if (editorFrame && editorFrame.innerHTML) {
+ htmlContent = editorFrame.innerHTML;
+ // Check if there are draggable elements - if so, don't regenerate template
+ const tempDiv = document.createElement("div");
+ tempDiv.innerHTML = htmlContent;
+ const draggableElements = tempDiv.querySelectorAll(
+ ".draggable-element, .draggable-image-container, .draggable-table-container"
+ );
+ if (draggableElements.length === 0 && htmlContent.length < 100) {
+ // Only regenerate if truly empty and no draggable elements
+ htmlContent = this.createCompleteTemplateHTML();
+ }
+ } else {
+ // Fallback: generate template HTML if editor is empty
+ htmlContent = this.createCompleteTemplateHTML();
+ }
+
+ // Generate PDF using the template HTML
+ const result = await generatePropertyPDF({
+ propertyData: JSON.stringify(this.propertyData),
+ templateName: this.selectedTemplateId,
+ generatePDF: true,
+ htmlContent: htmlContent,
+ });
+
+ if (result.success) {
+ // Handle successful PDF generation
+ this.showSuccess("PDF generated successfully!");
+ // You can add logic here to display the PDF or provide download link
+ } else {
+ this.error =
+ result.message || result.error || "Failed to generate PDF.";
+ }
+ } catch (error) {
+ this.error =
+ "Error generating PDF: " +
+ (error.body?.message || error.message || "Unknown error");
+ } finally {
+ this.isLoading = false;
+ }
+ }
+
+ // Scroll to Generate PDF button at the top
+ scrollToGeneratePdf() {
+ try {
+ // Find the Generate PDF button at the top
+ const generatePdfButton = this.template.querySelector(".export-pdf-btn");
+ if (generatePdfButton) {
+ // Scroll to the button with smooth animation
+ generatePdfButton.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ });
+
+ // Add a subtle highlight effect
+ generatePdfButton.style.boxShadow = "0 0 20px rgba(102, 126, 234, 0.6)";
+ setTimeout(() => {
+ generatePdfButton.style.boxShadow = "";
+ }, 2000);
+ } else {
+ }
+ } catch (error) {}
+ }
+
+ // Simple PDF generation method for the export button - using the working approach from first prompt
+ async generatePdfSimple() {
+ if (!this.selectedTemplateId || !this.selectedPropertyId) {
+ this.error = "Please select both a template and a property.";
+ return;
+ }
+
+ try {
+ // Set loading state
+ this.isGeneratingPdf = true;
+
+ // Show progress
+ this.showProgress("Starting AI-powered PDF generation...");
+
+ // Get current editor content
+ const editorFrame = this.template.querySelector(
+ ".enhanced-editor-content"
+ );
+ if (!editorFrame) {
+ throw new Error("Editor frame not found");
+ }
+
+ this.editorContent = editorFrame.innerHTML;
+
+ // Generate PDF using external API
+ await this.generatePdfViaExternalApi();
+
+ // Success message is handled in generatePdfViaExternalApi
+ } catch (error) {
+ // Provide more user-friendly error messages
+ let errorMessage = "PDF generation failed. ";
+ if (error.message && error.message.includes("timeout")) {
+ errorMessage +=
+ "The service is taking longer than expected. Please try again.";
+ } else if (error.message && error.message.includes("unavailable")) {
+ errorMessage +=
+ "The service is temporarily unavailable. Please try again in a few minutes.";
+ } else if (error.message && error.message.includes("connection")) {
+ errorMessage += "Please check your internet connection and try again.";
+ } else {
+ errorMessage += error.message || "Please try again.";
+ }
+
+ this.showError(errorMessage);
+ } finally {
+ // Reset loading state
+ this.isGeneratingPdf = false;
+ }
+ }
+ // Clean HTML content for PDF generation by removing editor controls
+ async cleanHtmlForPdf(htmlContent) {
+ // Use string manipulation to preserve inline styles better
+ let cleanedHtml = htmlContent;
+
+ // Convert company logo URLs to base64 for PDF compatibility
+ cleanedHtml = await this.replaceCompanyLogoWithBase64(cleanedHtml);
+
+ // Remove preview-only UI elements using regex
+ cleanedHtml = cleanedHtml.replace(
+ /<[^>]*(?:data-preview-only="true"|class="[^"]*(?:floating-placeholder|placeholder-badge|placeholder-bubble)[^"]*")[^>]*>.*?<\/[^>]*>/gi,
+ ''
+ );
+
+ // Remove resize handles using regex
+ cleanedHtml = cleanedHtml.replace(
+ /
]*class="[^"]*resize-handle[^"]*"[^>]*>.*?<\/div>/gi,
+ ''
+ );
+
+ // Remove delete buttons using regex
+ cleanedHtml = cleanedHtml.replace(
+ /
-
+
+
+
+
+
+
-
+
Property Details Preview
@@ -545,6 +648,33 @@
+
+
+
+
Agent Information
+
+
+ Agent Name:
+ {agentData.name}
+
+
+ Agent Email:
+ {agentData.email}
+
+
+ Agent Phone:
+ {agentData.phone}
+
+
+ Agent Title:
+ {agentData.title}
+
+
+ Company:
+ {agentData.company}
+
+
+
@@ -576,28 +706,6 @@
-
-
-
Pricing Information
-
-
- Rent Price (Min):
- {propertyData.rentPriceMin}
-
-
- Rent Price (Max):
- {propertyData.rentPriceMax}
-
-
- Sale Price (Min):
- {propertyData.salePriceMin}
-
-
- Sale Price (Max):
- {propertyData.salePriceMax}
-
-
-
@@ -669,6 +777,17 @@
{propertyData.offeringType}
+
+
+
+
Private Amenities:
+
+
+ {amenity}
+
+
+
+
@@ -814,6 +933,7 @@
→
+
@@ -875,7 +995,7 @@
title="Insert Square Footage">
Sq Ft
-
Floor
@@ -1041,12 +1161,12 @@
- Bullet(*)
+ Bullet
- Number(1)
+ Number
@@ -1104,8 +1224,6 @@
-
-
@@ -1163,17 +1281,17 @@
-
-
-
+
+
{htmlContent}
@@ -1331,6 +1449,7 @@
Cancel
+ Insert Image
@@ -1429,4 +1548,59 @@
+
+
+
+
+
+
+
+
Choose Section Type:
+
+
+
📝
+
Text Section
+
Add a text-based section with title and content
+
+
+
🖼️
+
Image Gallery
+
Add an image gallery section
+
+
+
✨
+
Features List
+
Add a features and amenities section
+
+
+
📞
+
Contact Info
+
Add contact information section
+
+
+
+
+
+
Section Configuration:
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+ Add Section
+
+
+
+
\ No newline at end of file
diff --git a/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js
index 0aae682..070815c 100644
--- a/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js
+++ b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js
@@ -4,11 +4,21 @@ import getProperties from "@salesforce/apex/PropertyDataController.getProperties
import generatePDFFromHTML from "@salesforce/apex/PDFGenerationProxy.generatePDFFromHTML";
import generateCompressedPDF from "@salesforce/apex/PDFGenerationProxy.generateCompressedPDF";
import getPropertyImages from "@salesforce/apex/PropertyDataController.getPropertyImages";
+import getAgentData from "@salesforce/apex/PropertyDataController.getAgentData";
+import getListingData from "@salesforce/apex/PropertyDataController.getListingData";
+import logoUrlResource from '@salesforce/resourceUrl/PropertyLogo';
export default class PropertyTemplateSelector extends LightningElement {
@track currentStep = 1;
htmlContent = ""; // Remove @track to prevent reactive updates
+ // Getter for logo URL - using actual PropertyLogo.svg from static resources
+ get logoUrl() {
+ // Use the actual PropertyLogo.svg from static resources
+ console.log("Getting logo URL:", logoUrlResource);
+ return logoUrlResource;
+ }
+
// Lifecycle method - called when component is rendered
renderedCallback() {
// If we're on step 3 and have template/property selected, load the template
@@ -17,14 +27,41 @@ export default class PropertyTemplateSelector extends LightningElement {
this.selectedTemplateId &&
this.selectedPropertyId
) {
- this.loadTemplateInStep3();
+ // Ensure images are loaded before loading template
+ if (this.realPropertyImages.length === 0) {
+ console.log("RenderedCallback: Loading images before template creation...");
+ this.loadPropertyImages().then(() => {
+ this.loadTemplateInStep3();
+ });
+ } else {
+ this.loadTemplateInStep3();
+ }
+ }
+
+ // Initialize available fields when property data is loaded
+ if (this.currentStep === 2 && this.propertyData && this.availableFields.length === 0) {
+ this.initializeAvailableFields();
+ }
+
+ // Preserve element positions after rendering
+ if (this.currentStep === 3 && this.htmlContent) {
+ setTimeout(() => {
+ this.preserveElementPositions();
+ }, 100);
}
}
@track properties = [];
@track selectedPropertyId = "";
@track propertyData = {};
+ @track agentData = {};
+ @track listingData = {};
@track isLoading = false;
@track error = "";
+
+ // Search and filter properties
+ @track propertySearchTerm = "";
+ @track propertyFilterType = "all";
+ @track propertyFilterCity = "all";
@track marketAnalysis = {
includeMarketData: true,
includeROIAnalysis: true,
@@ -33,6 +70,18 @@ export default class PropertyTemplateSelector extends LightningElement {
includeGrowthProjection: true,
};
+ // Pricing selection properties
+ @track pricingSelection = {
+ includeRentPriceMin: true,
+ includeRentPriceMax: true,
+ includeSalePriceMin: true,
+ includeSalePriceMax: true,
+ };
+
+ // Field selection properties
+ @track selectedFields = {}; // Track which fields are selected for inclusion
+ @track availableFields = []; // Available fields for selection
+
// PDF generation properties
@track exportPdfButtonText = "📄 Generate PDF";
@track showPdfPreview = false;
@@ -43,8 +92,27 @@ export default class PropertyTemplateSelector extends LightningElement {
@track zoom = 1.0; // Step 3 viewport zoom
@track isGeneratingPdf = false; // Loading state for PDF generation
@track previewPages = []; // Array of pages for viewport display
+ @track showPrice = true; // Toggle for showing price vs "Price on Request"
cachedTemplateContent = null; // Cache template content to prevent regeneration
+ // New Section Modal Properties
+ @track showNewSectionModal = false;
+ @track selectedSectionType = '';
+ @track newSectionTitle = '';
+ @track newSectionContent = '';
+ @track insertSectionDisabled = true;
+
+ // Helper function to get page dimensions based on selected size
+ getPageDimensions() {
+ const isA3 = this.selectedPageSize === "A3";
+ return {
+ width: isA3 ? "297mm" : "210mm",
+ height: isA3 ? "420mm" : "297mm",
+ widthPx: isA3 ? 1123 : 794,
+ heightPx: isA3 ? 1587 : 1123
+ };
+ }
+
// Image review properties
@track showImageReview = false;
@track selectedCategory = "Interior"; // Will be updated when images load
@@ -55,6 +123,11 @@ export default class PropertyTemplateSelector extends LightningElement {
// Real property images from Salesforce
@track realPropertyImages = [];
@track propertyImages = [];
+
+ // AI Image Classification properties
+ @track currentImageClassification = null;
+ @track isClassifyingImage = false;
+ @track classificationError = "";
@track imagesByCategory = {
Interior: [
{
@@ -176,11 +249,13 @@ export default class PropertyTemplateSelector extends LightningElement {
@track replacementSelectedCategory = "Interior"; // Will be updated when images load
@track filteredReplacementImages = [];
@track uploadedImagePreview = null;
+ @track selectedReplacementImage = null; // Track selected image in popup
// Triple click detection for image replacement
@track imageClickCount = 0;
@track lastClickedImage = null;
@track clickTimeout = null;
+ @track isFileDialogOpen = false;
// Undo/Redo functionality
@track undoStack = [];
@@ -221,10 +296,7 @@ export default class PropertyTemplateSelector extends LightningElement {
@track selectedElement = null;
// z-index controls removed per request
- // Undo functionality
- @track undoStack = [];
- @track redoStack = [];
- maxUndoSteps = 50;
+ // Undo functionality - removed duplicate declaration
// Computed properties for image replacement tabs
get propertyImagesTabClass() {
@@ -233,12 +305,35 @@ export default class PropertyTemplateSelector extends LightningElement {
: "source-tab";
}
+ get localUploadTabClass() {
+ return this.replacementActiveTab === "upload"
+ ? "source-tab active"
+ : "source-tab";
+ }
+
+ get showPropertyImagesTab() {
+ return this.replacementActiveTab === "property";
+ }
+
+ get showLocalUploadTab() {
+ return this.replacementActiveTab === "upload";
+ }
+
+ // Tab selection methods for image replacement
+ selectPropertyImagesTab() {
+ this.replacementActiveTab = "property";
+ this.filterReplacementImages();
+ }
+
+ selectLocalUploadTab() {
+ this.replacementActiveTab = "upload";
+ }
+
// Unified gallery section used across templates
generateUnifiedGallerySectionHTML() {
const imagesHTML = this.generatePropertyGalleryHTML();
return `
-
Property Gallery
${imagesHTML}
@@ -353,6 +448,10 @@ export default class PropertyTemplateSelector extends LightningElement {
return this.selectedTemplateId === "coreshift-template";
}
+ get showSectionContentInput() {
+ return this.selectedSectionType === 'text' || this.selectedSectionType === 'features';
+ }
+
get isModernHomeTemplateSelected() {
return this.selectedTemplateId === "modern-home-template";
}
@@ -501,6 +600,80 @@ export default class PropertyTemplateSelector extends LightningElement {
return !this.selectedPropertyId;
}
+ // Getter for processed amenities list
+ get propertyAmenitiesList() {
+ const amenities = [];
+
+ // First, get private amenities if available
+ if (this.propertyData.privateAmenities && this.propertyData.privateAmenities !== 'N/A') {
+ const privateAmenities = this.mapAmenityCodes(this.propertyData.privateAmenities);
+ amenities.push(...privateAmenities);
+ }
+
+ // Add other amenity-related fields from property data
+ const amenityFields = [
+ { field: 'parkingSpaces', label: 'Parking Spaces' },
+ { field: 'furnished', label: 'Furnished' },
+ { field: 'offeringType', label: 'Offering Type' },
+ { field: 'heating', label: 'Heating' },
+ { field: 'cooling', label: 'Cooling' },
+ { field: 'roof', label: 'Roof Type' },
+ { field: 'exterior', label: 'Exterior' },
+ { field: 'foundation', label: 'Foundation' },
+ { field: 'utilities', label: 'Utilities' },
+ { field: 'zoning', label: 'Zoning' },
+ { field: 'hoa', label: 'HOA' },
+ { field: 'hoaFee', label: 'HOA Fee' },
+ { field: 'taxYear', label: 'Tax Year' },
+ { field: 'maintenanceFee', label: 'Maintenance Fee' },
+ { field: 'serviceCharge', label: 'Service Charge' }
+ ];
+
+ // Add amenities from various fields
+ amenityFields.forEach(({ field, label }) => {
+ if (this.propertyData[field] && this.propertyData[field] !== 'N/A' && this.propertyData[field] !== '') {
+ amenities.push(`${label}: ${this.propertyData[field]}`);
+ }
+ });
+
+ // Check for additional amenity fields that might contain lists
+ const listFields = [
+ 'amenities', 'features', 'facilities', 'amenitiesList',
+ 'propertyAmenities', 'Amenities__c', 'Features__c',
+ 'Facilities__c', 'Property_Amenities__c'
+ ];
+
+ listFields.forEach(field => {
+ if (this.propertyData[field] && this.propertyData[field] !== 'N/A' && this.propertyData[field] !== '') {
+ if (Array.isArray(this.propertyData[field])) {
+ amenities.push(...this.propertyData[field]);
+ } else if (typeof this.propertyData[field] === 'string') {
+ // Split by common delimiters
+ const amenityList = this.propertyData[field]
+ .split(/[,;|\n]/)
+ .map(a => a.trim())
+ .filter(a => a);
+ amenities.push(...amenityList);
+ }
+ }
+ });
+
+ // If no amenities found, add basic property information as amenities
+ if (amenities.length === 0) {
+ const basicInfo = [
+ this.propertyData.bedrooms ? `${this.propertyData.bedrooms} Bedrooms` : null,
+ this.propertyData.bathrooms ? `${this.propertyData.bathrooms} Bathrooms` : null,
+ this.propertyData.area ? `${this.propertyData.area} sq ft` : null,
+ this.propertyData.propertyType || null,
+ this.propertyData.status || null
+ ].filter(info => info);
+ amenities.push(...basicInfo);
+ }
+
+ // Remove duplicates and empty values
+ return [...new Set(amenities)].filter(amenity => amenity && amenity.trim() !== '');
+ }
+
// Wire method to get properties
@wire(getProperties)
wiredProperties({ error, data }) {
@@ -526,6 +699,60 @@ export default class PropertyTemplateSelector extends LightningElement {
}));
}
+ // Filtered properties based on search and filters
+ get filteredProperties() {
+ let filtered = this.properties || [];
+
+ // Apply search filter
+ if (this.propertySearchTerm) {
+ const searchTerm = this.propertySearchTerm.toLowerCase();
+ filtered = filtered.filter(property =>
+ (property.Name && property.Name.toLowerCase().includes(searchTerm)) ||
+ (property.pcrm__Title_English__c && property.pcrm__Title_English__c.toLowerCase().includes(searchTerm)) ||
+ (property.pcrm__Property_Type__c && property.pcrm__Property_Type__c.toLowerCase().includes(searchTerm)) ||
+ (property.pcrm__City_Bayut_Dubizzle__c && property.pcrm__City_Bayut_Dubizzle__c.toLowerCase().includes(searchTerm)) ||
+ (property.Address__c && property.Address__c.toLowerCase().includes(searchTerm))
+ );
+ }
+
+ // Apply property type filter
+ if (this.propertyFilterType !== "all") {
+ filtered = filtered.filter(property =>
+ property.pcrm__Property_Type__c === this.propertyFilterType
+ );
+ }
+
+ // Apply city filter
+ if (this.propertyFilterCity !== "all") {
+ filtered = filtered.filter(property =>
+ property.pcrm__City_Bayut_Dubizzle__c === this.propertyFilterCity
+ );
+ }
+
+ return filtered;
+ }
+
+ // Get unique property types for filter dropdown
+ get uniquePropertyTypes() {
+ const types = [...new Set((this.properties || []).map(p => p.pcrm__Property_Type__c).filter(Boolean))];
+ return types.sort();
+ }
+
+ // Get unique cities for filter dropdown
+ get uniqueCities() {
+ const cities = [...new Set((this.properties || []).map(p => p.pcrm__City_Bayut_Dubizzle__c).filter(Boolean))];
+ return cities.sort();
+ }
+
+ // Get filtered properties with selected flag
+ get filteredPropertiesWithSelected() {
+ const selectedId = this.selectedPropertyId || "";
+ return this.filteredProperties.map((p) => ({
+ ...p,
+ _selected: p.Id === selectedId,
+ }));
+ }
+
// Template selection handler
handleTemplateSelect(event) {
const templateId = event.currentTarget.dataset.templateId;
@@ -589,14 +816,317 @@ export default class PropertyTemplateSelector extends LightningElement {
resetTemplateSelections() {
this.selectedTemplateId = "";
}
+
+ // Preserve current image state before page size change
+ preserveCurrentImageState() {
+ const previewFrame = this.template.querySelector(".enhanced-editor-content");
+ if (!previewFrame) return null;
+
+ console.log("Preserving current image state...");
+
+ const imageState = {
+ draggableImages: [],
+ draggableElements: [],
+ draggableTables: [],
+ allImages: [], // Preserve ALL images including gallery and template images
+ backgroundImages: [] // Preserve background images
+ };
+
+ // Preserve ALL images in the template (including gallery and template images)
+ const allImages = previewFrame.querySelectorAll('img');
+ allImages.forEach((img, index) => {
+ const rect = img.getBoundingClientRect();
+ const parentRect = previewFrame.getBoundingClientRect();
+ const container = img.closest('.draggable-image-container, .draggable-element, .gallery-item, .brochure, .brochure-page');
+
+ imageState.allImages.push({
+ src: img.src,
+ alt: img.alt,
+ className: img.className,
+ style: img.getAttribute('style'),
+ x: rect.left - parentRect.left,
+ y: rect.top - parentRect.top,
+ width: rect.width,
+ height: rect.height,
+ zIndex: img.style.zIndex || 'auto',
+ containerClass: container ? container.className : '',
+ containerStyle: container ? container.getAttribute('style') : '',
+ isGallery: img.closest('.gallery-item') !== null,
+ isTemplate: img.closest('.brochure, .brochure-page') !== null
+ });
+ });
+
+ // Preserve background images
+ const allElements = previewFrame.querySelectorAll('*');
+ allElements.forEach((el, index) => {
+ const bgImage = window.getComputedStyle(el).backgroundImage;
+ if (bgImage && bgImage !== 'none') {
+ const rect = el.getBoundingClientRect();
+ const parentRect = previewFrame.getBoundingClientRect();
+ imageState.backgroundImages.push({
+ element: el.tagName,
+ className: el.className,
+ style: el.getAttribute('style'),
+ backgroundImage: bgImage,
+ x: rect.left - parentRect.left,
+ y: rect.top - parentRect.top,
+ width: rect.width,
+ height: rect.height,
+ zIndex: el.style.zIndex || 'auto'
+ });
+ }
+ });
+
+ // Preserve draggable images
+ const images = previewFrame.querySelectorAll('.draggable-image-container');
+ images.forEach(img => {
+ const rect = img.getBoundingClientRect();
+ const parentRect = previewFrame.getBoundingClientRect();
+ imageState.draggableImages.push({
+ src: img.querySelector('img')?.src,
+ style: img.getAttribute('style'),
+ x: rect.left - parentRect.left,
+ y: rect.top - parentRect.top,
+ width: rect.width,
+ height: rect.height,
+ zIndex: img.style.zIndex || '1000'
+ });
+ });
+
+ // Preserve draggable elements
+ const elements = previewFrame.querySelectorAll('.draggable-element');
+ elements.forEach(el => {
+ const rect = el.getBoundingClientRect();
+ const parentRect = previewFrame.getBoundingClientRect();
+ imageState.draggableElements.push({
+ content: el.innerHTML,
+ style: el.getAttribute('style'),
+ x: rect.left - parentRect.left,
+ y: rect.top - parentRect.top,
+ width: rect.width,
+ height: rect.height,
+ zIndex: el.style.zIndex || '1000'
+ });
+ });
+
+ // Preserve draggable tables
+ const tables = previewFrame.querySelectorAll('.draggable-table-container');
+ tables.forEach(table => {
+ const rect = table.getBoundingClientRect();
+ const parentRect = previewFrame.getBoundingClientRect();
+ imageState.draggableTables.push({
+ content: table.innerHTML,
+ style: table.getAttribute('style'),
+ x: rect.left - parentRect.left,
+ y: rect.top - parentRect.top,
+ width: rect.width,
+ height: rect.height,
+ zIndex: table.style.zIndex || '1000'
+ });
+ });
+
+ console.log(`Preserved state: ${imageState.allImages.length} all images, ${imageState.backgroundImages.length} background images, ${imageState.draggableImages.length} draggable images`);
+
+ return imageState;
+ }
+
+ // Restore image state after page size change
+ restoreCurrentImageState(imageState) {
+ if (!imageState) return;
+
+ setTimeout(() => {
+ const previewFrame = this.template.querySelector(".enhanced-editor-content");
+ if (!previewFrame) return;
+
+ console.log("Restoring image state for page size change...");
+
+ // Restore ALL images (including gallery and template images)
+ imageState.allImages.forEach((imgData, index) => {
+ const allImages = previewFrame.querySelectorAll('img');
+ if (allImages[index]) {
+ const img = allImages[index];
+
+ // Only restore if it's not a gallery image (gallery images should stay in their grid)
+ if (!imgData.isGallery) {
+ // Restore positioning for non-gallery images
+ if (imgData.containerClass) {
+ const container = img.closest(imgData.containerClass);
+ if (container) {
+ container.style.position = "absolute";
+ container.style.left = imgData.x + "px";
+ container.style.top = imgData.y + "px";
+ container.style.width = imgData.width + "px";
+ container.style.height = imgData.height + "px";
+ container.style.zIndex = imgData.zIndex;
+ container.style.boxSizing = "border-box";
+
+ console.log(`Restored container for image ${index}:`, {
+ left: imgData.x + "px",
+ top: imgData.y + "px",
+ width: imgData.width + "px",
+ height: imgData.height + "px"
+ });
+ }
+ } else {
+ // Direct image positioning
+ img.style.position = "absolute";
+ img.style.left = imgData.x + "px";
+ img.style.top = imgData.y + "px";
+ img.style.width = imgData.width + "px";
+ img.style.height = imgData.height + "px";
+ img.style.zIndex = imgData.zIndex;
+ img.style.boxSizing = "border-box";
+
+ console.log(`Restored direct image ${index}:`, {
+ left: imgData.x + "px",
+ top: imgData.y + "px",
+ width: imgData.width + "px",
+ height: imgData.height + "px"
+ });
+ }
+ } else {
+ console.log(`Skipping gallery image ${index} - keeping in grid`);
+ }
+ }
+ });
+
+ // Restore background images
+ imageState.backgroundImages.forEach((bgData, index) => {
+ const elements = previewFrame.querySelectorAll(bgData.element);
+ const matchingElement = Array.from(elements).find(el =>
+ el.className === bgData.className &&
+ window.getComputedStyle(el).backgroundImage === bgData.backgroundImage
+ );
+
+ if (matchingElement) {
+ matchingElement.style.position = "absolute";
+ matchingElement.style.left = bgData.x + "px";
+ matchingElement.style.top = bgData.y + "px";
+ matchingElement.style.width = bgData.width + "px";
+ matchingElement.style.height = bgData.height + "px";
+ matchingElement.style.zIndex = bgData.zIndex;
+ matchingElement.style.boxSizing = "border-box";
+
+ console.log(`Restored background element ${index}:`, {
+ left: bgData.x + "px",
+ top: bgData.y + "px",
+ width: bgData.width + "px",
+ height: bgData.height + "px"
+ });
+ }
+ });
+
+ // Restore draggable images with enhanced positioning
+ imageState.draggableImages.forEach((imgData, index) => {
+ const existingImg = previewFrame.querySelectorAll('.draggable-image-container')[index];
+ if (existingImg) {
+ // Preserve exact positioning
+ existingImg.style.position = "absolute";
+ existingImg.style.left = imgData.x + "px";
+ existingImg.style.top = imgData.y + "px";
+ existingImg.style.width = imgData.width + "px";
+ existingImg.style.height = imgData.height + "px";
+ existingImg.style.zIndex = imgData.zIndex || "1000";
+ existingImg.style.boxSizing = "border-box";
+
+ console.log(`Restored draggable image ${index}:`, {
+ left: imgData.x + "px",
+ top: imgData.y + "px",
+ width: imgData.width + "px",
+ height: imgData.height + "px"
+ });
+ }
+ });
+
+ // Restore draggable elements with enhanced positioning
+ imageState.draggableElements.forEach((elData, index) => {
+ const existingEl = previewFrame.querySelectorAll('.draggable-element')[index];
+ if (existingEl) {
+ // Preserve exact positioning
+ existingEl.style.position = "absolute";
+ existingEl.style.left = elData.x + "px";
+ existingEl.style.top = elData.y + "px";
+ existingEl.style.width = elData.width + "px";
+ existingEl.style.height = elData.height + "px";
+ existingEl.style.zIndex = elData.zIndex || "1000";
+ existingEl.style.boxSizing = "border-box";
+ existingEl.innerHTML = elData.content;
+
+ console.log(`Restored draggable element ${index}:`, {
+ left: elData.x + "px",
+ top: elData.y + "px",
+ width: elData.width + "px",
+ height: elData.height + "px"
+ });
+ }
+ });
+
+ // Restore draggable tables with enhanced positioning
+ imageState.draggableTables.forEach((tableData, index) => {
+ const existingTable = previewFrame.querySelectorAll('.draggable-table-container')[index];
+ if (existingTable) {
+ // Preserve exact positioning
+ existingTable.style.position = "absolute";
+ existingTable.style.left = tableData.x + "px";
+ existingTable.style.top = tableData.y + "px";
+ existingTable.style.width = tableData.width + "px";
+ existingTable.style.height = tableData.height + "px";
+ existingTable.style.zIndex = tableData.zIndex || "1000";
+ existingTable.style.boxSizing = "border-box";
+ existingTable.innerHTML = tableData.content;
+
+ console.log(`Restored draggable table ${index}:`, {
+ left: tableData.x + "px",
+ top: tableData.y + "px",
+ width: tableData.width + "px",
+ height: tableData.height + "px"
+ });
+ }
+ });
+
+ // Call position preservation to ensure all elements maintain their positions
+ this.preserveElementPositions();
+
+ // Re-initialize drag and drop functionality
+ this.initializeDragAndDrop();
+ }, 200);
+ }
// Page size change handler
handlePageSizeChange(event) {
const newPageSize = event.target.value;
+
+ console.log(`Page size changing from ${this.selectedPageSize} to ${newPageSize}`);
+
+ // Store current image state before changing page size
+ const currentImageState = this.preserveCurrentImageState();
+
this.selectedPageSize = newPageSize;
// Update the preview frame with new dimensions
this.updatePreviewFrameSize(newPageSize);
+ // Initialize viewport with exact PDF dimensions
+ this.initializeViewportDimensions();
+
+ // Special handling for A3 mode to prevent image reset
+ if (newPageSize === "A3") {
+ console.log("A3 mode selected - preserving all image positions");
+ // For A3, we need to be extra careful about position preservation
+ setTimeout(() => {
+ this.restoreCurrentImageState(currentImageState);
+ // Additional position preservation for A3
+ this.preserveElementPositions();
+ // Extra A3-specific position lock
+ setTimeout(() => {
+ this.preserveElementPositions();
+ console.log("A3 mode - final position preservation completed");
+ }, 100);
+ }, 300);
+ } else {
+ // Normal restoration for other page sizes
+ this.restoreCurrentImageState(currentImageState);
+ }
+
// Re-fit to width when page size changes
setTimeout(() => this.fitToWidth(), 0);
}
@@ -709,6 +1239,11 @@ export default class PropertyTemplateSelector extends LightningElement {
this.previewPages = [];
}
this.updatePageCount();
+
+ // Force proper HTML rendering after content changes
+ setTimeout(() => {
+ this.forceHTMLRendering();
+ }, 100);
}
fitToWidth() {
@@ -716,9 +1251,19 @@ export default class PropertyTemplateSelector extends LightningElement {
if (!container) {
return;
}
+ // Use exact PDF dimensions for perfect matching
const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794;
- const available = Math.max((container.clientWidth || baseWidth) - 32, 100);
- this.zoom = Math.max(Math.min(available / baseWidth, 4), 0.3);
+ const baseHeight = this.selectedPageSize === "A3" ? 1587 : 1123;
+ const available = Math.max((container.clientWidth || baseWidth) - 20, 100);
+ this.zoom = Math.max(Math.min(available / baseWidth, 4), 0.2);
+
+ // Update canvas dimensions to match PDF exactly
+ const canvas = this.template?.querySelector(".pdf-canvas");
+ if (canvas) {
+ canvas.style.width = `${baseWidth}px`;
+ canvas.style.height = `${baseHeight}px`;
+ canvas.setAttribute('data-page-size', this.selectedPageSize);
+ }
}
fitToPage() {
@@ -726,16 +1271,22 @@ export default class PropertyTemplateSelector extends LightningElement {
if (!container) {
return;
}
+ // Use exact PDF dimensions for perfect matching
const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794;
const baseHeight = this.selectedPageSize === "A3" ? 1587 : 1123;
- const availableW = Math.max((container.clientWidth || baseWidth) - 32, 100);
- const availableH = Math.max(
- (container.clientHeight || baseHeight) - 32,
- 100
- );
+ const availableW = Math.max((container.clientWidth || baseWidth) - 20, 100);
+ const availableH = Math.max((container.clientHeight || baseHeight) - 20, 100);
const scaleW = availableW / baseWidth;
const scaleH = availableH / baseHeight;
- this.zoom = Math.max(Math.min(Math.min(scaleW, scaleH), 4), 0.3);
+ this.zoom = Math.max(Math.min(Math.min(scaleW, scaleH), 4), 0.2);
+
+ // Update canvas dimensions to match PDF exactly
+ const canvas = this.template?.querySelector(".pdf-canvas");
+ if (canvas) {
+ canvas.style.width = `${baseWidth}px`;
+ canvas.style.height = `${baseHeight}px`;
+ canvas.setAttribute('data-page-size', this.selectedPageSize);
+ }
}
// Update preview frame size based on selected page size
updatePreviewFrameSize(pageSize) {
@@ -746,6 +1297,18 @@ export default class PropertyTemplateSelector extends LightningElement {
// Update the data attribute for the CSS content
previewFrame.setAttribute("data-page-size", pageSize);
+ // Set exact PDF dimensions for perfect matching
+ const baseWidth = pageSize === "A3" ? 1123 : 794;
+ const baseHeight = pageSize === "A3" ? 1587 : 1123;
+
+ // Update canvas dimensions
+ const canvas = this.template?.querySelector(".pdf-canvas");
+ if (canvas) {
+ canvas.style.width = `${baseWidth}px`;
+ canvas.style.height = `${baseHeight}px`;
+ canvas.setAttribute('data-page-size', pageSize);
+ }
+
// Re-render content with new page size
this.renderContentInPages(pageSize);
@@ -754,7 +1317,7 @@ export default class PropertyTemplateSelector extends LightningElement {
}
}
- // Render content in separate pages based on selected size
+ // Render content exactly as generated HTML - no modifications
renderContentInPages(pageSize) {
const previewFrame = this.template.querySelector(
".enhanced-editor-content"
@@ -768,17 +1331,13 @@ export default class PropertyTemplateSelector extends LightningElement {
// Clear existing content
previewFrame.innerHTML = "";
- // Split content into pages based on size
- const pages = this.splitContentIntoPages(templateHTML, pageSize);
-
- // Create page containers
- pages.forEach((pageContent, index) => {
- const pageContainer = document.createElement("div");
- pageContainer.className = `preview-page page-size-${pageSize.toLowerCase()}`;
- pageContainer.setAttribute("data-page-number", `Page ${index + 1}`);
- pageContainer.innerHTML = pageContent;
- previewFrame.appendChild(pageContainer);
- });
+ // Render the exact HTML without any wrapper containers or modifications
+ previewFrame.innerHTML = templateHTML;
+
+ // Force proper HTML rendering after content is set
+ setTimeout(() => {
+ this.forceHTMLRendering();
+ }, 50);
}
// Split HTML content into pages based on page size
splitContentIntoPages(htmlContent, pageSize) {
@@ -888,7 +1447,7 @@ export default class PropertyTemplateSelector extends LightningElement {
}
// Navigation methods
- nextStep() {
+ async nextStep() {
if (this.currentStep === 1 && !this.selectedTemplateId) {
this.showError("Please select a template first.");
return;
@@ -897,8 +1456,12 @@ export default class PropertyTemplateSelector extends LightningElement {
this.currentStep++;
// Reset click tracking when changing steps
this.resetImageClickTracking();
- // If moving to step 3, automatically load the template
+ // If moving to step 3, ensure images are loaded before loading template
if (this.currentStep === 3) {
+ // Ensure property images are loaded before creating template
+ if (this.realPropertyImages.length === 0) {
+ await this.loadPropertyImages();
+ }
this.loadTemplateInStep3();
requestAnimationFrame(() => {
this.updatePreviewFrameSize(this.selectedPageSize || "A4");
@@ -950,7 +1513,7 @@ export default class PropertyTemplateSelector extends LightningElement {
return result;
}
- goToStep(event) {
+ async goToStep(event) {
const step = parseInt(event.currentTarget.dataset.step);
this.currentStep = step;
// Reset click tracking when changing steps
@@ -986,11 +1549,15 @@ export default class PropertyTemplateSelector extends LightningElement {
});
});
}
- // If going directly to step 3, load the template only if not already loaded
+ // If going directly to step 3, ensure images are loaded before loading template
if (
this.currentStep === 3 &&
(!this.htmlContent || this.htmlContent.trim() === "")
) {
+ // Ensure property images are loaded before creating template
+ if (this.realPropertyImages.length === 0) {
+ await this.loadPropertyImages();
+ }
this.loadTemplateInStep3();
requestAnimationFrame(() => {
this.updatePreviewFrameSize(this.selectedPageSize || "A4");
@@ -1024,14 +1591,40 @@ export default class PropertyTemplateSelector extends LightningElement {
setTimeout(() => {
this.updatePageCountForSize(this.selectedPageSize);
this.updatePageCount();
+
+ // Set exact PDF dimensions for perfect matching
+ const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794;
+ const baseHeight = this.selectedPageSize === "A3" ? 1587 : 1123;
+
+ // Update canvas dimensions to match PDF exactly
+ const canvas = this.template?.querySelector(".pdf-canvas");
+ if (canvas) {
+ canvas.style.width = `${baseWidth}px`;
+ canvas.style.height = `${baseHeight}px`;
+ canvas.setAttribute('data-page-size', this.selectedPageSize);
+ }
+
this.fitToWidth && this.fitToWidth();
+ // Apply dynamic font sizing for cached content
+ this.applyDynamicFontSizing();
+ // Force image reload to ensure they display properly
+ this.forceImageReload();
+ // Force proper HTML rendering to match PDF exactly
+ this.forceHTMLRendering();
}, 100);
return;
}
// Ensure property images are loaded before creating template
if (this.realPropertyImages.length === 0) {
+ console.log("Loading property images before template creation...");
await this.loadPropertyImages();
+ // Double-check that images were loaded successfully
+ if (this.realPropertyImages.length === 0) {
+ console.warn("No property images found after loading attempt");
+ } else {
+ console.log(`Successfully loaded ${this.realPropertyImages.length} property images`);
+ }
}
const templateHTML = this.createTemplateHTML();
@@ -1060,10 +1653,30 @@ export default class PropertyTemplateSelector extends LightningElement {
setTimeout(() => {
this.updatePageCountForSize(this.selectedPageSize);
this.updatePageCount(); // Update page count display
+
+ // Set exact PDF dimensions for perfect matching
+ const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794;
+ const baseHeight = this.selectedPageSize === "A3" ? 1587 : 1123;
+
+ // Update canvas dimensions to match PDF exactly
+ const canvas = this.template?.querySelector(".pdf-canvas");
+ if (canvas) {
+ canvas.style.width = `${baseWidth}px`;
+ canvas.style.height = `${baseHeight}px`;
+ canvas.setAttribute('data-page-size', this.selectedPageSize);
+ }
+
// After content settles, fit viewport to width
this.fitToWidth && this.fitToWidth();
// Ensure CSS background images reflect current property images
this.updateCSSBackgroundImages();
+ // Apply dynamic font sizing after template loads
+ this.applyDynamicFontSizing();
+
+ // Force image reload to ensure they display properly
+ this.forceImageReload();
+ // Force proper HTML rendering to match PDF exactly
+ this.forceHTMLRendering();
}, 100);
} catch (error) {
this.error = "Error loading template: " + error.message;
@@ -1099,7 +1712,15 @@ export default class PropertyTemplateSelector extends LightningElement {
// Property selection handler
handlePropertySelection(event) {
- this.selectedPropertyId = event.target.value;
+ // Handle both select dropdown and custom dropdown clicks
+ let propertyId;
+ if (event.target.tagName === 'SELECT') {
+ propertyId = event.target.value;
+ } else {
+ propertyId = event.currentTarget.dataset.value;
+ }
+
+ this.selectedPropertyId = propertyId;
// Clear cached content when selecting a new property
this.cachedTemplateContent = null;
@@ -1115,6 +1736,28 @@ export default class PropertyTemplateSelector extends LightningElement {
}
}
+ // Search input handler
+ handlePropertySearch(event) {
+ this.propertySearchTerm = event.target.value;
+ }
+
+ // Property type filter handler
+ handlePropertyTypeFilter(event) {
+ this.propertyFilterType = event.target.value;
+ }
+
+ // City filter handler
+ handleCityFilter(event) {
+ this.propertyFilterCity = event.target.value;
+ }
+
+ // Clear all filters
+ handleClearFilters() {
+ this.propertySearchTerm = "";
+ this.propertyFilterType = "all";
+ this.propertyFilterCity = "all";
+ }
+
// Scroll to property preview section
scrollToPropertyPreview() {
const propertyPreviewSection = this.template.querySelector(
@@ -1250,6 +1893,7 @@ export default class PropertyTemplateSelector extends LightningElement {
// Parking & Amenities
parkingSpaces: get("pcrm__Parking_Spaces__c"),
+ privateAmenities: get("Private_Amenities__c"),
// Furnishing & Details
furnished: get("pcrm__Furnished__c"),
@@ -1313,8 +1957,137 @@ export default class PropertyTemplateSelector extends LightningElement {
// Load property images
await this.loadPropertyImages();
+
+ // Load agent data
+ await this.loadAgentData();
+
+ // Load listing data with agent information
+ await this.loadListingData();
}
}
+
+ // Load agent data from User object
+ async loadAgentData() {
+ try {
+ if (this.selectedPropertyId) {
+ console.log('Loading agent data for property:', this.selectedPropertyId);
+ const agentData = await getAgentData({
+ propertyId: this.selectedPropertyId
+ });
+
+ console.log('Agent data received:', agentData);
+
+ if (agentData) {
+ this.agentData = {
+ name: agentData.Name || (agentData.FirstName && agentData.LastName ? `${agentData.FirstName} ${agentData.LastName}` : 'N/A'),
+ email: agentData.Email || 'N/A',
+ phone: agentData.Phone || agentData.MobilePhone || 'N/A',
+ title: agentData.Title || 'Real Estate Agent',
+ department: agentData.Department || 'Sales',
+ company: agentData.CompanyName || 'Real Estate Company',
+ photo: agentData.SmallPhotoUrl || agentData.FullPhotoUrl || ''
+ };
+ console.log('Agent data set:', this.agentData);
+ } else {
+ // Fallback to contact data if no agent found
+ console.log('No agent data found, using contact fallback');
+ console.log('Available property data:', this.propertyData);
+ this.agentData = {
+ name: this.propertyData?.contactName || this.propertyData?.Name || 'Agent Name',
+ email: this.propertyData?.contactEmail || 'agent@company.com',
+ phone: this.propertyData?.contactPhone || '+971501234567',
+ title: 'Real Estate Agent',
+ department: 'Sales',
+ company: 'Real Estate Company',
+ photo: ''
+ };
+ console.log('Fallback agent data set:', this.agentData);
+ }
+ } else {
+ console.log('No property selected, using default agent data');
+ this.agentData = {
+ name: 'N/A',
+ email: 'N/A',
+ phone: 'N/A',
+ title: 'Real Estate Agent',
+ department: 'Sales',
+ company: 'Real Estate Company',
+ photo: ''
+ };
+ }
+ } catch (error) {
+ console.error('Error loading agent data:', error);
+ // Fallback to contact data on error
+ this.agentData = {
+ name: this.propertyData?.contactName || this.propertyData?.Name || 'Agent Name',
+ email: this.propertyData?.contactEmail || 'agent@company.com',
+ phone: this.propertyData?.contactPhone || '+971501234567',
+ title: 'Real Estate Agent',
+ department: 'Sales',
+ company: 'Real Estate Company',
+ photo: ''
+ };
+ }
+ }
+
+ // Load listing data with agent information
+ async loadListingData() {
+ try {
+ if (this.selectedPropertyId) {
+ console.log('Loading listing data for property:', this.selectedPropertyId);
+
+ const listingData = await getListingData({
+ propertyId: this.selectedPropertyId
+ });
+
+ console.log('Listing data received:', listingData);
+
+ if (listingData) {
+ this.listingData = {
+ id: listingData.Id,
+ name: listingData.Name,
+ propertyId: listingData.Property__r?.Id,
+ propertyName: listingData.Property__r?.Name,
+ listingAgent: listingData.Listing_Agent__r ? {
+ id: listingData.Listing_Agent__r.Id,
+ name: listingData.Listing_Agent__r.Name,
+ email: listingData.Listing_Agent__r.Email,
+ phone: listingData.Listing_Agent__r.Phone,
+ title: listingData.Listing_Agent__r.Title || 'Real Estate Agent'
+ } : null,
+ selectAgent: listingData.Select_Agent__r ? {
+ id: listingData.Select_Agent__r.Id,
+ name: listingData.Select_Agent__r.Name,
+ email: listingData.Select_Agent__r.Email,
+ phone: listingData.Select_Agent__r.Phone,
+ title: listingData.Select_Agent__r.Title || 'Real Estate Agent'
+ } : null
+ };
+
+ // Use listing agent data to override agent data if available
+ if (this.listingData.listingAgent) {
+ this.agentData = {
+ ...this.agentData,
+ name: this.listingData.listingAgent.name,
+ email: this.listingData.listingAgent.email,
+ phone: this.listingData.listingAgent.phone,
+ title: this.listingData.listingAgent.title
+ };
+ }
+
+ console.log('Listing data set:', this.listingData);
+ console.log('Agent data updated from listing:', this.agentData);
+ } else {
+ console.log('No listing data found');
+ }
+ } else {
+ console.log('No property selected, cannot load listing data');
+ }
+ } catch (error) {
+ console.error('Error loading listing data:', error);
+ }
+ }
+
// Load property images from Image Genie
async loadPropertyImages() {
if (!this.selectedPropertyId) {
@@ -1327,6 +2100,7 @@ export default class PropertyTemplateSelector extends LightningElement {
}
try {
+ console.log(`Loading property images for property ID: ${this.selectedPropertyId}`);
const images = await getPropertyImages({
propertyId: this.selectedPropertyId,
});
@@ -1340,6 +2114,8 @@ export default class PropertyTemplateSelector extends LightningElement {
url: img.url,
}));
+ console.log(`Successfully loaded ${this.realPropertyImages.length} images`);
+
if (this.realPropertyImages && this.realPropertyImages.length > 0) {
// Find the first category that has images
const firstAvailableCategory = this.findFirstAvailableCategory();
@@ -1347,7 +2123,7 @@ export default class PropertyTemplateSelector extends LightningElement {
this.selectedCategory = firstAvailableCategory;
this.initialCategorySelected = true;
- // Update active button visually
+ // Update active button visually and auto-classify first image
setTimeout(() => {
const categoryButtons = this.template.querySelectorAll(
".category-btn-step2"
@@ -1358,9 +2134,12 @@ export default class PropertyTemplateSelector extends LightningElement {
btn.classList.add("active");
}
});
+ // Auto-classify the first image immediately after images are loaded
+ this.autoClassifyCurrentImage();
}, 100);
} else {
// No images found for this property
+ console.warn("No images found for the selected property");
this.currentImage = null;
this.totalImages = 0;
this.currentImageIndex = 0;
@@ -1368,6 +2147,7 @@ export default class PropertyTemplateSelector extends LightningElement {
this.initialCategorySelected = false;
}
} catch (error) {
+ console.error("Error loading property images:", error);
this.realPropertyImages = [];
this.currentImage = null;
this.totalImages = 0;
@@ -1380,6 +2160,537 @@ export default class PropertyTemplateSelector extends LightningElement {
const { name, checked } = event.target;
this.marketAnalysis[name] = checked;
}
+
+ // Pricing selection change handler
+ handlePricingSelectionChange(event) {
+ const { name, checked } = event.target;
+ this.pricingSelection[name] = checked;
+
+ // Reinitialize available fields to reflect pricing selection changes
+ if (this.propertyData && Object.keys(this.propertyData).length > 0) {
+ this.initializeAvailableFields();
+ }
+ }
+
+ // Handle price toggle change (show actual price vs "Price on Request")
+ handlePriceToggleChange(event) {
+ this.showPrice = event.target.checked;
+ console.log("Price display toggle changed:", this.showPrice ? "Show actual price" : "Show 'Price on Request'");
+ }
+
+ // New Section Modal Handlers
+ openNewSectionDialog() {
+ this.showNewSectionModal = true;
+ this.selectedSectionType = '';
+ this.newSectionTitle = '';
+ this.newSectionContent = '';
+ this.insertSectionDisabled = true;
+ }
+
+ closeNewSectionModal() {
+ this.showNewSectionModal = false;
+ this.selectedSectionType = '';
+ this.newSectionTitle = '';
+ this.newSectionContent = '';
+ this.insertSectionDisabled = true;
+ }
+
+ selectSectionType(event) {
+ const sectionType = event.currentTarget.dataset.type;
+ this.selectedSectionType = sectionType;
+ this.insertSectionDisabled = !this.newSectionTitle.trim();
+
+ // Remove selected class from all options
+ const allOptions = this.template.querySelectorAll('.section-type-option');
+ allOptions.forEach(option => option.classList.remove('selected'));
+
+ // Add selected class to clicked option
+ event.currentTarget.classList.add('selected');
+
+ // Set default title based on section type
+ if (!this.newSectionTitle) {
+ switch(sectionType) {
+ case 'text':
+ this.newSectionTitle = 'New Text Section';
+ break;
+ case 'image':
+ this.newSectionTitle = 'Image Gallery';
+ break;
+ case 'features':
+ this.newSectionTitle = 'Features & Amenities';
+ break;
+ case 'contact':
+ this.newSectionTitle = 'Contact Information';
+ break;
+ }
+ }
+ }
+
+ handleSectionTitleChange(event) {
+ this.newSectionTitle = event.target.value;
+ this.insertSectionDisabled = !this.newSectionTitle.trim();
+ }
+
+ handleSectionContentChange(event) {
+ this.newSectionContent = event.target.value;
+ }
+
+ insertNewSection() {
+ if (!this.selectedSectionType || !this.newSectionTitle.trim()) {
+ return;
+ }
+
+ const editor = this.template.querySelector(".enhanced-editor-content");
+ if (!editor) {
+ console.error("Editor not found");
+ return;
+ }
+
+ // Debug: Check logoUrl availability
+ console.log("LogoUrl in insertNewSection:", this.logoUrl);
+ console.log("LogoUrl type:", typeof this.logoUrl);
+
+ // Store logoUrl in a local variable to avoid template string issues
+ const logoUrl = this.logoUrl;
+ console.log("Local logoUrl:", logoUrl);
+
+ // Generate the new section HTML based on type
+ const sectionHTML = this.generateSectionHTML(this.selectedSectionType, this.newSectionTitle, this.newSectionContent);
+
+ // Create a new page container
+ const newPageHTML = `
+
+ `;
+
+ // Insert the new section at the end of the editor
+ editor.insertAdjacentHTML('beforeend', newPageHTML);
+
+ // Update the page count
+ this.updatePageCount();
+
+ // Close the modal
+ this.closeNewSectionModal();
+
+ // Show success message
+ this.showSuccess(`New ${this.selectedSectionType} section added successfully!`);
+
+ console.log("New section inserted:", {
+ type: this.selectedSectionType,
+ title: this.newSectionTitle,
+ content: this.newSectionContent
+ });
+ }
+
+ generateSectionHTML(type, title, content) {
+ const data = this.propertyData || {};
+
+ switch(type) {
+ case 'text':
+ return `
+
+
+
+
${content || 'Add your content here...'}
+
+
+ `;
+
+ case 'image':
+ return `
+
+
+
+
+
+
+
Add your images here
+
+
+
+
Add your images here
+
+
+
+
+ `;
+
+ case 'features':
+ return `
+
+
+
+
+
+
+ Feature 1
+
+
+
+ Feature 2
+
+
+
+ Feature 3
+
+
+
+ Feature 4
+
+
+ ${content ? `
` : ''}
+
+
+ `;
+
+ case 'contact':
+ return `
+
+
+
+
+ `;
+
+ default:
+ return `
+
+
+
+
${content || 'Add your content here...'}
+
+
+ `;
+ }
+ }
+
+ // Auto-classify current image when it changes
+ async autoClassifyCurrentImage() {
+ if (!this.currentImage || !this.currentImage.url) {
+ this.currentImageClassification = null;
+ this.classificationError = "";
+ return;
+ }
+
+ this.isClassifyingImage = true;
+ this.classificationError = "";
+ this.currentImageClassification = null;
+
+ try {
+ // Simulate API call delay (replace with real API call)
+ await new Promise(resolve => setTimeout(resolve, 1500));
+
+ // Get mock classification based on image title or random
+ const mockClassification = this.getMockClassificationForImage();
+ this.currentImageClassification = mockClassification;
+
+ } catch (error) {
+ console.error('Error classifying image:', error);
+ this.classificationError = `Classification failed: ${error.message}`;
+ } finally {
+ this.isClassifyingImage = false;
+ }
+ }
+
+ // Generate pricing section HTML based on selected pricing fields
+ generatePricingSection() {
+ const selectedFields = [];
+
+ if (this.pricingSelection.includeRentPriceMin) {
+ selectedFields.push(`
Rent Price (Min): ${this.propertyData.rentPriceMin || "N/A"}
`);
+ }
+ if (this.pricingSelection.includeRentPriceMax) {
+ selectedFields.push(`
Rent Price (Max): ${this.propertyData.rentPriceMax || "N/A"}
`);
+ }
+ if (this.pricingSelection.includeSalePriceMin) {
+ selectedFields.push(`
Sale Price (Min): ${this.propertyData.salePriceMin || "N/A"}
`);
+ }
+ if (this.pricingSelection.includeSalePriceMax) {
+ selectedFields.push(`
Sale Price (Max): ${this.propertyData.salePriceMax || "N/A"}
`);
+ }
+
+ if (selectedFields.length === 0) {
+ return ''; // Don't show pricing section if no fields are selected
+ }
+
+ return `
+
+ Pricing
+
+ ${selectedFields.join('')}
+
+
+ `;
+ }
+
+ // Get mock classification based on current image
+ getMockClassificationForImage() {
+ const imageTitle = this.currentImage.title || "";
+ const imageUrl = this.currentImage.url || "";
+
+ // More accurate mock classifications based on actual image content
+ const mockClassifications = {
+ // Cityscape/Exterior views
+ cityscape: { class: "Exterior", confidence: 92.3 },
+ exterior: { class: "Exterior", confidence: 88.7 },
+ building: { class: "Exterior", confidence: 85.4 },
+
+ // Interior spaces
+ livingRoom: { class: "Living Room", confidence: 91.2 },
+ bedroom: { class: "Bedroom", confidence: 89.6 },
+ kitchen: { class: "Kitchen", confidence: 87.8 },
+ bathroom: { class: "Bathroom", confidence: 90.1 },
+
+ // Amenities
+ gym: { class: "Gym/Fitness Room", confidence: 86.9 },
+ pool: { class: "Swimming Pool", confidence: 93.5 },
+ parking: { class: "Parking", confidence: 88.2 },
+
+ // Common areas
+ lobby: { class: "Lobby/Reception", confidence: 84.7 },
+ balcony: { class: "Balcony/Terrace", confidence: 82.3 }
+ };
+
+ // Improved logic to assign classification based on image title and URL
+ const titleLower = imageTitle.toLowerCase();
+ const urlLower = imageUrl.toLowerCase();
+
+ // Check for specific keywords in title
+ if (titleLower.includes('bedroom') || titleLower.includes('bed') || titleLower.includes('master')) {
+ return mockClassifications.bedroom;
+ } else if (titleLower.includes('kitchen') || titleLower.includes('cook') || titleLower.includes('chef')) {
+ return mockClassifications.kitchen;
+ } else if (titleLower.includes('bathroom') || titleLower.includes('bath') || titleLower.includes('toilet')) {
+ return mockClassifications.bathroom;
+ } else if (titleLower.includes('living') || titleLower.includes('lounge') || titleLower.includes('sitting')) {
+ return mockClassifications.livingRoom;
+ } else if (titleLower.includes('exterior') || titleLower.includes('outside') || titleLower.includes('facade') || titleLower.includes('building')) {
+ return mockClassifications.exterior;
+ } else if (titleLower.includes('gym') || titleLower.includes('fitness') || titleLower.includes('workout')) {
+ return mockClassifications.gym;
+ } else if (titleLower.includes('pool') || titleLower.includes('swimming')) {
+ return mockClassifications.pool;
+ } else if (titleLower.includes('parking') || titleLower.includes('garage') || titleLower.includes('car')) {
+ return mockClassifications.parking;
+ } else if (titleLower.includes('lobby') || titleLower.includes('reception') || titleLower.includes('entrance')) {
+ return mockClassifications.lobby;
+ } else if (titleLower.includes('balcony') || titleLower.includes('terrace') || titleLower.includes('patio')) {
+ return mockClassifications.balcony;
+ } else {
+ // Check URL for clues
+ if (urlLower.includes('bedroom') || urlLower.includes('bed')) {
+ return mockClassifications.bedroom;
+ } else if (urlLower.includes('kitchen') || urlLower.includes('cook')) {
+ return mockClassifications.kitchen;
+ } else if (urlLower.includes('bathroom') || urlLower.includes('bath')) {
+ return mockClassifications.bathroom;
+ } else if (urlLower.includes('living') || urlLower.includes('lounge')) {
+ return mockClassifications.livingRoom;
+ } else if (urlLower.includes('exterior') || urlLower.includes('building') || urlLower.includes('city')) {
+ return mockClassifications.exterior;
+ } else {
+ // Default based on image index for consistency
+ const imageIndex = this.currentImageIndex || 0;
+ const defaultClassifications = [
+ mockClassifications.exterior, // First image - usually exterior
+ mockClassifications.livingRoom, // Second image - usually living room
+ mockClassifications.bedroom, // Third image - usually bedroom
+ mockClassifications.kitchen, // Fourth image - usually kitchen
+ mockClassifications.bathroom, // Fifth image - usually bathroom
+ mockClassifications.gym, // Sixth image - usually amenities
+ mockClassifications.pool, // Seventh image - usually pool
+ mockClassifications.parking, // Eighth image - usually parking
+ mockClassifications.lobby, // Ninth image - usually lobby
+ mockClassifications.balcony // Tenth image - usually balcony
+ ];
+
+ return defaultClassifications[imageIndex % defaultClassifications.length];
+ }
+ }
+ }
+
+
+ // Initialize available fields from property data
+ initializeAvailableFields() {
+ const data = this.propertyData || {};
+ const fields = [
+ // Basic Information
+ { key: 'propertyName', label: 'Property Name', category: 'Basic Information', value: data.Name || data.propertyName || data.pcrm__Title_English__c },
+ { key: 'location', label: 'Location/Address', category: 'Basic Information', value: data.Address__c || data.location },
+ { key: 'price', label: 'Price', category: 'Basic Information', value: data.Sale_Price_Min__c || data.Rent_Price_Min__c || data.Price__c || data.price },
+ { key: 'bedrooms', label: 'Bedrooms', category: 'Basic Information', value: data.Bedrooms__c || data.bedrooms },
+ { key: 'bathrooms', label: 'Bathrooms', category: 'Basic Information', value: data.Bathrooms__c || data.bathrooms },
+ { key: 'squareFeet', label: 'Square Feet', category: 'Basic Information', value: data.Square_Feet__c || data.squareFeet || data.area },
+ { key: 'status', label: 'Status', category: 'Basic Information', value: data.Status__c || data.status },
+ { key: 'propertyType', label: 'Property Type', category: 'Basic Information', value: data.Property_Type__c || data.propertyType },
+
+ // Property Details
+ { key: 'yearBuilt', label: 'Year Built', category: 'Specifications', value: data.Build_Year__c || data.yearBuilt },
+ { key: 'furnished', label: 'Furnished', category: 'Amenities & Features', value: data.Furnished__c || data.furnished },
+ { key: 'parkingSpaces', label: 'Parking Spaces', category: 'Amenities & Features', value: data.Parking_Spaces__c || data.parkingSpaces },
+ { key: 'offeringType', label: 'Offering Type', category: 'Amenities & Features', value: data.Offering_Type__c || data.offeringType },
+ { key: 'floor', label: 'Floor', category: 'Property Details', value: data.Floor__c || data.floor },
+ { key: 'lotSize', label: 'Lot Size', category: 'Property Details', value: data.Lot_Size__c || data.lotSize },
+ { key: 'maintenanceFee', label: 'Maintenance Fee', category: 'Property Details', value: data.Maintenance_Fee__c || data.maintenanceFee },
+ { key: 'serviceCharge', label: 'Service Charge', category: 'Property Details', value: data.Service_Charge__c || data.serviceCharge },
+
+ // Features & Amenities
+ { key: 'heating', label: 'Heating', category: 'Features & Amenities', value: data.Heating__c || data.heating },
+ { key: 'cooling', label: 'Cooling', category: 'Features & Amenities', value: data.Cooling__c || data.cooling },
+ { key: 'roof', label: 'Roof', category: 'Features & Amenities', value: data.Roof__c || data.roof },
+ { key: 'exterior', label: 'Exterior', category: 'Features & Amenities', value: data.Exterior__c || data.exterior },
+ { key: 'foundation', label: 'Foundation', category: 'Features & Amenities', value: data.Foundation__c || data.foundation },
+ { key: 'utilities', label: 'Utilities', category: 'Features & Amenities', value: data.Utilities__c || data.utilities },
+ { key: 'zoning', label: 'Zoning', category: 'Features & Amenities', value: data.Zoning__c || data.zoning },
+
+ // Financial Information
+ { key: 'hoa', label: 'HOA', category: 'Financial Information', value: data.HOA__c || data.hoa },
+ { key: 'hoaFee', label: 'HOA Fee', category: 'Financial Information', value: data.HOA_Fee__c || data.hoaFee },
+ { key: 'taxYear', label: 'Tax Year', category: 'Financial Information', value: data.Tax_Year__c || data.taxYear },
+ { key: 'taxAmount', label: 'Tax Amount', category: 'Financial Information', value: data.Tax_Amount__c || data.taxAmount },
+ { key: 'lastSold', label: 'Last Sold Date', category: 'Financial Information', value: data.Last_Sold__c || data.lastSold },
+ { key: 'lastSoldPrice', label: 'Last Sold Price', category: 'Financial Information', value: data.Last_Sold_Price__c || data.lastSoldPrice },
+
+ // Pricing Information - only include selected fields
+ ...(this.pricingSelection.includeRentPriceMin ? [{ key: 'rentPriceMin', label: 'Rent Price (Min)', category: 'Pricing Information', value: data.Rent_Price_Min__c || data.rentPriceMin }] : []),
+ ...(this.pricingSelection.includeRentPriceMax ? [{ key: 'rentPriceMax', label: 'Rent Price (Max)', category: 'Pricing Information', value: data.Rent_Price_Max__c || data.rentPriceMax }] : []),
+ ...(this.pricingSelection.includeSalePriceMin ? [{ key: 'salePriceMin', label: 'Sale Price (Min)', category: 'Pricing Information', value: data.Sale_Price_Min__c || data.salePriceMin }] : []),
+ ...(this.pricingSelection.includeSalePriceMax ? [{ key: 'salePriceMax', label: 'Sale Price (Max)', category: 'Pricing Information', value: data.Sale_Price_Max__c || data.salePriceMax }] : []),
+
+ // Location Information
+ { key: 'city', label: 'City', category: 'Location Details', value: data.City__c || data.city },
+ { key: 'community', label: 'Community', category: 'Location Details', value: data.Community__c || data.community },
+ { key: 'subCommunity', label: 'Sub Community', category: 'Location Details', value: data.Sub_Community__c || data.subCommunity },
+ { key: 'locality', label: 'Locality', category: 'Location Details', value: data.Locality__c || data.locality },
+ { key: 'tower', label: 'Tower', category: 'Location Details', value: data.Tower__c || data.tower },
+ { key: 'unitNumber', label: 'Unit Number', category: 'Location Details', value: data.Unit_Number__c || data.unitNumber },
+ { key: 'schools', label: 'Schools', category: 'Location Information', value: data.Schools__c || data.schools },
+ { key: 'shoppingCenters', label: 'Shopping Centers', category: 'Location Information', value: data.Shopping_Centers__c || data.shoppingCenters },
+ { key: 'airportDistance', label: 'Airport Distance', category: 'Location Information', value: data.Airport_Distance__c || data.airportDistance },
+ { key: 'nearbyLandmarks', label: 'Nearby Landmarks', category: 'Location Information', value: data.Nearby_Landmarks__c || data.nearbyLandmarks },
+ { key: 'transportation', label: 'Transportation', category: 'Location Information', value: data.Transportation__c || data.transportation },
+ { key: 'hospitals', label: 'Hospitals', category: 'Location Information', value: data.Hospitals__c || data.hospitals },
+ { key: 'beachDistance', label: 'Beach Distance', category: 'Location Information', value: data.Beach_Distance__c || data.beachDistance },
+ { key: 'metroDistance', label: 'Metro Distance', category: 'Location Information', value: data.Metro_Distance__c || data.metroDistance },
+
+ // Additional Information
+ { key: 'petFriendly', label: 'Pet Friendly', category: 'Additional Information', value: data.Pet_Friendly__c || data.petFriendly },
+ { key: 'smokingAllowed', label: 'Smoking Allowed', category: 'Additional Information', value: data.Smoking_Allowed__c || data.smokingAllowed },
+ { key: 'rentAvailableFrom', label: 'Available From', category: 'Availability', value: data.Rent_Available_From__c || data.rentAvailableFrom },
+ { key: 'rentAvailableTo', label: 'Available To', category: 'Availability', value: data.Rent_Available_To__c || data.rentAvailableTo },
+ { key: 'minimumContract', label: 'Minimum Contract', category: 'Additional Information', value: data.Minimum_Contract__c || data.minimumContract },
+ { key: 'securityDeposit', label: 'Security Deposit', category: 'Additional Information', value: data.Security_Deposit__c || data.securityDeposit },
+ { key: 'utilitiesIncluded', label: 'Utilities Included', category: 'Additional Information', value: data.Utilities_Included__c || data.utilitiesIncluded },
+ { key: 'internetIncluded', label: 'Internet Included', category: 'Additional Information', value: data.Internet_Included__c || data.internetIncluded },
+ { key: 'cableIncluded', label: 'Cable Included', category: 'Additional Information', value: data.Cable_Included__c || data.cableIncluded },
+
+ // Contact Information
+ { key: 'contactName', label: 'Contact Name', category: 'Contact Information', value: data.Contact_Name__c || data.contactName },
+ { key: 'contactPhone', label: 'Contact Phone', category: 'Contact Information', value: data.Contact_Phone__c || data.contactPhone },
+ { key: 'contactEmail', label: 'Contact Email', category: 'Contact Information', value: data.Contact_Email__c || data.contactEmail },
+ { key: 'agentName', label: 'Agent Name', category: 'Contact Information', value: data.Agent_Name__c || data.agentName },
+ { key: 'agentPhone', label: 'Agent Phone', category: 'Contact Information', value: data.Agent_Phone__c || data.agentPhone },
+ { key: 'agentEmail', label: 'Agent Email', category: 'Contact Information', value: data.Agent_Email__c || data.agentEmail },
+ { key: 'ownerName', label: 'Owner Name', category: 'Contact Information', value: data.Owner_Name__c || data.ownerName },
+ { key: 'ownerPhone', label: 'Owner Phone', category: 'Contact Information', value: data.Owner_Phone__c || data.ownerPhone },
+ { key: 'ownerEmail', label: 'Owner Email', category: 'Contact Information', value: data.Owner_Email__c || data.ownerEmail },
+
+ // Description
+ { key: 'description', label: 'Description', category: 'Description', value: data.Description_English__c || data.descriptionEnglish || data.description },
+ { key: 'titleEnglish', label: 'Property Title', category: 'Description', value: data.Title_English__c || data.titleEnglish || data.title },
+ { key: 'descriptionEnglish', label: 'Full Description', category: 'Description', value: data.Description_English__c || data.descriptionEnglish || data.description }
+ ].filter(field => {
+ // Only include fields that have data
+ return field.value && field.value !== 'N/A' && field.value !== '' && field.value !== null && field.value !== undefined;
+ });
+
+ this.availableFields = fields;
+
+ // Initialize all fields as selected by default
+ const selectedFields = {};
+ fields.forEach(field => {
+ selectedFields[field.key] = true;
+ });
+ this.selectedFields = selectedFields;
+ }
+
+ // Handle field selection toggle
+ handleFieldToggle(event) {
+ const fieldKey = event.target.dataset.field;
+ this.selectedFields = {
+ ...this.selectedFields,
+ [fieldKey]: event.target.checked
+ };
+ }
+
+ // Get fields grouped by category
+ get fieldsByCategory() {
+ const grouped = {};
+ if (this.availableFields && this.availableFields.length > 0) {
+ this.availableFields.forEach(field => {
+ if (!grouped[field.category]) {
+ grouped[field.category] = [];
+ }
+ grouped[field.category].push({
+ ...field,
+ selected: this.selectedFields[field.key] || false
+ });
+ });
+ }
+ return grouped;
+ }
+
+ // Get field categories as array for template iteration
+ get fieldCategories() {
+ const grouped = this.fieldsByCategory;
+ return Object.keys(grouped).map(categoryName => ({
+ key: categoryName.toLowerCase().replace(/\s+/g, '-'),
+ name: categoryName,
+ fields: grouped[categoryName]
+ }));
+ }
+
// Generate template content
async generateTemplateContent() {
if (!this.selectedTemplateId || !this.selectedPropertyId) {
@@ -1511,9 +2822,21 @@ export default class PropertyTemplateSelector extends LightningElement {
}
}
// Clean HTML content for PDF generation by removing editor controls
- cleanHtmlForPdf(htmlContent) {
+ async cleanHtmlForPdf(htmlContent) {
// Use string manipulation to preserve inline styles better
let cleanedHtml = htmlContent;
+
+ // Debug: Check for logo URLs before conversion
+ const logoUrl = this.logoUrl;
+ const logoMatches = (cleanedHtml.match(new RegExp(logoUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
+ console.log(`Found ${logoMatches} instances of logo URL in HTML before conversion`);
+
+ // Ensure logo is properly converted to base64 for PDF compatibility
+ cleanedHtml = await this.ensureLogoInPDF(cleanedHtml);
+
+ // Debug: Check for base64 logos after conversion
+ const base64Matches = (cleanedHtml.match(/data:image\/[^;]+;base64,/g) || []).length;
+ console.log(`Found ${base64Matches} base64 images in HTML after conversion`);
// Remove preview-only UI elements using regex
cleanedHtml = cleanedHtml.replace(
@@ -1556,11 +2879,16 @@ export default class PropertyTemplateSelector extends LightningElement {
const draggableElements = tempDiv.querySelectorAll(
".draggable-element, .draggable-image-container, .draggable-table-container"
);
- draggableElements.forEach((el) => {
+ draggableElements.forEach((el, index) => {
// Ensure absolute positioning is maintained and properly formatted
if (el.style.position === "absolute" || el.classList.contains('draggable-image-container') || el.classList.contains('draggable-element')) {
el.style.position = "absolute";
+ // Preserve existing position values if they exist
+ const currentLeft = el.style.left;
+ const currentTop = el.style.top;
+ const currentZIndex = el.style.zIndex;
+
// Ensure positioning values are properly set
if (!el.style.left) el.style.left = "0px";
if (!el.style.top) el.style.top = "0px";
@@ -1572,6 +2900,17 @@ export default class PropertyTemplateSelector extends LightningElement {
// Remove any borders that might interfere
el.style.border = "none";
el.style.outline = "none";
+
+ // Debug logging for position preservation
+ console.log(`Element ${index} position preserved:`, {
+ left: el.style.left,
+ top: el.style.top,
+ zIndex: el.style.zIndex,
+ position: el.style.position,
+ wasLeft: currentLeft,
+ wasTop: currentTop,
+ wasZIndex: currentZIndex
+ });
}
// Ensure images inside draggable containers maintain proper styling
@@ -1728,7 +3067,7 @@ export default class PropertyTemplateSelector extends LightningElement {
}
// Clean HTML content for PDF generation to preserve exact positioning
- htmlContent = this.cleanHtmlForPdf(htmlContent);
+ htmlContent = await this.cleanHtmlForPdf(htmlContent);
// Debug: Check if draggable elements are still present after cleaning
const tempDivAfter = document.createElement("div");
@@ -1738,6 +3077,9 @@ export default class PropertyTemplateSelector extends LightningElement {
);
console.log(`Found ${draggableElementsAfter.length} draggable elements after cleaning`);
+ // Final position verification and correction
+ this.verifyAndCorrectPositions(tempDivAfter);
+
// Log positioning information for debugging
draggableElementsAfter.forEach((el, index) => {
console.log(`Element ${index}:`, {
@@ -1886,11 +3228,15 @@ export default class PropertyTemplateSelector extends LightningElement {
}, 1000);
// Call the Apex method with the complete HTML and page size
+ // Use the selected page size for PDF generation
+ const pdfPageSize = this.selectedPageSize || "A4";
+ console.log(`Generating PDF in ${pdfPageSize} mode`);
+
// Set timeout to 2 minutes (120000ms) for API response
const pdfResult = await Promise.race([
generatePDFFromHTML({
htmlContent: htmlContent,
- pageSize: this.selectedPageSize,
+ pageSize: pdfPageSize,
}),
new Promise((_, reject) =>
setTimeout(
@@ -1968,6 +3314,225 @@ export default class PropertyTemplateSelector extends LightningElement {
const byteArray = new Uint8Array(byteNumbers);
return new Blob([byteArray], { type: mimeType });
}
+
+ // Method to convert image URL to base64 data URL for PDF compatibility
+ async convertImageToBase64(imageUrl) {
+ try {
+ console.log('Fetching image for base64 conversion:', imageUrl);
+ const response = await fetch(imageUrl);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const blob = await response.blob();
+
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ console.log(`Successfully converted image to base64 (${blob.type}, ${blob.size} bytes)`);
+ resolve(reader.result);
+ };
+ reader.onerror = (error) => {
+ console.warn('FileReader error:', error);
+ reject(error);
+ };
+ reader.readAsDataURL(blob);
+ });
+ } catch (error) {
+ console.warn('Failed to convert image to base64:', error);
+
+ // Return a more visible fallback image for debugging
+ const fallbackSvg = `data:image/svg+xml;base64,${btoa(`
+
+ `)}`;
+
+ console.log('Using enhanced fallback image for logo');
+ return fallbackSvg;
+ }
+ }
+
+ // Method to replace all company logo URLs with base64 data URLs
+ async replaceCompanyLogoWithBase64(htmlContent) {
+ const companyLogoUrl = this.logoUrl;
+ console.log('Converting logo to base64:', companyLogoUrl);
+
+ if (!companyLogoUrl) {
+ console.warn('No logo URL found, using fallback');
+ return this.createFallbackLogo(htmlContent);
+ }
+
+ try {
+ // For SVG files, try to fetch and convert to base64
+ if (companyLogoUrl && companyLogoUrl.includes('.svg')) {
+ console.log('Processing SVG logo');
+ const response = await fetch(companyLogoUrl);
+ if (response.ok) {
+ const svgText = await response.text();
+ const base64Svg = `data:image/svg+xml;base64,${btoa(svgText)}`;
+ console.log('SVG converted to base64 successfully');
+
+ // Replace all instances of the company logo URL with base64 data URL
+ const escapedUrl = companyLogoUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ const originalMatches = (htmlContent.match(new RegExp(escapedUrl, 'g')) || []).length;
+ const updatedHtml = htmlContent.replace(
+ new RegExp(escapedUrl, 'g'),
+ base64Svg
+ );
+ console.log(`Replaced ${originalMatches} logo instances with base64 SVG`);
+ return updatedHtml;
+ } else {
+ console.warn('Failed to fetch SVG logo, status:', response.status);
+ }
+ }
+
+ // Fallback to regular image conversion
+ console.log('Converting regular image to base64');
+ const base64Logo = await this.convertImageToBase64(companyLogoUrl);
+ console.log('Image converted to base64 successfully');
+
+ // Replace all instances of the company logo URL with base64 data URL
+ const escapedUrl = companyLogoUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ const originalMatches = (htmlContent.match(new RegExp(escapedUrl, 'g')) || []).length;
+ const updatedHtml = htmlContent.replace(
+ new RegExp(escapedUrl, 'g'),
+ base64Logo
+ );
+ console.log(`Replaced ${originalMatches} logo instances with base64 image`);
+ return updatedHtml;
+ } catch (error) {
+ console.warn('Failed to convert company logo to base64, using fallback:', error);
+ return this.createFallbackLogo(htmlContent);
+ }
+ }
+
+ // Enhanced method to ensure logo is always present in PDF
+ async ensureLogoInPDF(htmlContent) {
+ console.log('Ensuring logo is present in PDF...');
+
+ // First try to convert the actual logo
+ let updatedHtml = await this.replaceCompanyLogoWithBase64(htmlContent);
+
+ // Check if any logo instances still exist and are not base64
+ const logoImgPattern = /
![]()
]*src="(?!data:image)[^"]*"[^>]*alt="[^"]*[Ll]ogo[^"]*"[^>]*>/gi;
+ const logoMatches = updatedHtml.match(logoImgPattern);
+
+ if (logoMatches && logoMatches.length > 0) {
+ console.log(`Found ${logoMatches.length} non-base64 logo instances, applying fallback`);
+
+ // Create a more robust fallback logo
+ const fallbackSvg = `data:image/svg+xml;base64,${btoa(`
+
+ `)}`;
+
+ // Replace all logo instances with fallback
+ updatedHtml = updatedHtml.replace(logoImgPattern, (match) => {
+ return match.replace(/src="[^"]*"/, `src="${fallbackSvg}"`);
+ });
+
+ console.log('Applied fallback logo to all instances');
+ }
+
+ return updatedHtml;
+ }
+
+ // Create fallback logo for when logo conversion fails
+ createFallbackLogo(htmlContent) {
+ // Create a more detailed fallback SVG logo
+ const fallbackSvg = `data:image/svg+xml;base64,${btoa(`
+
+ `)}`;
+
+ console.log('Using fallback SVG logo');
+
+ // Replace all instances of logo URLs with fallback SVG
+ const logoPatterns = [
+ /src="[^"]*logo[^"]*"/gi,
+ /src="[^"]*PropertyLogo[^"]*"/gi,
+ /src="[^"]*\.svg[^"]*"/gi
+ ];
+
+ let updatedHtml = htmlContent;
+ let totalReplacements = 0;
+
+ logoPatterns.forEach(pattern => {
+ const matches = updatedHtml.match(pattern);
+ if (matches) {
+ updatedHtml = updatedHtml.replace(pattern, `src="${fallbackSvg}"`);
+ totalReplacements += matches.length;
+ }
+ });
+
+ console.log(`Replaced ${totalReplacements} logo instances with fallback SVG`);
+ return updatedHtml;
+ }
+
+ // Verify and correct positions of draggable elements before PDF generation
+ verifyAndCorrectPositions(container) {
+ const draggableElements = container.querySelectorAll(
+ ".draggable-element, .draggable-image-container, .draggable-table-container"
+ );
+
+ console.log(`Verifying positions for ${draggableElements.length} draggable elements`);
+
+ draggableElements.forEach((el, index) => {
+ // Ensure absolute positioning
+ el.style.position = "absolute";
+ el.style.boxSizing = "border-box";
+
+ // Get data attributes if they exist (from drag operations)
+ const dataLeft = el.getAttribute('data-final-left');
+ const dataTop = el.getAttribute('data-final-top');
+
+ // Use data attributes if available, otherwise use current style values
+ if (dataLeft !== null) {
+ el.style.left = dataLeft + "px";
+ } else if (!el.style.left || el.style.left === "0px") {
+ el.style.left = "0px";
+ }
+
+ if (dataTop !== null) {
+ el.style.top = dataTop + "px";
+ } else if (!el.style.top || el.style.top === "0px") {
+ el.style.top = "0px";
+ }
+
+ // Ensure z-index is set
+ if (!el.style.zIndex) {
+ el.style.zIndex = "1000";
+ }
+
+ // Round position values to ensure pixel precision
+ const leftValue = Math.round(parseFloat(el.style.left) || 0);
+ const topValue = Math.round(parseFloat(el.style.top) || 0);
+
+ el.style.left = leftValue + "px";
+ el.style.top = topValue + "px";
+
+ console.log(`Element ${index} position verified:`, {
+ left: el.style.left,
+ top: el.style.top,
+ zIndex: el.style.zIndex,
+ dataLeft: dataLeft,
+ dataTop: dataTop
+ });
+ });
+ }
+
// Create complete template HTML with property data
createCompleteTemplateHTML() {
try {
@@ -2015,6 +3580,107 @@ export default class PropertyTemplateSelector extends LightningElement {
display: block;
page-break-inside: avoid;
}
+ /* Universal Footer Image Styling */
+ footer img, .page-footer img, .p1-footer img, .agent-footer img, .brochure-page img, .gallery-page img {
+ height: 14px !important;
+ width: auto !important;
+ max-width: 48px !important;
+ display: block !important;
+ margin: 0 auto !important;
+ object-fit: contain !important;
+ filter: brightness(1.1) !important;
+ transition: opacity 0.3s ease !important;
+ opacity: 0.9 !important;
+ }
+ /* Hover effect for interactive elements */
+ footer img:hover, .page-footer img:hover, .p1-footer img:hover, .agent-footer img:hover, .brochure-page img:hover, .gallery-page img:hover {
+ opacity: 1 !important;
+ transform: scale(1.02) !important;
+ }
+ /* Responsive footer image sizing */
+ @media (max-width: 768px) {
+ footer img, .page-footer img, .p1-footer img, .agent-footer img, .brochure-page img, .gallery-page img {
+ height: 16px !important;
+ max-width: 48px !important;
+ }
+ }
+ @media (max-width: 480px) {
+ footer img, .page-footer img, .p1-footer img, .agent-footer img, .brochure-page img, .gallery-page img {
+ height: 14px !important;
+ max-width: 40px !important;
+ }
+ }
+ /* Print/PDF specific styling */
+ @media print {
+ footer img, .page-footer img, .p1-footer img, .agent-footer img, .brochure-page img, .gallery-page img {
+ height: 16px !important;
+ max-width: 48px !important;
+ filter: none !important;
+ opacity: 1 !important;
+ object-fit: contain !important;
+ }
+ }
+
+ /* A4 specific logo sizing */
+ @media print and (max-width: 210mm) {
+ footer img, .page-footer img, .p1-footer img, .agent-footer img, .brochure-page img, .gallery-page img {
+ height: 14px !important;
+ max-width: 40px !important;
+ object-fit: contain !important;
+ }
+ }
+
+ /* A3 specific logo sizing */
+ @media print and (min-width: 211mm) {
+ footer img, .page-footer img, .p1-footer img, .agent-footer img, .brochure-page img, .gallery-page img {
+ height: 20px !important;
+ max-width: 64px !important;
+ object-fit: contain !important;
+ display: block !important;
+ margin: 0 auto !important;
+ }
+ }
+
+ /* SVG specific styling for better rendering */
+ footer img[src*=".svg"],
+ .page-footer img[src*=".svg"],
+ .p1-footer img[src*=".svg"],
+ .agent-footer img[src*=".svg"],
+ .brochure-page img[src*=".svg"],
+ .gallery-page img[src*=".svg"] {
+ height: 14px !important;
+ width: auto !important;
+ max-width: 48px !important;
+ object-fit: contain !important;
+ display: block !important;
+ margin: 0 auto !important;
+ filter: none !important;
+ opacity: 1 !important;
+ }
+
+ /* Base64 SVG styling */
+ footer img[src*="data:image/svg"],
+ .page-footer img[src*="data:image/svg"],
+ .p1-footer img[src*="data:image/svg"],
+ .agent-footer img[src*="data:image/svg"],
+ .brochure-page img[src*="data:image/svg"],
+ .gallery-page img[src*="data:image/svg"],
+ footer img[src*="data:image"],
+ .page-footer img[src*="data:image"],
+ .p1-footer img[src*="data:image"],
+ .agent-footer img[src*="data:image"],
+ .brochure-page img[src*="data:image"],
+ .gallery-page img[src*="data:image"] {
+ height: 14px !important;
+ width: auto !important;
+ max-width: 48px !important;
+ object-fit: contain !important;
+ display: block !important;
+ margin: 0 auto !important;
+ visibility: visible !important;
+ opacity: 1 !important;
+ }
+ }
table {
page-break-inside: avoid;
width: 100%;
@@ -2152,8 +3818,7 @@ export default class PropertyTemplateSelector extends LightningElement {
Square Feet:
${
this.selectedProperty.Square_Feet__c
- ? this.selectedProperty.Square_Feet__c.toLocaleString() +
- " sq ft"
+ ? this.selectedProperty.Square_Feet__c.toLocaleString()
: "N/A"
}
@@ -2161,9 +3826,9 @@ export default class PropertyTemplateSelector extends LightningElement {
-
-
Description
-
+
+
Description
+
${
this.selectedProperty.Description__c ||
"This beautiful property offers exceptional value and is located in a prime area. Contact us for more details and to schedule a viewing."
@@ -2574,10 +4239,174 @@ export default class PropertyTemplateSelector extends LightningElement {
return htmlParts.join("");
}
+ // Map amenity codes to full text descriptions
+ mapAmenityCodes(amenityCodes) {
+ const amenityMap = {
+ 'BA': 'Balcony',
+ 'BR': 'Barbecue Area',
+ 'BK': 'Built in Kitchen Appliances',
+ 'BW': 'Built in Wardrobes',
+ 'CO': 'Children\'s Pool',
+ 'CS': 'Concierge Service',
+ 'CP': 'Covered Parking',
+ 'GY': 'Gym/Fitness Center',
+ 'PO': 'Swimming Pool',
+ 'GA': 'Garden',
+ 'TE': 'Tennis Court',
+ 'SQ': 'Squash Court',
+ 'PL': 'Playground',
+ 'LI': 'Library',
+ 'CA': 'Cafeteria',
+ 'MA': 'Maintenance Service',
+ 'SE': 'Security Service',
+ 'EL': 'Elevator',
+ 'AC': 'Air Conditioning',
+ 'HE': 'Heating',
+ 'WA': 'Washing Machine',
+ 'DR': 'Dryer',
+ 'DI': 'Dishwasher',
+ 'RE': 'Refrigerator',
+ 'ST': 'Storage',
+ 'RO': 'Roof Access',
+ 'PA': 'Pet Allowed',
+ 'SM': 'Smoking Allowed',
+ 'WI': 'WiFi',
+ 'CA': 'Cable TV',
+ 'SA': 'Satellite TV',
+ // Additional common amenities
+ 'SA': 'Sauna',
+ 'JA': 'Jacuzzi',
+ 'SP': 'Spa',
+ 'BI': 'Bike Storage',
+ 'PE': 'Pet Friendly',
+ 'NO': 'Non-Smoking',
+ 'FU': 'Furnished',
+ 'UN': 'Unfurnished',
+ 'SE': 'Semi-Furnished',
+ 'WO': 'Wooden Floors',
+ 'TI': 'Tiled Floors',
+ 'CA': 'Carpeted',
+ 'MA': 'Marble Floors',
+ 'GR': 'Granite Countertops',
+ 'QU': 'Quartz Countertops',
+ 'ST': 'Stainless Steel Appliances',
+ 'GL': 'Glass Windows',
+ 'DO': 'Double Glazed Windows',
+ 'BL': 'Blinds',
+ 'CU': 'Curtains',
+ 'SH': 'Shutters',
+ 'CE': 'Central Air',
+ 'SP': 'Split AC',
+ 'WI': 'Window AC',
+ 'FA': 'Fans',
+ 'LI': 'Lighting',
+ 'CH': 'Chandelier',
+ 'SP': 'Spotlights',
+ 'RE': 'Recessed Lighting',
+ 'DI': 'Dimming Lights',
+ 'SM': 'Smart Home',
+ 'AU': 'Automation',
+ 'VO': 'Voice Control',
+ 'AP': 'App Control',
+ 'SE': 'Sensor Lights',
+ 'MO': 'Motion Sensors',
+ 'CA': 'Carbon Monoxide Detector',
+ 'SM': 'Smoke Detector',
+ 'FI': 'Fire Extinguisher',
+ 'ES': 'Emergency Exit',
+ 'ST': 'Staircase',
+ 'RA': 'Ramp Access',
+ 'WH': 'Wheelchair Accessible',
+ 'EL': 'Electricity',
+ 'WA': 'Water',
+ 'GA': 'Gas',
+ 'SE': 'Sewage',
+ 'IN': 'Internet',
+ 'PH': 'Phone Line',
+ 'VE': 'Ventilation',
+ 'IN': 'Insulation',
+ 'SO': 'Soundproofing',
+ 'FI': 'Fire Safety',
+ 'CA': 'CCTV',
+ 'AL': 'Alarm',
+ 'IN': 'Intercom',
+ 'DO': 'Doorman',
+ 'PO': 'Porter',
+ 'SE': 'Security Guard',
+ 'PA': 'Patrol',
+ 'GA': 'Gated Community',
+ 'FE': 'Fenced',
+ 'WA': 'Wall',
+ 'GA': 'Gate',
+ 'IN': 'Intercom Gate',
+ 'RE': 'Remote Control',
+ 'CA': 'Card Access',
+ 'KE': 'Key Access',
+ 'CO': 'Code Access',
+ 'BI': 'Biometric Access',
+ 'FA': 'Facial Recognition',
+ 'FI': 'Fingerprint Access',
+ 'IR': 'Iris Recognition',
+ 'VO': 'Voice Recognition',
+ 'AP': 'App Access',
+ 'SM': 'Smart Lock',
+ 'DI': 'Digital Lock',
+ 'EL': 'Electronic Lock',
+ 'MA': 'Magnetic Lock',
+ 'RE': 'Retina Scanner',
+ 'PA': 'Palm Scanner',
+ 'HA': 'Hand Scanner',
+ 'VE': 'Vein Scanner',
+ 'RE': 'Retinal Scanner',
+ 'IR': 'Iris Scanner',
+ 'FA': 'Face Scanner',
+ 'FI': 'Fingerprint Scanner',
+ 'PA': 'Palm Scanner',
+ 'HA': 'Hand Scanner',
+ 'VE': 'Vein Scanner',
+ 'RE': 'Retinal Scanner',
+ 'IR': 'Iris Scanner',
+ 'FA': 'Face Scanner',
+ 'FI': 'Fingerprint Scanner',
+ 'PA': 'Palm Scanner',
+ 'HA': 'Hand Scanner',
+ 'VE': 'Vein Scanner',
+ 'BU': 'Bus Stop Nearby',
+ 'ME': 'Metro Nearby',
+ 'SC': 'Shopping Center Nearby',
+ 'HO': 'Hospital Nearby',
+ 'SC': 'School Nearby',
+ 'RE': 'Restaurant Nearby',
+ 'EN': 'Entertainment Nearby',
+ 'BE': 'Beach Nearby',
+ 'PA': 'Park Nearby',
+ 'GO': 'Golf Course Nearby',
+ 'MA': 'Marina Nearby',
+ 'AI': 'Airport Nearby'
+ };
+
+ if (!amenityCodes || amenityCodes === 'N/A') {
+ return [];
+ }
+
+ // Split by semicolon and map each code to full text
+ return amenityCodes.split(';')
+ .map(code => code.trim())
+ .filter(code => code)
+ .map(code => amenityMap[code] || code) // Use full name if found, otherwise use original code
+ .filter(amenity => amenity); // Remove any empty values
+ }
+
// Generate amenities HTML from property data
generateAmenitiesHTML(data) {
const amenities = [];
+ // Check for Private Amenities field first
+ if (data.privateAmenities && data.privateAmenities !== 'N/A') {
+ const privateAmenities = this.mapAmenityCodes(data.privateAmenities);
+ amenities.push(...privateAmenities);
+ }
+
// Check for common amenity fields in the property data
const amenityFields = [
"amenities",
@@ -2638,6 +4467,12 @@ export default class PropertyTemplateSelector extends LightningElement {
generateAmenitiesListItems(data) {
const amenities = [];
+ // Check for Private Amenities field first
+ if (data.privateAmenities && data.privateAmenities !== 'N/A') {
+ const privateAmenities = this.mapAmenityCodes(data.privateAmenities);
+ amenities.push(...privateAmenities);
+ }
+
const amenityFields = [
"amenities",
"features",
@@ -2688,7 +4523,8 @@ export default class PropertyTemplateSelector extends LightningElement {
const data = this.propertyData || {};
const propertyName = data.Name || data.propertyName || "Property Name";
const location = data.Address__c || data.location || "Location";
- const price = data.Price__c || data.price || "Price";
+ // Use price toggle to determine what to display
+ const price = this.showPrice ? (data.Price__c || data.price || "Price") : "Price on Request";
const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A";
const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A";
const size = data.Square_Feet__c || data.size || "N/A";
@@ -2715,6 +4551,9 @@ export default class PropertyTemplateSelector extends LightningElement {
const rentPriceMin = data.Rent_Price_Min__c || data.rentPriceMin || "N/A";
const salePriceMin = data.Sale_Price_Min__c || data.salePriceMin || "N/A";
+
+ // Define logoUrl for template usage
+ const logoUrl = this.logoUrl;
// Build gallery pages so ALL images render in the empty template
const allImages = Array.isArray(this.realPropertyImages)
@@ -2728,7 +4567,6 @@ export default class PropertyTemplateSelector extends LightningElement {
const chunk = allImages.slice(i, i + imagesPerPage);
additionalGalleryPagesHTML += `
-
Property Gallery
${this.generatePropertyGalleryHTMLForImages(chunk)}
@@ -2753,18 +4591,25 @@ export default class PropertyTemplateSelector extends LightningElement {
@media print {
@page {
size: A4;
- margin: 10mm;
+ margin: 0;
}
body {
margin: 0;
padding: 0;
+ background: white;
-webkit-print-color-adjust: exact;
}
- .brochure-page.a4 {
- box-shadow: none;
- page-break-inside: avoid;
+ .brochure {
+ width: 210mm !important;
+ height: 297mm !important;
+ box-shadow: none !important;
+ page-break-after: always;
+ }
+
+ .brochure:last-child {
+ page-break-after: avoid;
}
}
@@ -2849,15 +4694,7 @@ export default class PropertyTemplateSelector extends LightningElement {
-
- Pricing
-
-
Rent Price: ${rentPriceMin}
-
Sale Price: ${salePriceMin}
-
Rent Price (Max): ${data.rentPriceMax || "N/A"}
-
Sale Price (Max): ${data.salePriceMax || "N/A"}
-
-
+ ${this.generatePricingSection()}
@@ -2869,10 +4706,10 @@ export default class PropertyTemplateSelector extends LightningElement {
-
+
Property Description
${titleEnglish}
- ${descriptionEnglish}
+ ${descriptionEnglish}
@@ -2883,12 +4720,21 @@ export default class PropertyTemplateSelector extends LightningElement {
Furnished: ${data.furnished || "N/A"}
Offering Type: ${data.offeringType || "N/A"}
Build Year: ${data.buildYear || "N/A"}
+ ${data.privateAmenities && data.privateAmenities !== 'N/A' ?
+ `
+
Private Amenities:
+
+ ${this.mapAmenityCodes(data.privateAmenities).map(amenity =>
+ `${amenity}`
+ ).join('')}
+
+
` : ''
+ }
- Property Gallery
${
firstChunk.length > 0
@@ -2903,14 +4749,40 @@ export default class PropertyTemplateSelector extends LightningElement {
`;
}
-
createModernHomeTemplate() {
const data = this.propertyData || {};
+ const dimensions = this.getPageDimensions();
console.log("data-----------", data);
const propertyName = data.Name || data.propertyName;
const propertyType = data.Property_Type__c || data.propertyType;
const location = data.Address__c || data.location;
- const price = data.Price__c || data.price;
+ // Use price toggle and selected pricing fields to determine what to display
+ let price = "Price on Request";
+ if (this.showPrice) {
+ const selectedPrices = [];
+
+ // Add selected pricing fields based on step 2 selection
+ if (this.pricingSelection.includeRentPriceMin && data.rentPriceMin && data.rentPriceMin !== "N/A") {
+ selectedPrices.push(data.rentPriceMin);
+ }
+ if (this.pricingSelection.includeRentPriceMax && data.rentPriceMax && data.rentPriceMax !== "N/A") {
+ selectedPrices.push(data.rentPriceMax);
+ }
+ if (this.pricingSelection.includeSalePriceMin && data.salePriceMin && data.salePriceMin !== "N/A") {
+ selectedPrices.push(data.salePriceMin);
+ }
+ if (this.pricingSelection.includeSalePriceMax && data.salePriceMax && data.salePriceMax !== "N/A") {
+ selectedPrices.push(data.salePriceMax);
+ }
+
+ // If no pricing fields are selected, fall back to default price
+ if (selectedPrices.length === 0) {
+ price = data.Price__c || data.price || "Price on Request";
+ } else {
+ // Join selected prices with " | " separator
+ price = selectedPrices.join(" | ");
+ }
+ }
const bedrooms = data.Bedrooms__c || data.bedrooms;
const bathrooms = data.Bathrooms__c || data.bathrooms;
const area = data.Square_Feet__c || data.area;
@@ -2925,13 +4797,13 @@ export default class PropertyTemplateSelector extends LightningElement {
const referenceId =
data.pcrm__Title_English__c || data.Name || data.propertyName || "";
- // Contact information
- const agentName =
- data.contactName || data.Agent_Name__c || data.agentName || "N/A";
- const agentPhone =
- data.contactPhone || data.Agent_Phone__c || data.agentPhone || "N/A";
- const agentEmail =
- data.contactEmail || data.Agent_Email__c || data.agentEmail || "N/A";
+ // Define logoUrl for template usage
+ const logoUrl = "https://tso3listingimages.s3.amazonaws.com/00DFV000001HtSX/a0LFV000001NhJq2AK/companyLogo.jpeg?t=1757764346286";
+
+ // Agent information from loaded agent data
+ const agentName = this.agentData.name || "N/A";
+ const agentPhone = this.agentData.phone || "N/A";
+ const agentEmail = this.agentData.email || "N/A";
// Dynamic gallery and amenities
const propertyGallery = this.generatePropertyGalleryHTML();
@@ -2986,39 +4858,49 @@ export default class PropertyTemplateSelector extends LightningElement {
this.getMapsImageUrl() ||
"https://plus.unsplash.com/premium_photo-1676467963268-5a20d7a7a448?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D";
- // Build dynamic gallery pages like the screenshot (2 columns, last image full width)
+ // Build dynamic gallery pages with responsive grid
const allImages = Array.isArray(this.realPropertyImages)
? this.realPropertyImages
: [];
- const imagesPerPage = 7; // layout similar to screenshot
+ const imagesPerPage = 6; // Optimal for A4 with 2x3 grid
let galleryPagesHTML = "";
if (allImages.length > 0) {
for (let i = 0; i < allImages.length; i += imagesPerPage) {
const chunk = allImages.slice(i, i + imagesPerPage);
+ const pageNumber = Math.floor(i / imagesPerPage) + 1;
+ const totalPages = Math.ceil(allImages.length / imagesPerPage);
+
const chunkHTML = chunk
.map((img, idx) => {
const title =
img.title ||
img.pcrm__Title__c ||
`Property Image ${i + idx + 1}`;
- const extraStyle =
- idx === chunk.length - 1 ? " grid-column: 1 / -1;" : "";
- return `

`;
+
+ // Ensure image URL is absolute for PDF generation
+ const imageUrl = img.url && img.url.startsWith('http') ? img.url :
+ img.url ? `https://salesforce.tech4biz.io${img.url}` :
+ 'https://via.placeholder.com/400x200?text=No+Image';
+
+ return `
+

+
+
`;
})
.join("");
galleryPagesHTML += `
-
${propertyName}
${location}
Specifications
Reference ID: ${referenceId}
Status: ${status}
Type: ${propertyType}
Year Built: ${yearBuilt}
Floor: ${floor}
Parking: ${parking}
Furnishing: ${furnishing}
@@ -3545,126 +5445,6 @@ ${galleryPagesHTML}
`;
}
- createAsgar1Template() {
- const data = this.propertyData || {};
-
- // Basic property information
- const propertyName = data.Name || data.propertyName || "Property Name";
- const location = data.Address__c || data.location || "Location";
- const price = data.Price__c || data.price || "Price";
- const referenceId =
- data.pcrm__Title_English__c || data.Name || data.propertyName || "";
- const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A";
- const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A";
- const area = data.Square_Feet__c || data.area || "N/A";
- const squareFeet = data.Square_Feet__c || data.area || "N/A";
- const sizeUnit = data.sizeUnit || "sq ft";
- const propertyType = data.Property_Type__c || data.propertyType || "N/A";
- const description =
- data.Description_English__c ||
- data.description ||
- "Property description not available.";
-
- // Get smart images
- const exteriorImage =
- this.getExteriorImageUrl() ||
- "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200";
- const interiorImage = this.getSmartImageForSection(
- "interior",
- "https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200"
- );
- const bedroomImage = this.getSmartImageForSection(
- "bedroom",
- "https://images.unsplash.com/photo-1586023492125-27b2c045efd7?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200"
- );
-
- // Generate property gallery
- const propertyGallery = this.generatePropertyGalleryHTML();
-
- // Generate amenities from property data
- const amenitiesHTML = this.generateAmenitiesHTML(data);
-
- // Contact information
- const contactName = data.Agent_Name__c || data.contactName || "N/A";
- const contactPhone = data.Agent_Phone__c || data.contactPhone || "N/A";
- const contactEmail = data.Agent_Email__c || data.contactEmail || "N/A";
- const ownerName = data.Owner_Name__c || data.ownerName || "N/A";
- const ownerPhone = data.Owner_Phone__c || data.ownerPhone || "N/A";
- const ownerEmail = data.Owner_Email__c || data.ownerEmail || "N/A";
-
- // Property specifications
- const status = data.Status__c || data.status || "N/A";
- const yearBuilt = data.Build_Year__c || data.yearBuilt || "N/A";
- const floor = data.Floor__c || data.floor || "N/A";
- const parking = data.Parking_Spaces__c || data.parking || "N/A";
- const furnishing = data.Furnished__c || data.furnishing || "N/A";
- const maintenanceFee =
- data.Maintenance_Fee__c || data.maintenanceFee || "N/A";
- const serviceCharge = data.Service_Charge__c || data.serviceCharge || "N/A";
-
- // Property details
- const acres = data.acres || "0.75";
-
- // Location details
- const cityBayut = data.City__c || data.cityBayut || "N/A";
- const cityPropertyfinder = data.City__c || data.cityPropertyfinder || "N/A";
- const communityBayut = data.Community__c || data.communityBayut || "N/A";
- const subCommunityBayut =
- data.Sub_Community__c || data.subCommunityBayut || "N/A";
- const localityBayut = data.Locality__c || data.localityBayut || "N/A";
- const subLocalityBayut =
- data.Sub_Locality__c || data.subLocalityBayut || "N/A";
- const towerBayut = data.Tower__c || data.towerBayut || "N/A";
- // Nearby amenities
- const schools = data.Schools__c || data.schools || "N/A";
- const shoppingCenters =
- data.Shopping_Centers__c || data.shoppingCenters || "N/A";
- const hospitals = data.Hospitals__c || data.hospitals || "N/A";
- const countryClub = data.Country_Club__c || data.countryClub || "N/A";
- const airportDistance =
- data.Airport_Distance__c || data.airportDistance || "N/A";
- const nearbyLandmarks = data.Landmarks__c || data.nearbyLandmarks || "N/A";
- const transportation =
- data.Transportation__c || data.transportation || "N/A";
- const beachDistance = data.Beach_Distance__c || data.beachDistance || "N/A";
- const metroDistance = data.Metro_Distance__c || data.metroDistance || "N/A";
-
- // Additional information
- const petFriendly = data.Pet_Friendly__c || data.petFriendly || "N/A";
- const smokingAllowed =
- data.Smoking_Allowed__c || data.smokingAllowed || "N/A";
- const availableFrom = data.Available_From__c || data.availableFrom || "N/A";
- const utilitiesIncluded =
- data.Utilities_Included__c || data.utilitiesIncluded || "N/A";
- const internetIncluded =
- data.Internet_Included__c || data.internetIncluded || "N/A";
- const cableIncluded = data.Cable_Included__c || data.cableIncluded || "N/A";
-
- // Additional property fields
- const titleEnglish = data.Title_English__c || data.titleEnglish || "N/A";
- const descriptionEnglish =
- data.Description_English__c || data.descriptionEnglish || "N/A";
- const amenities = data.Amenities__c || data.amenities || "N/A";
- const features = data.Features__c || data.features || "N/A";
- const size = data.Square_Feet__c || data.size || "N/A";
- const parkingSpaces = data.Parking_Spaces__c || data.parkingSpaces || "N/A";
- const buildYear = data.Build_Year__c || data.buildYear || "N/A";
- const offeringType = data.Offering_Type__c || data.offeringType || "N/A";
- // Financial and availability fields
- const rentPriceMin = data.Rent_Price_Min__c || data.rentPriceMin || "N/A";
- const salePriceMin = data.Sale_Price_Min__c || data.salePriceMin || "N/A";
- const rentAvailableFrom =
- data.Rent_Available_From__c || data.rentAvailableFrom || "N/A";
- const rentAvailableTo =
- data.Rent_Available_To__c || data.rentAvailableTo || "N/A";
- const minimumContract =
- data.Minimum_Contract__c || data.minimumContract || "N/A";
- const securityDeposit =
- data.Security_Deposit__c || data.securityDeposit || "N/A";
-
- return `