/** @odoo-module **/ // Fusion Claims - Document Preview (PDF and XML) // Copyright 2024-2025 Nexa Systems Inc. // License OPL-1 import { registry } from "@web/core/registry"; import { _t } from "@web/core/l10n/translation"; import { Component, useState, onMounted } from "@odoo/owl"; import { Dialog } from "@web/core/dialog/dialog"; import { useService } from "@web/core/utils/hooks"; /** * PDF Document Preview Dialog Component * Uses Odoo's built-in PDF.js viewer for XFA/protected PDF support */ export class DocumentPreviewDialog extends Component { static template = "fusion_claims.DocumentPreviewDialog"; static components = { Dialog }; setup() { this.state = useState({ isLoading: true, isMaximized: false }); } getViewerUrl() { const pdfUrl = `/web/content/${this.props.attachmentId}`; const encodedUrl = encodeURIComponent(pdfUrl); return `/web/static/lib/pdfjs/web/viewer.html?file=${encodedUrl}#pagemode=none`; } onIframeLoad() { this.state.isLoading = false; } toggleMaximize() { this.state.isMaximized = !this.state.isMaximized; } getDialogSize() { return this.state.isMaximized ? 'fullscreen' : 'xl'; } getFrameStyle() { return this.state.isMaximized ? 'height: calc(98vh - 100px); width: 100%;' : 'height: calc(85vh - 100px); width: 100%;'; } } /** * XML Viewer Dialog Component * Displays XML content with syntax highlighting */ export class XMLViewerDialog extends Component { static template = "fusion_claims.XMLViewerDialog"; static components = { Dialog }; setup() { this.state = useState({ isLoading: true, isMaximized: false, xmlContent: '', formattedXml: '', error: null }); this.notification = useService("notification"); onMounted(async () => { await this.loadXmlContent(); }); } async loadXmlContent() { try { const response = await fetch(`/web/content/${this.props.attachmentId}`); if (!response.ok) { throw new Error('Failed to load XML file'); } const xmlText = await response.text(); this.state.xmlContent = xmlText; this.state.formattedXml = this.formatXml(xmlText); this.state.isLoading = false; } catch (error) { this.state.error = error.message; this.state.isLoading = false; } } formatXml(xml) { // Format XML with indentation and syntax highlighting let formatted = ''; let indent = 0; const tab = ' '; // Split by tags xml = xml.replace(/>\s*<'); const nodes = xml.split(/(<[^>]+>)/g).filter(n => n.trim()); for (const node of nodes) { if (node.startsWith('')) { // Self-closing tag formatted += tab.repeat(indent) + this.highlightXml(node) + '\n'; } else if (node.startsWith('<')) { // Opening tag formatted += tab.repeat(indent) + this.highlightXml(node) + '\n'; indent++; } else { // Text content const trimmed = node.trim(); if (trimmed) { formatted += tab.repeat(indent) + this.escapeHtml(trimmed) + '\n'; } } } return formatted; } highlightXml(str) { // Escape HTML first str = this.escapeHtml(str); // Highlight tag names str = str.replace(/<(\/?)([\w:-]+)/g, '<$1$2'); // Highlight attributes str = str.replace(/([\w:-]+)=("[^&]*")/g, '$1=$2'); return str; } escapeHtml(str) { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } toggleMaximize() { this.state.isMaximized = !this.state.isMaximized; } getDialogSize() { return this.state.isMaximized ? 'fullscreen' : 'xl'; } async copyToClipboard() { try { await navigator.clipboard.writeText(this.state.xmlContent); this.notification.add(_t("XML copied to clipboard"), { type: 'success' }); } catch (error) { this.notification.add(_t("Failed to copy to clipboard"), { type: 'warning' }); } } downloadXml() { window.open(`/web/content/${this.props.attachmentId}?download=true`, '_blank'); } } /** * Client action to preview a PDF document */ async function previewDocumentAction(env, action) { const attachmentId = action.params?.attachment_id; const title = action.params?.title || "Document Preview"; if (!attachmentId) { env.services.notification.add( _t("No document has been uploaded yet."), { type: 'warning', title: _t("No Document") } ); return; } env.services.dialog.add(DocumentPreviewDialog, { attachmentId: attachmentId, title: title }); } /** * Client action to preview an XML document */ async function previewXmlAction(env, action) { const attachmentId = action.params?.attachment_id; const title = action.params?.title || "XML Viewer"; if (!attachmentId) { env.services.notification.add( _t("No XML file has been uploaded yet."), { type: 'warning', title: _t("No Document") } ); return; } env.services.dialog.add(XMLViewerDialog, { attachmentId: attachmentId, title: title }); } // Register client actions registry.category("actions").add("fusion_claims.preview_document", previewDocumentAction); registry.category("actions").add("fusion_claims.preview_xml", previewXmlAction); /** * Image Preview Dialog Component * Full-screen image preview with navigation */ export class ImagePreviewDialog extends Component { static template = "fusion_claims.ImagePreviewDialog"; static components = { Dialog }; setup() { this.state = useState({ currentIndex: this.props.initialIndex || 0, isLoading: true }); } get currentImage() { return this.props.images[this.state.currentIndex]; } get imageUrl() { return `/web/image/${this.currentImage.id}`; } get hasMultiple() { return this.props.images.length > 1; } get currentPosition() { return `${this.state.currentIndex + 1} / ${this.props.images.length}`; } onImageLoad() { this.state.isLoading = false; } previousImage() { if (this.state.currentIndex > 0) { this.state.isLoading = true; this.state.currentIndex--; } } nextImage() { if (this.state.currentIndex < this.props.images.length - 1) { this.state.isLoading = true; this.state.currentIndex++; } } downloadImage() { window.open(`/web/content/${this.currentImage.id}?download=true`, '_blank'); } } /** * Client action to preview images */ async function previewImageAction(env, action) { const images = action.params?.images || []; const initialIndex = action.params?.initial_index || 0; const title = action.params?.title || "Image Preview"; if (!images.length) { env.services.notification.add( _t("No images available."), { type: 'warning', title: _t("No Images") } ); return; } env.services.dialog.add(ImagePreviewDialog, { images: images, initialIndex: initialIndex, title: title }); } registry.category("actions").add("fusion_claims.preview_image", previewImageAction);