Initial commit
This commit is contained in:
296
fusion_claims/static/src/js/document_preview.js
Normal file
296
fusion_claims/static/src/js/document_preview.js
Normal file
@@ -0,0 +1,296 @@
|
||||
/** @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);
|
||||
Reference in New Issue
Block a user