dolibarr.filearchive/tpl/document_browser.tpl.php
data cbf04e6cc9 FileArchiv: Tile/Gallery view with toggle buttons
- 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>
2026-02-19 16:59:19 +01:00

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()">&times;</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>