1/** 2 * Project Export Generator 3 * Generates standalone HTML exports for client presentations 4 */ 5 6const ProjectExportGenerator = { 7 8 /** 9 * Generate export HTML with embedded project data 10 */ 11 async generateExport(projectData) { 12 console.log('[ExportGenerator] Generating export for project:', projectData.jobName); 13 14 try { 15 // Load the template HTML 16 const templateResponse = await fetch('project-export.html'); 17 let templateHTML = await templateResponse.text(); 18 19 // Prepare project data for embedding 20 const exportData = { 21 jobName: projectData.jobName, 22 client: projectData.client || '', 23 notes: projectData.notes || '', 24 categories: projectData.categories || [], 25 items: Array.from(projectData.items.entries()).map(([instanceId, item]) => ({ 26 plantId: item.plantId || instanceId, // Use the actual plantId from item, fallback to instanceId for old data 27 instanceId: instanceId, 28 quantity: item.quantity, 29 category: item.category, 30 notes: item.notes, 31 addedDate: item.addedDate, 32 plantData: item.plantData || ProjectFavoritesManager.plantsData.get(item.plantId || instanceId) 33 })), 34 createdDate: projectData.createdDate, 35 exportDate: new Date().toISOString() 36 }; 37 38 // Embed the project data into the HTML 39 const dataScript = ` 40 <script id="projectData"> 41 window.PROJECT_DATA = ${JSON.stringify(exportData, null, 2)}; 42 </script> 43 `; 44 45 // Replace the placeholder script with actual data 46 templateHTML = templateHTML.replace( 47 /<script id="projectData">[\s\S]*?<\/script>/, 48 dataScript 49 ); 50 51 return templateHTML; 52 } catch (error) { 53 console.error('[ExportGenerator] Failed to generate export:', error); 54 throw error; 55 } 56 }, 57 58 /** 59 * Open export in new window/tab 60 */ 61 async openExport(projectData) { 62 try { 63 const html = await this.generateExport(projectData); 64 65 // Open in new window 66 const exportWindow = window.open('', '_blank'); 67 exportWindow.document.write(html); 68 exportWindow.document.close(); 69 70 return exportWindow; 71 } catch (error) { 72 console.error('[ExportGenerator] Failed to open export:', error); 73 alert('Failed to generate export. Please try again.'); 74 } 75 }, 76 77 /** 78 * Download export as HTML file 79 */ 80 async downloadExport(projectData) { 81 try { 82 const html = await this.generateExport(projectData); 83 84 // Create blob and download 85 const blob = new Blob([html], { type: 'text/html' }); 86 const url = URL.createObjectURL(blob); 87 88 const a = document.createElement('a'); 89 a.href = url; 90 a.download = `${projectData.jobName.replace(/[^a-z0-9]/gi, '_')}_plant_proposal.html`; 91 document.body.appendChild(a); 92 a.click(); 93 document.body.removeChild(a); 94 95 URL.revokeObjectURL(url); 96 97 console.log('[ExportGenerator] Export downloaded successfully'); 98 } catch (error) { 99 console.error('[ExportGenerator] Failed to download export:', error); 100 alert('Failed to download export. Please try again.'); 101 } 102 }, 103 104 /** 105 * Generate shareable URL using Firebase storage for short URLs 106 */ 107 async generateShareableURL(projectData) { 108 try { 109 // Check Firebase availability 110 if (!window.FirebaseStorage || !FirebaseStorage.isInitialized) { 111 alert('Please wait for the system to initialize and try again.'); 112 return null; 113 } 114 115 console.log('[ExportGenerator] Generating Firebase short URL...'); 116 117 // Helper function to clean undefined values for Firebase 118 const cleanObject = (obj) => { 119 const cleaned = {}; 120 for (const [key, value] of Object.entries(obj)) { 121 if (value !== undefined && value !== null) { 122 if (typeof value === 'object' && !Array.isArray(value)) { 123 const cleanedSubObj = cleanObject(value); 124 if (Object.keys(cleanedSubObj).length > 0) { 125 cleaned[key] = cleanedSubObj; 126 } 127 } else { 128 cleaned[key] = value; 129 } 130 } 131 } 132 return cleaned; 133 }; 134 135 // Prepare export data 136 const exportData = { 137 jobName: projectData.jobName || '', 138 client: projectData.client || '', 139 notes: projectData.notes || '', 140 categories: projectData.categories || [], 141 items: Array.from(projectData.items.entries()).map(([instanceId, item]) => { 142 const plantId = item.plantId || instanceId; // Use the actual plantId from item 143 const cleanedItem = { 144 plantId: plantId, 145 instanceId: instanceId, 146 quantity: item.quantity || 1, 147 category: item.category || '', 148 notes: item.notes || '', 149 addedDate: item.addedDate || Date.now(), 150 // Include all plant data for display 151 plantData: item.plantData || ProjectFavoritesManager.plantsData.get(plantId) || { 152 'Botanical Name': plantId 153 } 154 }; 155 return cleanObject(cleanedItem); 156 }), 157 createdDate: projectData.createdDate || Date.now(), 158 exportDate: new Date().toISOString() 159 }; 160 161 // Final clean of the entire export data 162 const cleanedExportData = cleanObject(exportData); 163 164 // Save to Firebase and get short ID 165 const exportId = await FirebaseStorage.saveExport(cleanedExportData, projectData.jobName); 166 if (exportId) { 167 const baseURL = window.location.origin + '/2026projects/updategardenplan/project-export.html'; 168 const shortURL = `${baseURL}
168?id=${exportId}`; 169 console.log(`[ExportGenerator] â Generated short URL: ${shortURL}`); 170 return shortURL; 171 } else { 172 throw new Error('Failed to generate export ID'); 173 } 174 175 } catch (error) { 176 console.error('[ExportGenerator] Failed to generate shareable URL:', error); 177 alert('Failed to generate shareable link. Please check your internet connection and try again.'); 178 return null; 179 } 180 }, 181 182 /** 183 * Copy shareable URL to clipboard 184 */ 185 async copyShareableURL(projectData) { 186 try { 187 // Show loading indicator 188 const loadingMessage = 'Generating shareable link...'; 189 console.log('[ExportGenerator]', loadingMessage); 190 191 const url = await this.generateShareableURL(projectData); 192 193 if (!url) { 194 return false; 195 } 196 197 try { 198 await navigator.clipboard.writeText(url); 199 alert('Shareable link copied to clipboard!\n\nLink length: ' + url.length + ' characters'); 200 return true; 201 } catch (error) { 202 console.error('[ExportGenerator] Failed to copy URL:', error); 203 204 // Fallback for older browsers 205 const textArea = document.createElement('textarea'); 206 textArea.value = url; 207 textArea.style.position = 'fixed'; 208 textArea.style.left = '-999999px'; 209 document.body.appendChild(textArea); 210 textArea.select(); 211 212 try { 213 document.execCommand('copy'); 214 alert('Shareable link copied to clipboard!\n\nLink length: ' + url.length + ' characters'); 215 return true; 216 } catch (e) { 217 alert('Failed to copy link. Please copy manually:\n\n' + url); 218 return false; 219 } finally { 220 document.body.removeChild(textArea); 221 } 222 } 223 } catch (error) { 224 console.error('[ExportGenerator] Error generating shareable URL:', error); 225 alert('Failed to generate shareable link. Please try again.'); 226 return false; 227 } 228 }, 229 230 /** 231 * Show export options dialog 232 */ 233 showExportDialog(projectData) { 234 // Create modal HTML 235 const modalHTML = ` 236 <div id="exportModal" class="modal" style="display: block;"> 237 <div class="modal-content" style="max-width: 500px;"> 238 <span class="close-modal" onclick="document.getElementById('exportModal').remove()">×</span> 239 <h2>Export Project for Client</h2> 240 <p>Choose how to share "${projectData.jobName}" with your client:</p> 241 242 <div style="display: flex; flex-direction: column; gap: 15px; margin-top: 20px;"> 243 <button class="primary-btn" onclick="ProjectExportGenerator.openExport(ProjectFavoritesManager.projects.get('${projectData.jobName}')); document.getElementById('exportModal').remove();"> 244 ð Open in New Tab 245 <small style="display: block; font-weight: normal; margin-top: 5px;">View and print the presentation</small> 246 </button> 247 248 <!--<button class="primary-btn" onclick="ProjectExportGenerator.downloadExport(ProjectFavoritesManager.projects.get('${projectData.jobName}')); document.getElementById('exportModal').remove();"> 249 ð¾ Download HTML File 250 <small style="display: block; font-weight: normal; margin-top: 5px;">Save for email attachment</small> 251 </button>--> 252 253 <button class="primary-btn" onclick="ProjectExportGenerator.copyShareableURL(ProjectFavoritesManager.projects.get('${projectData.jobName}')); document.getElementById('exportModal').remove();"> 254 ð Copy Shareable Link 255 <small style="display: block; font-weight: normal; margin-top: 5px;">Generate a short link to share</small> 256 </button> 257 </div> 258 259 <div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee;"> 260 <p style="font-size: 12px; color: #666;"> 261 <strong>Note:</strong> The export will include all plants in the current project. Links are automatically shortened for easy sharing. 262 </p> 263 </div> 264 </div> 265 </div> 266 `; 267 268 // Add modal to page 269 const modalDiv = document.createElement('div'); 270 modalDiv.innerHTML = modalHTML; 271 document.body.appendChild(modalDiv.firstElementChild); 272 } 273}; 274 275// Make available globally 276window.ProjectExportGenerator = ProjectExportGenerator;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.