- Added formObjectOptions hook for document pages - CSS/JS injected via DOMContentLoaded for proper timing - Toggle between list and tile view (localStorage persisted) - Lightbox for image gallery with keyboard navigation - Fixed hook contexts (productdocuments, invoicedocuments, etc.) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
426 lines
11 KiB
PHP
426 lines
11 KiB
PHP
<?php
|
|
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
|
*
|
|
* Document Browser for Email Attachments
|
|
* Shows related documents from customer, proposals, orders, products, etc.
|
|
*/
|
|
|
|
// Requires: $element, $objectId, $trackid, $langs to be set
|
|
|
|
if (empty($element) || empty($objectId)) {
|
|
return;
|
|
}
|
|
|
|
$supportedElements = array('propal', 'commande', 'facture', 'expedition', 'order_supplier', 'invoice_supplier');
|
|
if (!in_array($element, $supportedElements)) {
|
|
return;
|
|
}
|
|
|
|
$ajaxUrl = dol_buildpath('/filearchiv/ajax/getdocuments.php', 1);
|
|
$addFileUrl = dol_buildpath('/filearchiv/ajax/addfile.php', 1);
|
|
?>
|
|
|
|
<style>
|
|
/* Document Browser Styles */
|
|
.filearchiv-docbrowser-section {
|
|
margin-top: 20px;
|
|
padding: 15px;
|
|
background: #f8f9fa;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 6px;
|
|
}
|
|
.filearchiv-docbrowser-title {
|
|
font-weight: 600;
|
|
margin-bottom: 10px;
|
|
color: #333;
|
|
}
|
|
.filearchiv-modal-overlay {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0,0,0,0.5);
|
|
z-index: 9999;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
.filearchiv-modal-overlay.active {
|
|
display: flex;
|
|
}
|
|
.filearchiv-modal {
|
|
background: #fff;
|
|
border-radius: 8px;
|
|
width: 90%;
|
|
max-width: 800px;
|
|
max-height: 80vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
|
}
|
|
.filearchiv-modal-header {
|
|
padding: 15px 20px;
|
|
border-bottom: 1px solid #ddd;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
background: #f8f9fa;
|
|
border-radius: 8px 8px 0 0;
|
|
}
|
|
.filearchiv-modal-header h3 {
|
|
margin: 0;
|
|
font-size: 18px;
|
|
color: #333;
|
|
}
|
|
.filearchiv-modal-close {
|
|
font-size: 24px;
|
|
cursor: pointer;
|
|
color: #666;
|
|
line-height: 1;
|
|
}
|
|
.filearchiv-modal-close:hover {
|
|
color: #333;
|
|
}
|
|
.filearchiv-modal-body {
|
|
padding: 20px;
|
|
overflow-y: auto;
|
|
flex: 1;
|
|
}
|
|
.filearchiv-modal-footer {
|
|
padding: 15px 20px;
|
|
border-top: 1px solid #ddd;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
background: #f8f9fa;
|
|
border-radius: 0 0 8px 8px;
|
|
}
|
|
.filearchiv-category {
|
|
margin-bottom: 15px;
|
|
border: 1px solid #e0e0e0;
|
|
border-radius: 6px;
|
|
overflow: hidden;
|
|
}
|
|
.filearchiv-category-header {
|
|
padding: 12px 15px;
|
|
background: #f5f5f5;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
.filearchiv-category-header:hover {
|
|
background: #eee;
|
|
}
|
|
.filearchiv-category-header i {
|
|
width: 20px;
|
|
text-align: center;
|
|
}
|
|
.filearchiv-category-header .count {
|
|
margin-left: auto;
|
|
background: #007bff;
|
|
color: #fff;
|
|
padding: 2px 8px;
|
|
border-radius: 10px;
|
|
font-size: 12px;
|
|
}
|
|
.filearchiv-category-header .toggle-icon {
|
|
margin-left: 5px;
|
|
transition: transform 0.2s;
|
|
}
|
|
.filearchiv-category-body {
|
|
display: none;
|
|
padding: 10px 15px;
|
|
background: #fff;
|
|
}
|
|
.filearchiv-category.open .filearchiv-category-body {
|
|
display: block;
|
|
}
|
|
.filearchiv-category.open .toggle-icon {
|
|
transform: rotate(180deg);
|
|
}
|
|
.filearchiv-file-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 8px 10px;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
gap: 10px;
|
|
}
|
|
.filearchiv-file-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.filearchiv-file-item:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
.filearchiv-file-item input[type="checkbox"] {
|
|
width: 18px;
|
|
height: 18px;
|
|
cursor: pointer;
|
|
}
|
|
.filearchiv-file-item .file-icon {
|
|
font-size: 20px;
|
|
width: 24px;
|
|
text-align: center;
|
|
}
|
|
.filearchiv-file-item .file-info {
|
|
flex: 1;
|
|
}
|
|
.filearchiv-file-item .file-name {
|
|
font-weight: 500;
|
|
color: #333;
|
|
word-break: break-word;
|
|
}
|
|
.filearchiv-file-item .file-meta {
|
|
font-size: 12px;
|
|
color: #888;
|
|
margin-top: 2px;
|
|
}
|
|
.filearchiv-file-item .file-source {
|
|
font-size: 11px;
|
|
color: #007bff;
|
|
background: #e7f3ff;
|
|
padding: 2px 6px;
|
|
border-radius: 3px;
|
|
}
|
|
.filearchiv-selected-count {
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
.filearchiv-btn {
|
|
padding: 8px 16px;
|
|
border-radius: 4px;
|
|
border: none;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.filearchiv-btn-primary {
|
|
background: #007bff;
|
|
color: #fff;
|
|
}
|
|
.filearchiv-btn-primary:hover {
|
|
background: #0056b3;
|
|
}
|
|
.filearchiv-btn-primary:disabled {
|
|
background: #ccc;
|
|
cursor: not-allowed;
|
|
}
|
|
.filearchiv-btn-secondary {
|
|
background: #6c757d;
|
|
color: #fff;
|
|
}
|
|
.filearchiv-btn-secondary:hover {
|
|
background: #545b62;
|
|
}
|
|
.filearchiv-loading {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: #666;
|
|
}
|
|
.filearchiv-loading i {
|
|
font-size: 32px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.filearchiv-empty {
|
|
text-align: center;
|
|
padding: 30px;
|
|
color: #888;
|
|
}
|
|
</style>
|
|
|
|
<!-- Document Browser Section -->
|
|
<div class="filearchiv-docbrowser-section">
|
|
<div class="filearchiv-docbrowser-title">
|
|
<i class="fas fa-folder-open"></i> <?php echo $langs->trans('BrowseRelatedDocuments'); ?>
|
|
</div>
|
|
<button type="button" class="filearchiv-btn filearchiv-btn-secondary" onclick="FileArchivDocBrowser.open()">
|
|
<i class="fas fa-search"></i> <?php echo $langs->trans('SelectDocumentsToAttach'); ?>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Modal -->
|
|
<div class="filearchiv-modal-overlay" id="filearchiv-docbrowser-modal">
|
|
<div class="filearchiv-modal">
|
|
<div class="filearchiv-modal-header">
|
|
<h3><i class="fas fa-folder-open"></i> <?php echo $langs->trans('SelectDocumentsToAttach'); ?></h3>
|
|
<span class="filearchiv-modal-close" onclick="FileArchivDocBrowser.close()">×</span>
|
|
</div>
|
|
<div class="filearchiv-modal-body" id="filearchiv-docbrowser-content">
|
|
<div class="filearchiv-loading">
|
|
<i class="fas fa-spinner fa-spin"></i>
|
|
<div><?php echo $langs->trans('Loading'); ?>...</div>
|
|
</div>
|
|
</div>
|
|
<div class="filearchiv-modal-footer">
|
|
<span class="filearchiv-selected-count">
|
|
<span id="filearchiv-selected-num">0</span> <?php echo $langs->trans('FilesSelected'); ?>
|
|
</span>
|
|
<div>
|
|
<button type="button" class="filearchiv-btn filearchiv-btn-secondary" onclick="FileArchivDocBrowser.close()">
|
|
<?php echo $langs->trans('Cancel'); ?>
|
|
</button>
|
|
<button type="button" class="filearchiv-btn filearchiv-btn-primary" id="filearchiv-add-btn" onclick="FileArchivDocBrowser.addSelected()" disabled>
|
|
<i class="fas fa-plus"></i> <?php echo $langs->trans('AddSelectedFiles'); ?>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
var FileArchivDocBrowser = {
|
|
element: "<?php echo dol_escape_js($element); ?>",
|
|
id: <?php echo (int) $objectId; ?>,
|
|
trackid: "<?php echo dol_escape_js($trackid); ?>",
|
|
ajaxUrl: "<?php echo dol_escape_js($ajaxUrl); ?>",
|
|
addFileUrl: "<?php echo dol_escape_js($addFileUrl); ?>",
|
|
token: "<?php echo newToken(); ?>",
|
|
loaded: false,
|
|
selectedFiles: [],
|
|
|
|
open: function() {
|
|
document.getElementById("filearchiv-docbrowser-modal").classList.add("active");
|
|
document.body.style.overflow = "hidden";
|
|
if (!this.loaded) {
|
|
this.loadDocuments();
|
|
}
|
|
},
|
|
|
|
close: function() {
|
|
document.getElementById("filearchiv-docbrowser-modal").classList.remove("active");
|
|
document.body.style.overflow = "";
|
|
},
|
|
|
|
loadDocuments: function() {
|
|
var self = this;
|
|
var url = this.ajaxUrl + "?element=" + this.element + "&id=" + this.id + "&token=" + this.token;
|
|
|
|
fetch(url)
|
|
.then(function(response) { return response.json(); })
|
|
.then(function(data) {
|
|
self.loaded = true;
|
|
self.renderDocuments(data);
|
|
})
|
|
.catch(function(error) {
|
|
console.error("Error loading documents:", error);
|
|
document.getElementById("filearchiv-docbrowser-content").innerHTML =
|
|
'<div class="filearchiv-empty"><i class="fas fa-exclamation-triangle"></i> <?php echo $langs->trans('ErrorLoadingDocuments'); ?></div>';
|
|
});
|
|
},
|
|
|
|
renderDocuments: function(data) {
|
|
var self = this;
|
|
var container = document.getElementById("filearchiv-docbrowser-content");
|
|
|
|
if (!data.success || !data.categories || data.categories.length === 0) {
|
|
container.innerHTML = '<div class="filearchiv-empty"><i class="fas fa-folder-open"></i><br><?php echo $langs->trans('NoRelatedDocumentsFound'); ?></div>';
|
|
return;
|
|
}
|
|
|
|
var html = "";
|
|
data.categories.forEach(function(category, catIndex) {
|
|
html += '<div class="filearchiv-category' + (catIndex === 0 ? ' open' : '') + '" id="cat-' + catIndex + '">';
|
|
html += '<div class="filearchiv-category-header" onclick="FileArchivDocBrowser.toggleCategory(' + catIndex + ')">';
|
|
html += '<i class="fas ' + category.icon + '"></i>';
|
|
html += '<span>' + category.title + '</span>';
|
|
html += '<span class="count">' + category.files.length + '</span>';
|
|
html += '<i class="fas fa-chevron-down toggle-icon"></i>';
|
|
html += '</div>';
|
|
html += '<div class="filearchiv-category-body">';
|
|
|
|
category.files.forEach(function(file, fileIndex) {
|
|
var fileId = "file-" + catIndex + "-" + fileIndex;
|
|
html += '<div class="filearchiv-file-item">';
|
|
html += '<input type="checkbox" id="' + fileId + '" onchange="FileArchivDocBrowser.toggleFile(this, \'' + self.escapeHtml(file.fullpath) + '\', \'' + self.escapeHtml(file.name) + '\')">';
|
|
html += '<i class="fas ' + file.icon + ' file-icon"></i>';
|
|
html += '<div class="file-info">';
|
|
html += '<div class="file-name">' + self.escapeHtml(file.name) + '</div>';
|
|
html += '<div class="file-meta">' + file.size_formatted + ' - ' + file.date + '</div>';
|
|
if (file.product_ref) {
|
|
html += '<span class="file-source">' + self.escapeHtml(file.product_ref) + ': ' + self.escapeHtml(file.product_label) + '</span>';
|
|
} else if (file.object_ref) {
|
|
html += '<span class="file-source">' + self.escapeHtml(file.object_ref) + '</span>';
|
|
}
|
|
html += '</div>';
|
|
html += '</div>';
|
|
});
|
|
|
|
html += '</div></div>';
|
|
});
|
|
|
|
container.innerHTML = html;
|
|
},
|
|
|
|
escapeHtml: function(text) {
|
|
var div = document.createElement("div");
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
},
|
|
|
|
toggleCategory: function(index) {
|
|
var cat = document.getElementById("cat-" + index);
|
|
cat.classList.toggle("open");
|
|
},
|
|
|
|
toggleFile: function(checkbox, filepath, filename) {
|
|
if (checkbox.checked) {
|
|
this.selectedFiles.push({ path: filepath, name: filename });
|
|
} else {
|
|
this.selectedFiles = this.selectedFiles.filter(function(f) {
|
|
return f.path !== filepath;
|
|
});
|
|
}
|
|
this.updateSelectedCount();
|
|
},
|
|
|
|
updateSelectedCount: function() {
|
|
document.getElementById("filearchiv-selected-num").textContent = this.selectedFiles.length;
|
|
document.getElementById("filearchiv-add-btn").disabled = this.selectedFiles.length === 0;
|
|
},
|
|
|
|
addSelected: function() {
|
|
if (this.selectedFiles.length === 0) return;
|
|
|
|
var self = this;
|
|
var addBtn = document.getElementById("filearchiv-add-btn");
|
|
addBtn.disabled = true;
|
|
addBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> <?php echo $langs->trans('Adding'); ?>...';
|
|
|
|
var promises = this.selectedFiles.map(function(file) {
|
|
return fetch(self.addFileUrl, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
},
|
|
body: "filepath=" + encodeURIComponent(file.path) +
|
|
"&filename=" + encodeURIComponent(file.name) +
|
|
"&trackid=" + encodeURIComponent(self.trackid) +
|
|
"&token=" + encodeURIComponent(self.token)
|
|
});
|
|
});
|
|
|
|
Promise.all(promises)
|
|
.then(function() {
|
|
window.location.reload();
|
|
})
|
|
.catch(function(error) {
|
|
console.error("Error adding files:", error);
|
|
addBtn.disabled = false;
|
|
addBtn.innerHTML = '<i class="fas fa-plus"></i> <?php echo $langs->trans('AddSelectedFiles'); ?>';
|
|
alert("<?php echo $langs->trans('ErrorAddingFiles'); ?>");
|
|
});
|
|
}
|
|
};
|
|
|
|
// Close modal on Escape
|
|
document.addEventListener("keydown", function(e) {
|
|
if (e.key === "Escape" && document.getElementById("filearchiv-docbrowser-modal").classList.contains("active")) {
|
|
FileArchivDocBrowser.close();
|
|
}
|
|
});
|
|
</script>
|