- 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>
1942 lines
57 KiB
PHP
1942 lines
57 KiB
PHP
<?php
|
|
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*/
|
|
|
|
/**
|
|
* \file class/actions_filearchiv.class.php
|
|
* \brief Hook actions for FileArchiv module - enhanced file display with gallery view
|
|
*/
|
|
|
|
require_once DOL_DOCUMENT_ROOT.'/core/class/commonhookactions.class.php';
|
|
|
|
class ActionsFilearchiv extends CommonHookActions
|
|
{
|
|
/**
|
|
* @var DoliDB Database handler
|
|
*/
|
|
public $db;
|
|
|
|
/**
|
|
* @var string Error message
|
|
*/
|
|
public $error = '';
|
|
|
|
/**
|
|
* @var array Errors
|
|
*/
|
|
public $errors = array();
|
|
|
|
/**
|
|
* @var array Hook results
|
|
*/
|
|
public $results = array();
|
|
|
|
/**
|
|
* @var string Returned HTML content (legacy)
|
|
*/
|
|
public $resPrint = '';
|
|
|
|
/**
|
|
* @var string Returned HTML content (used by HookManager for addreplace hooks)
|
|
*/
|
|
public $resprints = '';
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param DoliDB $db Database handler
|
|
*/
|
|
public function __construct($db)
|
|
{
|
|
$this->db = $db;
|
|
// Debug: Write to file when class is instantiated - use module dir for debugging
|
|
$debugFile = dirname(__FILE__) . '/../debug.log';
|
|
@file_put_contents($debugFile, date('Y-m-d H:i:s') . " === ActionsFilearchiv CONSTRUCTOR === PHP_SELF=" . $_SERVER['PHP_SELF'] . "\n", FILE_APPEND);
|
|
}
|
|
|
|
/**
|
|
* Hook called on document pages (formObjectOptions)
|
|
* Injects the gallery/tile view assets since resPrint is actually output here
|
|
*
|
|
* @param array $parameters Hook parameters
|
|
* @param CommonObject $object Object
|
|
* @param string $action Current action
|
|
* @param HookManager $hookmanager Hook manager
|
|
* @return int 0 = keep standard behavior
|
|
*/
|
|
public function formObjectOptions($parameters, &$object, &$action, $hookmanager)
|
|
{
|
|
global $conf, $langs;
|
|
|
|
// Only run on document pages
|
|
$context = $hookmanager->contextarray;
|
|
$isDocumentPage = false;
|
|
foreach ($context as $ctx) {
|
|
if (strpos($ctx, 'documents') !== false) {
|
|
$isDocumentPage = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$isDocumentPage || !isModEnabled('filearchiv')) {
|
|
return 0;
|
|
}
|
|
|
|
// Debug logging
|
|
$debugFile = dirname(__FILE__) . '/../debug.log';
|
|
@file_put_contents($debugFile, date('Y-m-d H:i:s') . " === formObjectOptions CALLED ===\n", FILE_APPEND);
|
|
@file_put_contents($debugFile, " context: " . implode(', ', $context) . "\n", FILE_APPEND);
|
|
|
|
// Inject the gallery assets - they will wait for DOMContentLoaded
|
|
$this->resprints = $this->getGalleryAssetsDeferred();
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Hook to add custom options/buttons to the file list
|
|
* Called in FormFile::list_of_documents after file list rendering
|
|
*
|
|
* @param array $parameters Hook parameters
|
|
* @param FormFile $object FormFile object
|
|
* @param string $action Current action
|
|
* @param HookManager $hookmanager Hook manager
|
|
* @return int 0 = keep standard behavior, >0 = replace standard behavior
|
|
*/
|
|
public function formBuilddocLineOptions($parameters, &$object, &$action, $hookmanager)
|
|
{
|
|
global $conf, $langs;
|
|
|
|
if (!isModEnabled('filearchiv')) {
|
|
return 0;
|
|
}
|
|
|
|
$filearray = isset($parameters['filearray']) ? $parameters['filearray'] : array();
|
|
$file = isset($parameters['file']) ? $parameters['file'] : array();
|
|
|
|
if (empty($file) || empty($file['name'])) {
|
|
return 0;
|
|
}
|
|
|
|
// Check if this is an image or PDF
|
|
$isImage = $this->isImageFile($file['name']);
|
|
$isPdf = $this->isPdfFile($file['name']);
|
|
|
|
if (!$isImage && !$isPdf) {
|
|
return 0;
|
|
}
|
|
|
|
// Get pinned status
|
|
$isPinned = $this->isFilePinned($file);
|
|
|
|
// Add pin button
|
|
$modulepart = isset($parameters['modulepart']) ? $parameters['modulepart'] : '';
|
|
$relativepath = isset($file['relativename']) ? $file['relativename'] : $file['name'];
|
|
|
|
$pinUrl = $_SERVER['PHP_SELF'] . '?' . $_SERVER['QUERY_STRING'];
|
|
$pinUrl .= '&action=filearchiv_togglepin';
|
|
$pinUrl .= '&filepath=' . urlencode($relativepath);
|
|
$pinUrl .= '&modulepart=' . urlencode($modulepart);
|
|
$pinUrl .= '&token=' . newToken();
|
|
|
|
$pinIcon = $isPinned ? 'fas fa-thumbtack' : 'far fa-thumbtack';
|
|
$pinTitle = $isPinned ? $langs->trans('Unpin') : $langs->trans('Pin');
|
|
$pinClass = $isPinned ? 'filearchiv-pinned' : 'filearchiv-unpinned';
|
|
|
|
$this->resprints = '<a href="' . $pinUrl . '" class="' . $pinClass . '" title="' . $pinTitle . '">';
|
|
$this->resprints .= '<i class="' . $pinIcon . '"></i>';
|
|
$this->resprints .= '</a> ';
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Hook called before showing the file list
|
|
* Used to inject CSS/JS only when needed and handle view mode switching
|
|
*
|
|
* @param array $parameters Hook parameters
|
|
* @param FormFile $object FormFile object
|
|
* @param string $action Current action
|
|
* @param HookManager $hookmanager Hook manager
|
|
* @return int 0 = keep standard behavior, >0 = replace standard behavior
|
|
*/
|
|
public function showFilesList($parameters, &$object, &$action, $hookmanager)
|
|
{
|
|
global $conf, $langs, $user;
|
|
|
|
// Debug logging
|
|
$debugFile = dirname(__FILE__) . '/../debug.log';
|
|
@file_put_contents($debugFile, date('Y-m-d H:i:s') . " === showFilesList CALLED ===\n", FILE_APPEND);
|
|
@file_put_contents($debugFile, " modulepart: " . (isset($parameters['modulepart']) ? $parameters['modulepart'] : 'none') . "\n", FILE_APPEND);
|
|
@file_put_contents($debugFile, " filearray count: " . (isset($parameters['filearray']) ? count($parameters['filearray']) : 0) . "\n", FILE_APPEND);
|
|
|
|
if (!isModEnabled('filearchiv')) {
|
|
@file_put_contents($debugFile, " -> module not enabled\n", FILE_APPEND);
|
|
return 0;
|
|
}
|
|
|
|
$filearray = isset($parameters['filearray']) ? $parameters['filearray'] : array();
|
|
|
|
// Check if there are any images or PDFs
|
|
$hasMediaFiles = false;
|
|
foreach ($filearray as $file) {
|
|
if ($this->isImageFile($file['name']) || $this->isPdfFile($file['name'])) {
|
|
$hasMediaFiles = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$hasMediaFiles) {
|
|
@file_put_contents($debugFile, " -> no media files found\n", FILE_APPEND);
|
|
return 0;
|
|
}
|
|
|
|
@file_put_contents($debugFile, " -> injecting gallery assets\n", FILE_APPEND);
|
|
|
|
// Inject CSS and JS only when needed
|
|
$assets = $this->getInlineAssets($filearray, $parameters);
|
|
@file_put_contents($debugFile, " -> assets length: " . strlen($assets) . "\n", FILE_APPEND);
|
|
@file_put_contents($debugFile, " -> first 200 chars: " . substr($assets, 0, 200) . "\n", FILE_APPEND);
|
|
|
|
$this->resprints = $assets;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Generate deferred gallery assets that wait for DOM
|
|
* Called from formObjectOptions which runs BEFORE the file table exists
|
|
*
|
|
* @return string HTML with CSS and deferred JS
|
|
*/
|
|
private function getGalleryAssetsDeferred()
|
|
{
|
|
$out = '';
|
|
|
|
// CSS - can be injected immediately
|
|
$out .= '<style id="filearchiv-styles">
|
|
/* FileArchiv View Toggle */
|
|
.filearchiv-view-toggle {
|
|
display: inline-flex;
|
|
gap: 2px;
|
|
margin-left: 10px;
|
|
vertical-align: middle;
|
|
}
|
|
.filearchiv-view-toggle button {
|
|
padding: 4px 8px;
|
|
border: 1px solid var(--colorborder, #ccc);
|
|
background: var(--colorbackbody, #fff);
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
color: var(--colortextlink, #333);
|
|
}
|
|
.filearchiv-view-toggle button:first-child {
|
|
border-radius: 3px 0 0 3px;
|
|
}
|
|
.filearchiv-view-toggle button:last-child {
|
|
border-radius: 0 3px 3px 0;
|
|
}
|
|
.filearchiv-view-toggle button.active {
|
|
background: var(--colortextlink, #0077b6);
|
|
color: #fff;
|
|
border-color: var(--colortextlink, #0077b6);
|
|
}
|
|
.filearchiv-view-toggle button:hover:not(.active) {
|
|
background: var(--colorbacklinepairhover, #f0f0f0);
|
|
}
|
|
|
|
/* Tile/Grid View */
|
|
.filearchiv-tiles-container {
|
|
display: none;
|
|
margin-top: 10px;
|
|
}
|
|
.filearchiv-tiles-container.active {
|
|
display: block;
|
|
}
|
|
.filearchiv-tiles {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
gap: 12px;
|
|
padding: 10px 0;
|
|
}
|
|
.filearchiv-tile {
|
|
border: 1px solid var(--colorborder, #ddd);
|
|
border-radius: 4px;
|
|
padding: 8px;
|
|
text-align: center;
|
|
background: var(--colorbackbody, #fff);
|
|
transition: box-shadow 0.2s, border-color 0.2s;
|
|
cursor: pointer;
|
|
}
|
|
.filearchiv-tile:hover {
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.12);
|
|
border-color: var(--colortextlink, #0077b6);
|
|
}
|
|
.filearchiv-tile .tile-preview {
|
|
width: 100%;
|
|
height: 100px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
overflow: hidden;
|
|
margin-bottom: 6px;
|
|
background: var(--colorbacklinepair1, #fafafa);
|
|
border-radius: 3px;
|
|
}
|
|
.filearchiv-tile .tile-preview img {
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
.filearchiv-tile .tile-preview .file-icon {
|
|
font-size: 42px;
|
|
color: var(--colortextlink, #666);
|
|
}
|
|
.filearchiv-tile .tile-name {
|
|
font-size: 11px;
|
|
word-break: break-word;
|
|
line-height: 1.3;
|
|
max-height: 2.6em;
|
|
overflow: hidden;
|
|
}
|
|
.filearchiv-tile .tile-meta {
|
|
font-size: 10px;
|
|
color: var(--colortexttitlenotab, #888);
|
|
margin-top: 3px;
|
|
}
|
|
|
|
/* Hide standard table when tiles active */
|
|
.filearchiv-list-hidden {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Lightbox */
|
|
.filearchiv-lightbox {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0,0,0,0.9);
|
|
z-index: 10000;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
.filearchiv-lightbox.active {
|
|
display: flex;
|
|
}
|
|
.filearchiv-lightbox-content {
|
|
position: relative;
|
|
max-width: 90%;
|
|
max-height: 90%;
|
|
}
|
|
.filearchiv-lightbox-content img {
|
|
max-width: 100%;
|
|
max-height: 85vh;
|
|
object-fit: contain;
|
|
}
|
|
.filearchiv-lightbox-close {
|
|
position: absolute;
|
|
top: -40px;
|
|
right: 0;
|
|
color: #fff;
|
|
font-size: 30px;
|
|
cursor: pointer;
|
|
}
|
|
.filearchiv-lightbox-nav {
|
|
position: absolute;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: #fff;
|
|
font-size: 40px;
|
|
cursor: pointer;
|
|
padding: 20px;
|
|
}
|
|
.filearchiv-lightbox-prev { left: -60px; }
|
|
.filearchiv-lightbox-next { right: -60px; }
|
|
.filearchiv-lightbox-info {
|
|
position: absolute;
|
|
bottom: -35px;
|
|
left: 0;
|
|
right: 0;
|
|
text-align: center;
|
|
color: #fff;
|
|
}
|
|
</style>';
|
|
|
|
// JavaScript - must wait for DOM to find the table
|
|
$out .= '<script>
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
console.log("FileArchiv: DOMContentLoaded - initializing gallery");
|
|
|
|
// Find the file list table
|
|
var fileTable = document.getElementById("tablelines");
|
|
if (!fileTable) {
|
|
console.log("FileArchiv: No #tablelines found");
|
|
return;
|
|
}
|
|
|
|
console.log("FileArchiv: Found table #tablelines");
|
|
|
|
// Check if table has files (more than just header row)
|
|
var rows = fileTable.querySelectorAll("tr");
|
|
if (rows.length < 2) {
|
|
console.log("FileArchiv: Table has no file rows");
|
|
return;
|
|
}
|
|
|
|
// Find the wrapper div
|
|
var tableWrapper = fileTable.closest(".div-table-responsive-no-min") ||
|
|
fileTable.closest(".div-table-responsive") ||
|
|
fileTable.parentElement;
|
|
|
|
// Extract file data from table rows
|
|
var filesData = [];
|
|
var galleryData = [];
|
|
var imageIndex = 0;
|
|
|
|
rows.forEach(function(row, idx) {
|
|
if (idx === 0 || row.classList.contains("liste_titre")) return; // Skip header
|
|
|
|
var link = row.querySelector("a[href*=\'viewimage\'], a[href*=\'document.php\']");
|
|
if (!link) return;
|
|
|
|
var filename = link.textContent.trim() || link.getAttribute("title") || "";
|
|
var href = link.getAttribute("href") || "";
|
|
var isImage = /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(filename);
|
|
|
|
var fileData = {
|
|
name: filename,
|
|
url: href,
|
|
isImage: isImage,
|
|
icon: isImage ? "fa-file-image" : "fa-file"
|
|
};
|
|
|
|
if (isImage) {
|
|
fileData.galleryIndex = imageIndex;
|
|
galleryData.push({
|
|
index: imageIndex,
|
|
name: filename,
|
|
path: href
|
|
});
|
|
imageIndex++;
|
|
}
|
|
|
|
filesData.push(fileData);
|
|
});
|
|
|
|
if (filesData.length === 0) {
|
|
console.log("FileArchiv: No files extracted from table");
|
|
return;
|
|
}
|
|
|
|
console.log("FileArchiv: Found " + filesData.length + " files, " + galleryData.length + " images");
|
|
|
|
// Create toggle buttons
|
|
var toggleHtml = \'<div class="filearchiv-view-toggle-wrapper" style="margin-bottom:8px; text-align:right;">\' +
|
|
\'<span class="filearchiv-view-toggle">\' +
|
|
\'<button type="button" class="btn-list" title="Listenansicht"><i class="fas fa-list"></i></button>\' +
|
|
\'<button type="button" class="btn-tiles" title="Kachelansicht"><i class="fas fa-th"></i></button>\' +
|
|
\'</span></div>\';
|
|
|
|
tableWrapper.insertAdjacentHTML("beforebegin", toggleHtml);
|
|
|
|
// Create tiles container
|
|
var tilesContainer = document.createElement("div");
|
|
tilesContainer.className = "filearchiv-tiles-container";
|
|
tilesContainer.id = "filearchiv-tiles";
|
|
|
|
var tilesHtml = \'<div class="filearchiv-tiles">\';
|
|
filesData.forEach(function(file, idx) {
|
|
var previewHtml = file.isImage
|
|
? \'<img src="\' + file.url + \'" alt="\' + file.name + \'" loading="lazy">\'
|
|
: \'<i class="fas \' + file.icon + \' file-icon"></i>\';
|
|
|
|
tilesHtml += \'<div class="filearchiv-tile" data-index="\' + idx + \'" data-url="\' + file.url + \'" data-isimage="\' + (file.isImage ? "1" : "0") + \'" data-gallery-index="\' + (file.galleryIndex !== undefined ? file.galleryIndex : -1) + \'">\' +
|
|
\'<div class="tile-preview">\' + previewHtml + \'</div>\' +
|
|
\'<div class="tile-name">\' + file.name + \'</div>\' +
|
|
\'</div>\';
|
|
});
|
|
tilesHtml += \'</div>\';
|
|
tilesContainer.innerHTML = tilesHtml;
|
|
|
|
tableWrapper.insertAdjacentElement("afterend", tilesContainer);
|
|
|
|
// Create lightbox
|
|
if (document.getElementById("filearchiv-lightbox")) return; // Already exists
|
|
|
|
var lightboxHtml = \'<div class="filearchiv-lightbox" id="filearchiv-lightbox">\' +
|
|
\'<div class="filearchiv-lightbox-content">\' +
|
|
\'<span class="filearchiv-lightbox-close">×</span>\' +
|
|
\'<span class="filearchiv-lightbox-nav filearchiv-lightbox-prev">‹</span>\' +
|
|
\'<img id="filearchiv-lightbox-img" src="" alt="">\' +
|
|
\'<span class="filearchiv-lightbox-nav filearchiv-lightbox-next">›</span>\' +
|
|
\'<div class="filearchiv-lightbox-info"><span id="filearchiv-lightbox-counter"></span> - <span id="filearchiv-lightbox-name"></span></div>\' +
|
|
\'</div></div>\';
|
|
document.body.insertAdjacentHTML("beforeend", lightboxHtml);
|
|
|
|
var lightbox = document.getElementById("filearchiv-lightbox");
|
|
var lightboxImg = document.getElementById("filearchiv-lightbox-img");
|
|
var currentIndex = 0;
|
|
var currentView = localStorage.getItem("filearchiv_view") || "list";
|
|
|
|
window.FileArchivGallery = {
|
|
open: function(index) {
|
|
if (galleryData.length === 0) return;
|
|
currentIndex = index;
|
|
this.updateImage();
|
|
lightbox.classList.add("active");
|
|
document.body.style.overflow = "hidden";
|
|
},
|
|
close: function() {
|
|
lightbox.classList.remove("active");
|
|
document.body.style.overflow = "";
|
|
},
|
|
prev: function() {
|
|
currentIndex = (currentIndex - 1 + galleryData.length) % galleryData.length;
|
|
this.updateImage();
|
|
},
|
|
next: function() {
|
|
currentIndex = (currentIndex + 1) % galleryData.length;
|
|
this.updateImage();
|
|
},
|
|
updateImage: function() {
|
|
var item = galleryData[currentIndex];
|
|
lightboxImg.src = item.path;
|
|
document.getElementById("filearchiv-lightbox-counter").textContent = (currentIndex + 1) + " / " + galleryData.length;
|
|
document.getElementById("filearchiv-lightbox-name").textContent = item.name;
|
|
}
|
|
};
|
|
|
|
// Lightbox events
|
|
lightbox.querySelector(".filearchiv-lightbox-close").onclick = function() { FileArchivGallery.close(); };
|
|
lightbox.querySelector(".filearchiv-lightbox-prev").onclick = function() { FileArchivGallery.prev(); };
|
|
lightbox.querySelector(".filearchiv-lightbox-next").onclick = function() { FileArchivGallery.next(); };
|
|
lightbox.onclick = function(e) { if (e.target === lightbox) FileArchivGallery.close(); };
|
|
|
|
document.addEventListener("keydown", function(e) {
|
|
if (!lightbox.classList.contains("active")) return;
|
|
if (e.key === "Escape") FileArchivGallery.close();
|
|
if (e.key === "ArrowLeft") FileArchivGallery.prev();
|
|
if (e.key === "ArrowRight") FileArchivGallery.next();
|
|
});
|
|
|
|
// View toggle
|
|
function setView(view) {
|
|
currentView = view;
|
|
localStorage.setItem("filearchiv_view", view);
|
|
|
|
var btnList = document.querySelector(".filearchiv-view-toggle .btn-list");
|
|
var btnTiles = document.querySelector(".filearchiv-view-toggle .btn-tiles");
|
|
|
|
if (view === "tiles") {
|
|
btnList.classList.remove("active");
|
|
btnTiles.classList.add("active");
|
|
tableWrapper.classList.add("filearchiv-list-hidden");
|
|
tilesContainer.classList.add("active");
|
|
} else {
|
|
btnList.classList.add("active");
|
|
btnTiles.classList.remove("active");
|
|
tableWrapper.classList.remove("filearchiv-list-hidden");
|
|
tilesContainer.classList.remove("active");
|
|
}
|
|
}
|
|
|
|
document.querySelector(".filearchiv-view-toggle .btn-list").onclick = function() { setView("list"); };
|
|
document.querySelector(".filearchiv-view-toggle .btn-tiles").onclick = function() { setView("tiles"); };
|
|
|
|
// Tile click events
|
|
tilesContainer.querySelectorAll(".filearchiv-tile").forEach(function(tile) {
|
|
tile.onclick = function() {
|
|
var isImage = tile.dataset.isimage === "1";
|
|
var galleryIndex = parseInt(tile.dataset.galleryIndex);
|
|
if (isImage && galleryIndex >= 0) {
|
|
FileArchivGallery.open(galleryIndex);
|
|
} else {
|
|
window.open(tile.dataset.url, "_blank");
|
|
}
|
|
};
|
|
});
|
|
|
|
// Initialize view
|
|
setView(currentView);
|
|
console.log("FileArchiv: Gallery initialized");
|
|
});
|
|
</script>';
|
|
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* Generate inline CSS and JS assets with toggle for tile view
|
|
*
|
|
* @param array $filearray Array of files
|
|
* @param array $parameters Hook parameters
|
|
* @return string HTML with inline CSS and JS
|
|
*/
|
|
private function getInlineAssets($filearray, $parameters)
|
|
{
|
|
global $conf, $langs;
|
|
|
|
$langs->load('filearchiv@filearchiv');
|
|
|
|
$modulepart = isset($parameters['modulepart']) ? $parameters['modulepart'] : '';
|
|
|
|
// Build file data for all files
|
|
$filesData = array();
|
|
$galleryData = array();
|
|
$imageIndex = 0;
|
|
|
|
foreach ($filearray as $file) {
|
|
$isImage = $this->isImageFile($file['name']);
|
|
$isPdf = $this->isPdfFile($file['name']);
|
|
|
|
$fileData = array(
|
|
'name' => $file['name'],
|
|
'size' => isset($file['size']) ? dol_print_size($file['size']) : '',
|
|
'date' => isset($file['date']) ? dol_print_date($file['date'], 'day') : '',
|
|
'url' => $this->getFileUrl($file, $modulepart),
|
|
'isImage' => $isImage,
|
|
'isPdf' => $isPdf,
|
|
'icon' => $this->getFileIcon($file['name']),
|
|
);
|
|
|
|
// Add thumbnail for images
|
|
if ($isImage) {
|
|
$fileData['thumb'] = $this->getThumbUrl($file, $modulepart);
|
|
$fileData['galleryIndex'] = $imageIndex;
|
|
$galleryData[] = array(
|
|
'index' => $imageIndex,
|
|
'name' => $file['name'],
|
|
'path' => $this->getFileUrl($file, $modulepart),
|
|
);
|
|
$imageIndex++;
|
|
}
|
|
|
|
$filesData[] = $fileData;
|
|
}
|
|
|
|
$out = '';
|
|
|
|
// CSS
|
|
$out .= '<style id="filearchiv-styles">
|
|
/* FileArchiv View Toggle */
|
|
.filearchiv-view-toggle {
|
|
display: inline-flex;
|
|
gap: 2px;
|
|
margin-left: 10px;
|
|
vertical-align: middle;
|
|
}
|
|
.filearchiv-view-toggle button {
|
|
padding: 4px 8px;
|
|
border: 1px solid var(--colorborder, #ccc);
|
|
background: var(--colorbackbody, #fff);
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
color: var(--colortextlink, #333);
|
|
}
|
|
.filearchiv-view-toggle button:first-child {
|
|
border-radius: 3px 0 0 3px;
|
|
}
|
|
.filearchiv-view-toggle button:last-child {
|
|
border-radius: 0 3px 3px 0;
|
|
}
|
|
.filearchiv-view-toggle button.active {
|
|
background: var(--colortextlink, #0077b6);
|
|
color: #fff;
|
|
border-color: var(--colortextlink, #0077b6);
|
|
}
|
|
.filearchiv-view-toggle button:hover:not(.active) {
|
|
background: var(--colorbacklinepairhover, #f0f0f0);
|
|
}
|
|
|
|
/* Tile/Grid View */
|
|
.filearchiv-tiles-container {
|
|
display: none;
|
|
margin-top: 10px;
|
|
}
|
|
.filearchiv-tiles-container.active {
|
|
display: block;
|
|
}
|
|
.filearchiv-tiles {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
gap: 12px;
|
|
padding: 10px 0;
|
|
}
|
|
.filearchiv-tile {
|
|
border: 1px solid var(--colorborder, #ddd);
|
|
border-radius: 4px;
|
|
padding: 8px;
|
|
text-align: center;
|
|
background: var(--colorbackbody, #fff);
|
|
transition: box-shadow 0.2s, border-color 0.2s;
|
|
cursor: pointer;
|
|
position: relative;
|
|
}
|
|
.filearchiv-tile:hover {
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.12);
|
|
border-color: var(--colortextlink, #0077b6);
|
|
}
|
|
.filearchiv-tile .tile-preview {
|
|
width: 100%;
|
|
height: 100px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
overflow: hidden;
|
|
margin-bottom: 6px;
|
|
background: var(--colorbacklinepair1, #fafafa);
|
|
border-radius: 3px;
|
|
}
|
|
.filearchiv-tile .tile-preview img {
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
.filearchiv-tile .tile-preview .file-icon {
|
|
font-size: 42px;
|
|
color: var(--colortextlink, #666);
|
|
}
|
|
.filearchiv-tile .tile-preview .file-icon.fa-file-pdf {
|
|
color: #dc3545;
|
|
}
|
|
.filearchiv-tile .tile-preview .file-icon.fa-file-word {
|
|
color: #2b579a;
|
|
}
|
|
.filearchiv-tile .tile-preview .file-icon.fa-file-excel {
|
|
color: #217346;
|
|
}
|
|
.filearchiv-tile .tile-preview .file-icon.fa-file-image {
|
|
color: #6c5ce7;
|
|
}
|
|
.filearchiv-tile .tile-name {
|
|
font-size: 11px;
|
|
word-break: break-word;
|
|
color: var(--colortexttitle, #333);
|
|
line-height: 1.3;
|
|
max-height: 2.6em;
|
|
overflow: hidden;
|
|
}
|
|
.filearchiv-tile .tile-meta {
|
|
font-size: 10px;
|
|
color: var(--colortexttitlenotab, #888);
|
|
margin-top: 3px;
|
|
}
|
|
|
|
/* Hide standard table when tiles active */
|
|
.filearchiv-list-hidden {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Lightbox */
|
|
.filearchiv-lightbox {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0,0,0,0.9);
|
|
z-index: 10000;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
.filearchiv-lightbox.active {
|
|
display: flex;
|
|
}
|
|
.filearchiv-lightbox-content {
|
|
position: relative;
|
|
max-width: 90%;
|
|
max-height: 90%;
|
|
}
|
|
.filearchiv-lightbox-content img {
|
|
max-width: 100%;
|
|
max-height: 85vh;
|
|
object-fit: contain;
|
|
}
|
|
.filearchiv-lightbox-close {
|
|
position: absolute;
|
|
top: -40px;
|
|
right: 0;
|
|
color: #fff;
|
|
font-size: 30px;
|
|
cursor: pointer;
|
|
padding: 5px 10px;
|
|
}
|
|
.filearchiv-lightbox-nav {
|
|
position: absolute;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: #fff;
|
|
font-size: 40px;
|
|
cursor: pointer;
|
|
padding: 20px;
|
|
user-select: none;
|
|
}
|
|
.filearchiv-lightbox-nav:hover {
|
|
color: #0077b6;
|
|
}
|
|
.filearchiv-lightbox-prev { left: -60px; }
|
|
.filearchiv-lightbox-next { right: -60px; }
|
|
.filearchiv-lightbox-info {
|
|
position: absolute;
|
|
bottom: -35px;
|
|
left: 0;
|
|
right: 0;
|
|
text-align: center;
|
|
color: #fff;
|
|
font-size: 14px;
|
|
}
|
|
.filearchiv-lightbox-zoom {
|
|
position: absolute;
|
|
bottom: -35px;
|
|
right: 0;
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
.filearchiv-lightbox-zoom button {
|
|
background: rgba(255,255,255,0.2);
|
|
border: none;
|
|
color: #fff;
|
|
padding: 5px 10px;
|
|
cursor: pointer;
|
|
border-radius: 3px;
|
|
}
|
|
.filearchiv-lightbox-zoom button:hover {
|
|
background: rgba(255,255,255,0.4);
|
|
}
|
|
</style>';
|
|
|
|
// JavaScript
|
|
$filesJson = json_encode($filesData);
|
|
$galleryJson = json_encode($galleryData);
|
|
|
|
$out .= '<script>
|
|
(function() {
|
|
"use strict";
|
|
console.log("FileArchiv: Script loaded");
|
|
|
|
var filesData = ' . $filesJson . ';
|
|
var galleryData = ' . $galleryJson . ';
|
|
var currentView = localStorage.getItem("filearchiv_view") || "list";
|
|
var currentIndex = 0;
|
|
var currentZoom = 1;
|
|
|
|
// Find the file list table - look for #tablelines or table.liste inside div-table-responsive
|
|
var fileTable = document.getElementById("tablelines") ||
|
|
document.querySelector(".div-table-responsive-no-min table.liste") ||
|
|
document.querySelector(".div-table-responsive table.liste") ||
|
|
document.querySelector("table.liste.formdoc");
|
|
|
|
console.log("FileArchiv: Looking for table, found:", fileTable);
|
|
|
|
if (!fileTable) {
|
|
console.log("FileArchiv: No file table found - will retry on DOMContentLoaded");
|
|
document.addEventListener("DOMContentLoaded", initFileArchiv);
|
|
return;
|
|
}
|
|
|
|
initFileArchiv();
|
|
|
|
function initFileArchiv() {
|
|
fileTable = document.getElementById("tablelines") ||
|
|
document.querySelector(".div-table-responsive-no-min table.liste") ||
|
|
document.querySelector(".div-table-responsive table.liste") ||
|
|
document.querySelector("table.liste.formdoc");
|
|
|
|
if (!fileTable) {
|
|
console.log("FileArchiv: Still no file table found");
|
|
return;
|
|
}
|
|
|
|
console.log("FileArchiv: Found file table", fileTable.id || fileTable.className);
|
|
|
|
// Find the wrapper div
|
|
var tableWrapper = fileTable.closest(".div-table-responsive-no-min") ||
|
|
fileTable.closest(".div-table-responsive") ||
|
|
fileTable.parentElement;
|
|
|
|
// Create toggle buttons
|
|
var toggleHtml = \'<div class="filearchiv-view-toggle-wrapper" style="margin-bottom:8px; text-align:right;">\' +
|
|
\'<span class="filearchiv-view-toggle">\' +
|
|
\'<button type="button" class="btn-list" title="Listenansicht"><i class="fas fa-list"></i></button>\' +
|
|
\'<button type="button" class="btn-tiles" title="Kachelansicht"><i class="fas fa-th"></i></button>\' +
|
|
\'</span></div>\';
|
|
|
|
// Insert toggle before the table wrapper
|
|
tableWrapper.insertAdjacentHTML("beforebegin", toggleHtml);
|
|
console.log("FileArchiv: Toggle buttons inserted");
|
|
|
|
// Create tiles container
|
|
var tilesContainer = document.createElement("div");
|
|
tilesContainer.className = "filearchiv-tiles-container";
|
|
tilesContainer.id = "filearchiv-tiles";
|
|
|
|
var tilesHtml = \'<div class="filearchiv-tiles">\';
|
|
filesData.forEach(function(file, idx) {
|
|
var previewHtml = "";
|
|
if (file.isImage && file.thumb) {
|
|
previewHtml = \'<img src="\' + file.thumb + \'" alt="\' + file.name + \'" loading="lazy">\';
|
|
} else {
|
|
previewHtml = \'<i class="fas \' + file.icon + \' file-icon"></i>\';
|
|
}
|
|
|
|
tilesHtml += \'<div class="filearchiv-tile" data-index="\' + idx + \'" data-url="\' + file.url + \'" data-isimage="\' + (file.isImage ? "1" : "0") + \'" data-gallery-index="\' + (file.galleryIndex !== undefined ? file.galleryIndex : -1) + \'">\' +
|
|
\'<div class="tile-preview">\' + previewHtml + \'</div>\' +
|
|
\'<div class="tile-name">\' + file.name + \'</div>\' +
|
|
\'<div class="tile-meta">\' + file.size + \'</div>\' +
|
|
\'</div>\';
|
|
});
|
|
tilesHtml += \'</div>\';
|
|
tilesContainer.innerHTML = tilesHtml;
|
|
|
|
// Insert tiles container after table wrapper
|
|
tableWrapper.insertAdjacentElement("afterend", tilesContainer);
|
|
console.log("FileArchiv: Tiles container inserted");
|
|
|
|
// Create lightbox
|
|
var lightboxHtml = \'<div class="filearchiv-lightbox" id="filearchiv-lightbox">\' +
|
|
\'<div class="filearchiv-lightbox-content">\' +
|
|
\'<span class="filearchiv-lightbox-close">×</span>\' +
|
|
\'<span class="filearchiv-lightbox-nav filearchiv-lightbox-prev">‹</span>\' +
|
|
\'<img id="filearchiv-lightbox-img" src="" alt="">\' +
|
|
\'<span class="filearchiv-lightbox-nav filearchiv-lightbox-next">›</span>\' +
|
|
\'<div class="filearchiv-lightbox-info"><span id="filearchiv-lightbox-counter"></span> - <span id="filearchiv-lightbox-name"></span></div>\' +
|
|
\'<div class="filearchiv-lightbox-zoom">\' +
|
|
\'<button type="button" onclick="FileArchivGallery.zoomOut()">-</button>\' +
|
|
\'<button type="button" onclick="FileArchivGallery.zoomReset()">100%</button>\' +
|
|
\'<button type="button" onclick="FileArchivGallery.zoomIn()">+</button>\' +
|
|
\'</div>\' +
|
|
\'</div>\' +
|
|
\'</div>\';
|
|
document.body.insertAdjacentHTML("beforeend", lightboxHtml);
|
|
|
|
var lightbox = document.getElementById("filearchiv-lightbox");
|
|
var lightboxImg = document.getElementById("filearchiv-lightbox-img");
|
|
var lightboxCounter = document.getElementById("filearchiv-lightbox-counter");
|
|
var lightboxName = document.getElementById("filearchiv-lightbox-name");
|
|
|
|
// Gallery functions
|
|
window.FileArchivGallery = {
|
|
open: function(index) {
|
|
if (galleryData.length === 0) return;
|
|
currentIndex = index;
|
|
currentZoom = 1;
|
|
this.updateImage();
|
|
lightbox.classList.add("active");
|
|
document.body.style.overflow = "hidden";
|
|
},
|
|
close: function() {
|
|
lightbox.classList.remove("active");
|
|
document.body.style.overflow = "";
|
|
},
|
|
prev: function() {
|
|
currentIndex = (currentIndex - 1 + galleryData.length) % galleryData.length;
|
|
currentZoom = 1;
|
|
this.updateImage();
|
|
},
|
|
next: function() {
|
|
currentIndex = (currentIndex + 1) % galleryData.length;
|
|
currentZoom = 1;
|
|
this.updateImage();
|
|
},
|
|
updateImage: function() {
|
|
var item = galleryData[currentIndex];
|
|
lightboxImg.src = item.path;
|
|
lightboxImg.style.transform = "scale(" + currentZoom + ")";
|
|
lightboxCounter.textContent = (currentIndex + 1) + " / " + galleryData.length;
|
|
lightboxName.textContent = item.name;
|
|
},
|
|
zoomIn: function() {
|
|
currentZoom = Math.min(currentZoom + 0.25, 3);
|
|
lightboxImg.style.transform = "scale(" + currentZoom + ")";
|
|
},
|
|
zoomOut: function() {
|
|
currentZoom = Math.max(currentZoom - 0.25, 0.5);
|
|
lightboxImg.style.transform = "scale(" + currentZoom + ")";
|
|
},
|
|
zoomReset: function() {
|
|
currentZoom = 1;
|
|
lightboxImg.style.transform = "scale(1)";
|
|
}
|
|
};
|
|
|
|
// Lightbox event listeners
|
|
lightbox.querySelector(".filearchiv-lightbox-close").addEventListener("click", function() {
|
|
FileArchivGallery.close();
|
|
});
|
|
lightbox.querySelector(".filearchiv-lightbox-prev").addEventListener("click", function() {
|
|
FileArchivGallery.prev();
|
|
});
|
|
lightbox.querySelector(".filearchiv-lightbox-next").addEventListener("click", function() {
|
|
FileArchivGallery.next();
|
|
});
|
|
lightbox.addEventListener("click", function(e) {
|
|
if (e.target === lightbox) FileArchivGallery.close();
|
|
});
|
|
|
|
// Keyboard navigation
|
|
document.addEventListener("keydown", function(e) {
|
|
if (!lightbox.classList.contains("active")) return;
|
|
if (e.key === "Escape") FileArchivGallery.close();
|
|
if (e.key === "ArrowLeft") FileArchivGallery.prev();
|
|
if (e.key === "ArrowRight") FileArchivGallery.next();
|
|
});
|
|
|
|
// View toggle functions
|
|
function setView(view) {
|
|
currentView = view;
|
|
localStorage.setItem("filearchiv_view", view);
|
|
|
|
var btnList = document.querySelector(".filearchiv-view-toggle .btn-list");
|
|
var btnTiles = document.querySelector(".filearchiv-view-toggle .btn-tiles");
|
|
|
|
if (view === "tiles") {
|
|
btnList?.classList.remove("active");
|
|
btnTiles?.classList.add("active");
|
|
tableWrapper.classList.add("filearchiv-list-hidden");
|
|
tilesContainer.classList.add("active");
|
|
} else {
|
|
btnList?.classList.add("active");
|
|
btnTiles?.classList.remove("active");
|
|
tableWrapper.classList.remove("filearchiv-list-hidden");
|
|
tilesContainer.classList.remove("active");
|
|
}
|
|
}
|
|
|
|
// Toggle button events
|
|
document.querySelector(".filearchiv-view-toggle .btn-list")?.addEventListener("click", function() {
|
|
setView("list");
|
|
});
|
|
document.querySelector(".filearchiv-view-toggle .btn-tiles")?.addEventListener("click", function() {
|
|
setView("tiles");
|
|
});
|
|
|
|
// Tile click events
|
|
tilesContainer.querySelectorAll(".filearchiv-tile").forEach(function(tile) {
|
|
tile.addEventListener("click", function() {
|
|
var isImage = tile.dataset.isimage === "1";
|
|
var galleryIndex = parseInt(tile.dataset.galleryIndex);
|
|
var url = tile.dataset.url;
|
|
|
|
if (isImage && galleryIndex >= 0) {
|
|
FileArchivGallery.open(galleryIndex);
|
|
} else {
|
|
window.open(url, "_blank");
|
|
}
|
|
});
|
|
});
|
|
|
|
// Initialize view
|
|
setView(currentView);
|
|
} // end initFileArchiv
|
|
})();
|
|
</script>';
|
|
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* Get Font Awesome icon class for file type
|
|
*
|
|
* @param string $filename Filename
|
|
* @return string Icon class
|
|
*/
|
|
private function getFileIcon($filename)
|
|
{
|
|
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
|
|
|
$icons = array(
|
|
'pdf' => 'fa-file-pdf',
|
|
'doc' => 'fa-file-word',
|
|
'docx' => 'fa-file-word',
|
|
'xls' => 'fa-file-excel',
|
|
'xlsx' => 'fa-file-excel',
|
|
'ppt' => 'fa-file-powerpoint',
|
|
'pptx' => 'fa-file-powerpoint',
|
|
'jpg' => 'fa-file-image',
|
|
'jpeg' => 'fa-file-image',
|
|
'png' => 'fa-file-image',
|
|
'gif' => 'fa-file-image',
|
|
'webp' => 'fa-file-image',
|
|
'bmp' => 'fa-file-image',
|
|
'svg' => 'fa-file-image',
|
|
'zip' => 'fa-file-archive',
|
|
'rar' => 'fa-file-archive',
|
|
'7z' => 'fa-file-archive',
|
|
'tar' => 'fa-file-archive',
|
|
'gz' => 'fa-file-archive',
|
|
'txt' => 'fa-file-alt',
|
|
'csv' => 'fa-file-csv',
|
|
'xml' => 'fa-file-code',
|
|
'json' => 'fa-file-code',
|
|
'html' => 'fa-file-code',
|
|
'htm' => 'fa-file-code',
|
|
'mp3' => 'fa-file-audio',
|
|
'wav' => 'fa-file-audio',
|
|
'mp4' => 'fa-file-video',
|
|
'avi' => 'fa-file-video',
|
|
'mov' => 'fa-file-video',
|
|
);
|
|
|
|
return isset($icons[$ext]) ? $icons[$ext] : 'fa-file';
|
|
}
|
|
|
|
/**
|
|
* Get full URL for a file
|
|
*
|
|
* @param array $file File array
|
|
* @param string $modulepart Module part
|
|
* @return string URL
|
|
*/
|
|
private function getFileUrl($file, $modulepart)
|
|
{
|
|
global $conf;
|
|
|
|
$relativepath = isset($file['relativename']) ? $file['relativename'] : $file['name'];
|
|
return DOL_URL_ROOT . '/document.php?modulepart=' . urlencode($modulepart) . '&file=' . urlencode($relativepath);
|
|
}
|
|
|
|
/**
|
|
* Get thumbnail URL for a file
|
|
*
|
|
* @param array $file File array
|
|
* @param string $modulepart Module part
|
|
* @return string Thumbnail URL
|
|
*/
|
|
private function getThumbUrl($file, $modulepart)
|
|
{
|
|
// For now, use the same as file URL
|
|
// Could be enhanced to use actual thumbnails
|
|
return $this->getFileUrl($file, $modulepart);
|
|
}
|
|
|
|
/**
|
|
* Check if file is an image
|
|
*
|
|
* @param string $filename Filename
|
|
* @return bool
|
|
*/
|
|
private function isImageFile($filename)
|
|
{
|
|
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
|
return in_array($ext, array('jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'));
|
|
}
|
|
|
|
/**
|
|
* Check if file is a PDF
|
|
*
|
|
* @param string $filename Filename
|
|
* @return bool
|
|
*/
|
|
private function isPdfFile($filename)
|
|
{
|
|
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
|
return $ext === 'pdf';
|
|
}
|
|
|
|
/**
|
|
* Check if file is pinned
|
|
*
|
|
* @param array $file File array
|
|
* @return bool
|
|
*/
|
|
private function isFilePinned($file)
|
|
{
|
|
// Will be implemented with database table
|
|
// For now return false
|
|
$filepath = isset($file['fullname']) ? $file['fullname'] : '';
|
|
if (empty($filepath)) {
|
|
return false;
|
|
}
|
|
|
|
$sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "filearchiv_pinned";
|
|
$sql .= " WHERE filepath = '" . $this->db->escape($filepath) . "'";
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql && $this->db->num_rows($resql) > 0) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Toggle pin status for a file
|
|
*
|
|
* @param string $filepath Full file path
|
|
* @param string $modulepart Module part
|
|
* @return bool Success
|
|
*/
|
|
public function togglePin($filepath, $modulepart)
|
|
{
|
|
global $user;
|
|
|
|
if ($this->isFilePinnedByPath($filepath)) {
|
|
// Unpin
|
|
$sql = "DELETE FROM " . MAIN_DB_PREFIX . "filearchiv_pinned";
|
|
$sql .= " WHERE filepath = '" . $this->db->escape($filepath) . "'";
|
|
} else {
|
|
// Pin
|
|
$sql = "INSERT INTO " . MAIN_DB_PREFIX . "filearchiv_pinned";
|
|
$sql .= " (filepath, modulepart, fk_user, datec)";
|
|
$sql .= " VALUES ('" . $this->db->escape($filepath) . "',";
|
|
$sql .= " '" . $this->db->escape($modulepart) . "',";
|
|
$sql .= " " . ((int) $user->id) . ",";
|
|
$sql .= " '" . $this->db->idate(dol_now()) . "')";
|
|
}
|
|
|
|
$resql = $this->db->query($sql);
|
|
return $resql ? true : false;
|
|
}
|
|
|
|
/**
|
|
* Check if file is pinned by path
|
|
*
|
|
* @param string $filepath File path
|
|
* @return bool
|
|
*/
|
|
private function isFilePinnedByPath($filepath)
|
|
{
|
|
$sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "filearchiv_pinned";
|
|
$sql .= " WHERE filepath = '" . $this->db->escape($filepath) . "'";
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql && $this->db->num_rows($resql) > 0) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Hook called when FormMail generates the email form
|
|
* This hook is called with initHooks(['formmail']) context
|
|
*
|
|
* @param array $parameters Hook parameters (addfileaction, removefileaction, trackid)
|
|
* @param FormMail $object FormMail object
|
|
* @param string $action Current action
|
|
* @param HookManager $hookmanager Hook manager
|
|
* @return int 0 = keep standard behavior (add resPrint to output), >0 = replace entire form
|
|
*/
|
|
public function getFormMail($parameters, &$object, &$action, $hookmanager)
|
|
{
|
|
global $conf, $langs;
|
|
|
|
// DEBUG - ALWAYS write first
|
|
@file_put_contents('/tmp/filearchiv_debug.log', date('Y-m-d H:i:s') . " === getFormMail ENTRY ===\n", FILE_APPEND);
|
|
|
|
// Get action from URL
|
|
$currentAction = GETPOST('action', 'aZ09');
|
|
$trackid = isset($parameters['trackid']) ? $parameters['trackid'] : '';
|
|
$id = GETPOST('id', 'int');
|
|
|
|
// DEBUG - detailed info
|
|
@file_put_contents('/tmp/filearchiv_debug.log', date('Y-m-d H:i:s') . " getFormMail called, action=$currentAction, trackid=$trackid, id=$id, PHP_SELF=" . $_SERVER['PHP_SELF'] . "\n", FILE_APPEND);
|
|
|
|
if (!isModEnabled('filearchiv')) {
|
|
file_put_contents('/tmp/filearchiv_debug.log', date('Y-m-d H:i:s') . " -> module not enabled\n", FILE_APPEND);
|
|
return 0;
|
|
}
|
|
|
|
// Only show on presend action
|
|
if ($currentAction != 'presend') {
|
|
file_put_contents('/tmp/filearchiv_debug.log', date('Y-m-d H:i:s') . " -> action is not presend, skipping\n", FILE_APPEND);
|
|
return 0;
|
|
}
|
|
|
|
// Get element and ID from URL/POST
|
|
$element = GETPOST('element', 'aZ09');
|
|
$id = GETPOST('id', 'int');
|
|
|
|
// Try to determine element from trackid pattern
|
|
$trackid = isset($parameters['trackid']) ? $parameters['trackid'] : '';
|
|
if (empty($element) && !empty($trackid)) {
|
|
// Trackid format: pro123 (propal), ord123 (order), inv123 (invoice), shi123 (shipping)
|
|
$patterns = array(
|
|
'pro' => 'propal',
|
|
'ord' => 'commande',
|
|
'inv' => 'facture',
|
|
'shi' => 'expedition',
|
|
'sor' => 'order_supplier',
|
|
'sin' => 'invoice_supplier',
|
|
);
|
|
foreach ($patterns as $prefix => $elemType) {
|
|
if (strpos($trackid, $prefix) === 0) {
|
|
$element = $elemType;
|
|
$id = (int) substr($trackid, strlen($prefix));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try to get from the current page URL
|
|
if (empty($element)) {
|
|
$script = basename($_SERVER['PHP_SELF']);
|
|
$pageMap = array(
|
|
'card.php' => array(
|
|
'/comm/propal/' => 'propal',
|
|
'/commande/' => 'commande',
|
|
'/compta/facture/' => 'facture',
|
|
'/expedition/' => 'expedition',
|
|
),
|
|
);
|
|
if (isset($pageMap[$script])) {
|
|
foreach ($pageMap[$script] as $path => $elemType) {
|
|
if (strpos($_SERVER['PHP_SELF'], $path) !== false) {
|
|
$element = $elemType;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (empty($element) || empty($id)) {
|
|
return 0;
|
|
}
|
|
|
|
// Load translations
|
|
$langs->load('filearchiv@filearchiv');
|
|
|
|
// Build the document browser button and modal HTML
|
|
$out = $this->getDocumentBrowserHTML($element, $id, $trackid);
|
|
|
|
$this->resprints = $out;
|
|
return 0; // Return 0 to add our HTML without replacing the form
|
|
}
|
|
|
|
/**
|
|
* Hook called on formConfirm - adds document browser for presend action
|
|
* This hook is called early on the page and allows adding HTML that will be output
|
|
*
|
|
* @param array $parameters Hook parameters
|
|
* @param CommonObject $object Current object
|
|
* @param string $action Current action
|
|
* @param HookManager $hookmanager Hook manager
|
|
* @return int 0 = keep standard behavior (add resPrint), >0 = replace
|
|
*/
|
|
/**
|
|
* Hook called at the beginning of each page (doActions)
|
|
* Used to inject document browser HTML on presend pages
|
|
*
|
|
* @param array $parameters Hook parameters
|
|
* @param CommonObject $object Current object
|
|
* @param string $action Current action
|
|
* @param HookManager $hookmanager Hook manager
|
|
* @return int 0 = keep standard behavior
|
|
*/
|
|
public function doActions($parameters, &$object, &$action, $hookmanager)
|
|
{
|
|
global $conf, $langs;
|
|
|
|
// Debug log
|
|
@file_put_contents('/tmp/filearchiv_debug.log', date('Y-m-d H:i:s') . " doActions called, action=" . GETPOST('action', 'aZ09') . "\n", FILE_APPEND);
|
|
|
|
return 0;
|
|
}
|
|
|
|
public function formConfirm($parameters, &$object, &$action, $hookmanager)
|
|
{
|
|
global $conf, $langs;
|
|
|
|
// Always write to debug log to confirm hook is being called
|
|
@file_put_contents('/tmp/filearchiv_debug.log', date('Y-m-d H:i:s') . " formConfirm called, action=" . GETPOST('action', 'aZ09') . "\n", FILE_APPEND);
|
|
|
|
if (!isModEnabled('filearchiv')) {
|
|
return 0;
|
|
}
|
|
|
|
// Only show on presend action
|
|
$currentAction = GETPOST('action', 'aZ09');
|
|
|
|
if ($currentAction != 'presend') {
|
|
return 0;
|
|
}
|
|
|
|
// Get current object info
|
|
$element = '';
|
|
$id = 0;
|
|
|
|
if (is_object($object) && !empty($object->element)) {
|
|
$element = $object->element;
|
|
$id = $object->id;
|
|
}
|
|
|
|
@file_put_contents('/tmp/filearchiv_debug.log', date('Y-m-d H:i:s') . " formConfirm presend: element=$element, id=$id\n", FILE_APPEND);
|
|
|
|
if (empty($element) || empty($id)) {
|
|
return 0;
|
|
}
|
|
|
|
// Check if this element type is supported
|
|
$supportedElements = array('propal', 'commande', 'facture', 'expedition', 'order_supplier', 'invoice_supplier');
|
|
if (!in_array($element, $supportedElements)) {
|
|
return 0;
|
|
}
|
|
|
|
// Load translations
|
|
$langs->load('filearchiv@filearchiv');
|
|
|
|
// Build the document browser button and modal HTML
|
|
$out = $this->getDocumentBrowserHTML($element, $id, '');
|
|
|
|
@file_put_contents('/tmp/filearchiv_debug.log', date('Y-m-d H:i:s') . " formConfirm injecting HTML, length=" . strlen($out) . "\n", FILE_APPEND);
|
|
|
|
$this->resprints = $out;
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Hook called after action buttons are displayed
|
|
* This hook is reliably called on all card pages
|
|
*
|
|
* @param array $parameters Hook parameters
|
|
* @param CommonObject $object Current object
|
|
* @param string $action Current action
|
|
* @param HookManager $hookmanager Hook manager
|
|
* @return int 0 = keep standard behavior
|
|
*/
|
|
public function addMoreActionsButtons($parameters, &$object, &$action, $hookmanager)
|
|
{
|
|
global $conf, $langs;
|
|
|
|
if (!isModEnabled('filearchiv')) {
|
|
return 0;
|
|
}
|
|
|
|
// Only show on presend action
|
|
$currentAction = GETPOST('action', 'aZ09');
|
|
if ($currentAction != 'presend') {
|
|
return 0;
|
|
}
|
|
|
|
// Get current object info
|
|
$element = '';
|
|
$id = 0;
|
|
|
|
if (is_object($object) && !empty($object->element)) {
|
|
$element = $object->element;
|
|
$id = $object->id;
|
|
}
|
|
|
|
if (empty($element) || empty($id)) {
|
|
return 0;
|
|
}
|
|
|
|
// Check if this element type is supported
|
|
$supportedElements = array('propal', 'commande', 'facture', 'expedition', 'order_supplier', 'invoice_supplier');
|
|
if (!in_array($element, $supportedElements)) {
|
|
return 0;
|
|
}
|
|
|
|
// Load translations
|
|
$langs->load('filearchiv@filearchiv');
|
|
|
|
// Build the document browser button and modal HTML
|
|
$this->resprints = $this->getDocumentBrowserHTML($element, $id, '');
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Generate HTML for document browser button and modal
|
|
*
|
|
* @param string $element Element type (facture, propal, commande, etc.)
|
|
* @param int $id Element ID
|
|
* @param string $trackid Email tracking ID
|
|
* @return string HTML output
|
|
*/
|
|
private function getDocumentBrowserHTML($element, $id, $trackid)
|
|
{
|
|
global $conf, $langs;
|
|
|
|
// Use custom module path for AJAX URLs
|
|
$ajaxUrl = dol_buildpath('/custom/filearchiv/ajax/getdocuments.php', 1);
|
|
$addFileUrl = dol_buildpath('/custom/filearchiv/ajax/addfile.php', 1);
|
|
|
|
// Debug: Log the URLs
|
|
@file_put_contents('/tmp/filearchiv_debug.log', date('Y-m-d H:i:s') . " ajaxUrl=$ajaxUrl, addFileUrl=$addFileUrl\n", FILE_APPEND);
|
|
|
|
$out = '';
|
|
|
|
// CSS for document browser - using Dolibarr native styles where possible
|
|
$out .= '
|
|
<style id="filearchiv-docbrowser-styles">
|
|
/* Document Browser Modal - Dolibarr style */
|
|
.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: var(--colorbackbody, #fff);
|
|
border-radius: 3px;
|
|
width: 90%;
|
|
max-width: 800px;
|
|
max-height: 80vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
|
border: 1px solid var(--colorborder, #bbb);
|
|
}
|
|
.filearchiv-modal-header {
|
|
padding: 10px 15px;
|
|
border-bottom: 1px solid var(--colorborder, #bbb);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
background: var(--colorbacktitle1, #f0f0f0);
|
|
}
|
|
.filearchiv-modal-header h3 {
|
|
margin: 0;
|
|
font-size: 14px;
|
|
font-weight: normal;
|
|
color: var(--colortexttitle, #333);
|
|
}
|
|
.filearchiv-modal-close {
|
|
font-size: 20px;
|
|
cursor: pointer;
|
|
color: var(--colortextlink, #333);
|
|
line-height: 1;
|
|
padding: 0 5px;
|
|
}
|
|
.filearchiv-modal-close:hover {
|
|
color: var(--colortextbackhmenu, #000);
|
|
}
|
|
.filearchiv-modal-body {
|
|
padding: 15px;
|
|
overflow-y: auto;
|
|
flex: 1;
|
|
background: var(--colorbackbody, #fff);
|
|
}
|
|
.filearchiv-modal-footer {
|
|
padding: 10px 15px;
|
|
border-top: 1px solid var(--colorborder, #bbb);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
background: var(--colorbacktitle1, #f0f0f0);
|
|
gap: 10px;
|
|
}
|
|
.filearchiv-category {
|
|
margin-bottom: 10px;
|
|
border: 1px solid var(--colorborder, #ccc);
|
|
border-radius: 3px;
|
|
}
|
|
.filearchiv-category-header {
|
|
padding: 8px 12px;
|
|
background: var(--colorbacktitle1, #f8f8f8);
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-weight: 600;
|
|
color: var(--colortexttitle, #333);
|
|
font-size: 13px;
|
|
}
|
|
.filearchiv-category-header:hover {
|
|
background: var(--colorbacklinepairhover, #eee);
|
|
}
|
|
.filearchiv-category-header i {
|
|
width: 16px;
|
|
text-align: center;
|
|
color: var(--colortextlink, #444);
|
|
}
|
|
.filearchiv-category-header .count {
|
|
margin-left: auto;
|
|
background: var(--colortextlink, #444);
|
|
color: #fff;
|
|
padding: 1px 6px;
|
|
border-radius: 8px;
|
|
font-size: 11px;
|
|
font-weight: normal;
|
|
}
|
|
.filearchiv-category-header .toggle-icon {
|
|
margin-left: 5px;
|
|
transition: transform 0.2s;
|
|
}
|
|
.filearchiv-category-body {
|
|
display: none;
|
|
padding: 8px 12px;
|
|
background: var(--colorbackbody, #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: 6px 8px;
|
|
border-bottom: 1px solid var(--colorbacklinebreak, #f0f0f0);
|
|
gap: 8px;
|
|
}
|
|
.filearchiv-file-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.filearchiv-file-item:hover {
|
|
background: var(--colorbacklinepairhover, #f5f5f5);
|
|
}
|
|
.filearchiv-file-item input[type="checkbox"] {
|
|
cursor: pointer;
|
|
}
|
|
.filearchiv-file-item .file-icon {
|
|
font-size: 16px;
|
|
width: 20px;
|
|
text-align: center;
|
|
color: var(--colortextlink, #444);
|
|
}
|
|
.filearchiv-file-item .file-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.filearchiv-file-item .file-name {
|
|
font-size: 13px;
|
|
color: var(--colortextlink, #333);
|
|
word-break: break-word;
|
|
}
|
|
.filearchiv-file-item .file-meta {
|
|
font-size: 11px;
|
|
color: var(--colortexttitlenotab, #888);
|
|
margin-top: 2px;
|
|
}
|
|
.filearchiv-file-item .file-source {
|
|
font-size: 10px;
|
|
color: var(--colortextlink, #444);
|
|
background: var(--colorbacklinepair1, #f0f0f0);
|
|
padding: 1px 5px;
|
|
border-radius: 2px;
|
|
margin-top: 2px;
|
|
display: inline-block;
|
|
}
|
|
.filearchiv-selected-count {
|
|
font-size: 13px;
|
|
color: var(--colortexttitle, #333);
|
|
}
|
|
.filearchiv-loading {
|
|
text-align: center;
|
|
padding: 30px;
|
|
color: var(--colortexttitlenotab, #666);
|
|
}
|
|
.filearchiv-loading i {
|
|
font-size: 24px;
|
|
margin-bottom: 8px;
|
|
display: block;
|
|
}
|
|
.filearchiv-loading i.fa-spinner {
|
|
animation: filearchiv-spin 1s linear infinite;
|
|
}
|
|
@keyframes filearchiv-spin {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
.filearchiv-empty {
|
|
text-align: center;
|
|
padding: 20px;
|
|
color: var(--colortexttitlenotab, #888);
|
|
font-size: 13px;
|
|
}
|
|
.filearchiv-browse-btn {
|
|
margin-left: 5px;
|
|
}
|
|
</style>';
|
|
|
|
// Button to open document browser (hidden initially, will be moved by JS)
|
|
$out .= '
|
|
<div id="filearchiv-browse-container" style="display: none; margin-top: 5px;">
|
|
<input type="button" class="button smallpaddingimp filearchiv-browse-btn" onclick="FileArchivDocBrowser.open()" value="' . dol_escape_htmltag($langs->trans('BrowseRelatedDocuments')) . '">
|
|
</div>';
|
|
|
|
// Modal HTML
|
|
$out .= '
|
|
<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 paddingright"></i>' . $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>' . $langs->trans('Loading') . '...</div>
|
|
</div>
|
|
</div>
|
|
<div class="filearchiv-modal-footer">
|
|
<span class="filearchiv-selected-count">
|
|
<span id="filearchiv-selected-num">0</span> ' . $langs->trans('FilesSelected') . '
|
|
</span>
|
|
<div style="display: flex; gap: 5px;">
|
|
<input type="button" class="button button-cancel" onclick="FileArchivDocBrowser.close()" value="' . dol_escape_htmltag($langs->trans('Cancel')) . '">
|
|
<input type="button" class="button button-add" id="filearchiv-add-btn" onclick="FileArchivDocBrowser.addSelected()" disabled value="' . dol_escape_htmltag($langs->trans('AddSelectedFiles')) . '">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>';
|
|
|
|
// JavaScript
|
|
$out .= '
|
|
<script>
|
|
var FileArchivDocBrowser = {
|
|
element: "' . dol_escape_js($element) . '",
|
|
id: ' . ((int) $id) . ',
|
|
trackid: "' . dol_escape_js($trackid) . '",
|
|
ajaxUrl: "' . dol_escape_js($ajaxUrl) . '",
|
|
addFileUrl: "' . dol_escape_js($addFileUrl) . '",
|
|
token: "' . 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;
|
|
console.log("FileArchiv: Loading documents from URL:", url);
|
|
console.log("FileArchiv: element=" + this.element + ", id=" + this.id);
|
|
|
|
fetch(url)
|
|
.then(function(response) {
|
|
console.log("FileArchiv: Response status:", response.status);
|
|
return response.text();
|
|
})
|
|
.then(function(text) {
|
|
console.log("FileArchiv: Response text:", text.substring(0, 500));
|
|
try {
|
|
var data = JSON.parse(text);
|
|
self.loaded = true;
|
|
self.renderDocuments(data);
|
|
} catch (e) {
|
|
console.error("FileArchiv: JSON parse error:", e);
|
|
document.getElementById("filearchiv-docbrowser-content").innerHTML =
|
|
\'<div class="filearchiv-empty"><i class="fas fa-exclamation-triangle"></i> Response was not JSON</div>\';
|
|
}
|
|
})
|
|
.catch(function(error) {
|
|
console.error("FileArchiv: Fetch error:", error);
|
|
document.getElementById("filearchiv-docbrowser-content").innerHTML =
|
|
\'<div class="filearchiv-empty"><i class="fas fa-exclamation-triangle"></i> ' . $langs->trans('ErrorLoadingDocuments') . '</div>\';
|
|
});
|
|
},
|
|
|
|
renderDocuments: function(data) {
|
|
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>' . $langs->trans('NoRelatedDocumentsFound') . '</div>\';
|
|
return;
|
|
}
|
|
|
|
var html = "";
|
|
data.categories.forEach(function(category, catIndex) {
|
|
html += \'<div class="filearchiv-category" 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;
|
|
|
|
// Open first category by default
|
|
if (data.categories.length > 0) {
|
|
document.getElementById("cat-0").classList.add("open");
|
|
}
|
|
},
|
|
|
|
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.value = "' . dol_escape_js($langs->trans('Adding')) . '...";
|
|
|
|
// Add files one by one
|
|
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(responses) {
|
|
console.log("FileArchiv: All files added, responses:", responses);
|
|
// Parse all responses
|
|
return Promise.all(responses.map(function(r) { return r.json(); }));
|
|
})
|
|
.then(function(results) {
|
|
console.log("FileArchiv: Results:", results);
|
|
// Check if all succeeded
|
|
var allSuccess = results.every(function(r) { return r.success; });
|
|
if (allSuccess) {
|
|
console.log("FileArchiv: All files added successfully, reloading page...");
|
|
// Reload page WITHOUT mode=init (which clears attachments)
|
|
var url = new URL(window.location.href);
|
|
url.searchParams.delete("mode");
|
|
window.location.href = url.toString();
|
|
} else {
|
|
var errors = results.filter(function(r) { return !r.success; }).map(function(r) { return r.error; });
|
|
console.error("FileArchiv: Some files failed:", errors);
|
|
alert("' . dol_escape_js($langs->trans('ErrorAddingFiles')) . ': " + errors.join(", "));
|
|
addBtn.disabled = false;
|
|
addBtn.value = "' . dol_escape_js($langs->trans('AddSelectedFiles')) . '";
|
|
}
|
|
})
|
|
.catch(function(error) {
|
|
console.error("FileArchiv: Error adding files:", error);
|
|
addBtn.disabled = false;
|
|
addBtn.value = "' . dol_escape_js($langs->trans('AddSelectedFiles')) . '";
|
|
alert("' . dol_escape_js($langs->trans('ErrorAddingFiles')) . '");
|
|
});
|
|
}
|
|
};
|
|
|
|
var self = FileArchivDocBrowser;
|
|
|
|
// Move button to correct position after DOM ready and get trackid from form
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
console.log("FileArchiv: DOMContentLoaded triggered");
|
|
var container = document.getElementById("filearchiv-browse-container");
|
|
if (!container) {
|
|
console.log("FileArchiv: Container not found");
|
|
return;
|
|
}
|
|
console.log("FileArchiv: Container found");
|
|
|
|
// Try to find the file attachment section in the email form
|
|
// Try multiple selectors for different Dolibarr versions
|
|
var attachSection = document.querySelector("input[name=\\"addedfile\\"]") ||
|
|
document.querySelector("input[name=\\"addedfile[]\\"]") ||
|
|
document.querySelector("#addedfile") ||
|
|
document.querySelector("input[type=\\"file\\"]");
|
|
|
|
console.log("FileArchiv: attachSection =", attachSection);
|
|
|
|
if (attachSection) {
|
|
var parent = attachSection.closest("td") || attachSection.closest("tr") || attachSection.parentElement;
|
|
console.log("FileArchiv: parent =", parent);
|
|
if (parent) {
|
|
parent.appendChild(container);
|
|
container.style.display = "block";
|
|
console.log("FileArchiv: Button moved and displayed");
|
|
}
|
|
} else {
|
|
// Fallback: Try to find the mail form and add before submit button
|
|
var mailForm = document.getElementById("mailform");
|
|
if (mailForm) {
|
|
var submitBtn = mailForm.querySelector("input[type=\\"submit\\"]");
|
|
if (submitBtn && submitBtn.parentElement) {
|
|
submitBtn.parentElement.insertBefore(container, submitBtn);
|
|
container.style.display = "block";
|
|
console.log("FileArchiv: Button added before submit (fallback)");
|
|
}
|
|
} else {
|
|
// Last resort: just show it where it is
|
|
container.style.display = "block";
|
|
console.log("FileArchiv: Button displayed in place (last resort)");
|
|
}
|
|
}
|
|
|
|
// Get trackid from hidden input in form
|
|
var trackidInput = document.querySelector("input[name=\\"trackid\\"]");
|
|
if (trackidInput) {
|
|
FileArchivDocBrowser.trackid = trackidInput.value;
|
|
console.log("FileArchiv: trackid =", trackidInput.value);
|
|
}
|
|
});
|
|
|
|
// Close modal on Escape
|
|
document.addEventListener("keydown", function(e) {
|
|
if (e.key === "Escape" && document.getElementById("filearchiv-docbrowser-modal").classList.contains("active")) {
|
|
FileArchivDocBrowser.close();
|
|
}
|
|
});
|
|
</script>';
|
|
|
|
return $out;
|
|
}
|
|
}
|