297 lines
8.4 KiB
JavaScript
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(/<(\/?)([\w:-]+)/g,
|
|
'<$1<span class="xml-tag">$2</span>');
|
|
|
|
// Highlight attributes
|
|
str = str.replace(/([\w:-]+)=("[^&]*")/g,
|
|
'<span class="xml-attr">$1</span>=<span class="xml-value">$2</span>');
|
|
|
|
return str;
|
|
}
|
|
|
|
escapeHtml(str) {
|
|
return str
|
|
.replace(/&/g, '&')
|
|
.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);
|