Files
Odoo-Modules/fusion_claims/static/src/js/document_preview.js
2026-02-22 01:22:18 -05:00

297 lines
8.4 KiB
JavaScript

/** @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*</g, '><');
const nodes = xml.split(/(<[^>]+>)/g).filter(n => n.trim());
for (const node of nodes) {
if (node.startsWith('</')) {
// Closing tag - decrease indent
indent = Math.max(0, indent - 1);
formatted += tab.repeat(indent) + this.highlightXml(node) + '\n';
} else if (node.startsWith('<?') || node.startsWith('<!')) {
// Declaration or comment
formatted += this.highlightXml(node) + '\n';
} else if (node.startsWith('<') && node.endsWith('/>')) {
// 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(/&lt;(\/?)([\w:-]+)/g,
'&lt;$1<span class="xml-tag">$2</span>');
// Highlight attributes
str = str.replace(/([\w:-]+)=(&quot;[^&]*&quot;)/g,
'<span class="xml-attr">$1</span>=<span class="xml-value">$2</span>');
return str;
}
escapeHtml(str) {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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);