added file uploads

This commit is contained in:
2025-10-24 09:30:48 +02:00
parent 0131f472f0
commit 6af9d8dc32
5 changed files with 654 additions and 30 deletions
+461 -30
View File
@@ -100,53 +100,484 @@ $pagination_entity_name = "Vorbestellungen";
}
}
/* styles for documents */
.document-upload-wrapper {
background: #fdfdfd;
border: 1px solid #e9ecef;
border-radius: .25rem;
}
.document-dropzone {
border: 2px dashed #ced4da;
border-radius: 0.25rem;
padding: 1.5rem;
text-align: center;
background-color: #f8f9fa;
transition: all 0.3s ease;
cursor: pointer;
}
.document-dropzone:hover {
border-color: #007bff;
background-color: #e9ecef;
}
.document-dropzone.active {
border-color: #007bff;
border-style: solid;
}
.document-staging-area {
max-height: 250px;
overflow-y: auto;
}
.document-staging-item {
display: flex;
align-items: flex-start;
padding: 0.75rem;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
margin-bottom: 0.5rem;
background-color: #fff;
}
.doc-staging-icon {
flex-shrink: 0;
width: 30px;
text-align: center;
padding-top: 0.25rem;
}
.doc-staging-details {
flex-grow: 1;
padding: 0 0.5rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.doc-staging-filename {
font-size: 0.9em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.doc-staging-filesize {
font-size: 0.8em;
}
.doc-staging-actions {
flex-shrink: 0;
}
.document-list-wrapper {
min-height: 300px;
}
.doc-spinner {
display: inline-block;
width: 3rem;
height: 3rem;
vertical-align: text-bottom;
border: 0.25em solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: doc-spin 0.75s linear infinite;
color: #007bff;
}
@keyframes doc-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.doc-row-icon i { font-size: 1.5rem; }
.doc-row-icon .fa-file-pdf { color: #dc3545; }
.doc-row-icon .fa-file-image { color: #28a745; }
.doc-row-icon .fa-file-word { color: #007bff; }
.doc-row-icon .fa-file-excel { color: #207245; }
.doc-row-icon .fa-file-archive { color: #ffc107; }
.doc-row-icon .fa-file { color: #6c757d; }
.doc-preview-modal-body img,
.doc-preview-modal-body embed,
.doc-preview-modal-body iframe {
max-width: 100%;
max-height: 75vh;
border: none;
}
</style>
<script>
$(document).ready(function () {
if (window.matchMedia('(min-width: 576px)').matches) {
return;
}
<script>
$(function() {
if (window.matchMedia('(min-width: 576px)').matches) return;
// in ul.pagination
// if .page-item.disabled.text-secondary exists
// only keep the first 2 .page-item, the last 2 .page-item and the .page-item.disabled.text-secondary and the one before and the one after that
const $pagination = $("ul.pagination");
const $disabled = $pagination.find(".page-item.disabled.points");
const pagination = $("ul.pagination");
const disabled = pagination.find(".page-item.disabled.points");
if ($disabled.length) {
const $first = $pagination.find(".page-item").first();
const $firstNext = $first.next();
const $last = $pagination.find(".page-item").last();
const $lastPrev = $last.prev();
const $prev = $disabled.prev();
const $next = $disabled.next();
if (disabled.length) {
const first = pagination.find(".page-item").first();
const firstNext = first.next();
const last = pagination.find(".page-item").last();
const lastNext = last.prev();
const prev = disabled.prev();
const next = disabled.next();
const notToDelete = [first, firstNext, last, lastNext, prev, next, disabled];
const $keep = $().add($first).add($firstNext).add($last).add($lastPrev).add($prev).add($next).add($disabled);
// loop through pagination.find(".page-item") and remove all but the first, last, prev, next, firstNext, lastNext
$pagination.find(".page-item").not($keep).remove();
}
});
pagination.find(".page-item").each(function (index, item) {
// if (!notToDelete.includes($(item))) {
// $(item).remove();
// } fix this becaues of we need [0] of notToDelete to compare
let check = false;
notToDelete.forEach(function (n) {
if (n[0] === item) {
check = true;
$(function() {
function initPreorderDocumentTabs() {
$('.preorder-documents-tab').each(function() {
const tabPane = $(this);
const preorderId = tabPane.data('preorder-id');
if (!preorderId) return;
const dropzone = tabPane.find('.doc-dropzone');
const browseBtn = tabPane.find('.doc-browse-btn');
const fileInput = tabPane.find('.doc-file-input');
const stagingArea = tabPane.find('.doc-staging-area');
const stagingTemplate = tabPane.find('.doc-staging-item-template')[0];
const uploadBtn = tabPane.find('.doc-upload-btn');
const uploadBtnText = tabPane.find('.doc-upload-btn-text');
const uploadSpinner = tabPane.find('.doc-upload-spinner');
const listLoader = tabPane.find('.document-list-loader');
const emptyState = tabPane.find('.document-empty-state');
const listContainer = tabPane.find('.document-list-container');
const listTbody = tabPane.find('.doc-list-tbody');
const listRowTemplate = tabPane.find('.doc-list-row-template')[0];
const modal = tabPane.find('.doc-preview-modal');
const modalTitle = modal.find('.doc-preview-modal-title');
const modalBody = modal.find('.doc-preview-modal-body');
let stagedFiles = new Map();
// --- START FIX 1: Helper function to decode HTML entities ---
function htmlDecode(input) {
if (!input || typeof input !== 'string') return input;
var doc = new DOMParser().parseFromString(input, "text/html");
return doc.documentElement.textContent;
}
// --- END FIX 1 ---
function formatFileSize(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
function guessMimeType(filename) {
const ext = (filename || '').split('.').pop().toLowerCase();
const mimeTypes = {
'pdf': 'application/pdf',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'png': 'image/png',
'gif': 'image/gif',
'doc': 'application/msword',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xls': 'application/vnd.ms-excel',
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'zip': 'application/zip',
};
return mimeTypes[ext] || 'application/octet-stream';
}
function getFileIcon(mimeType) {
if (!mimeType) return '<i class="fas fa-file fa-fw"></i>';
const iconMap = {
'image/': 'fa-file-image',
'application/pdf': 'fa-file-pdf',
'word': 'fa-file-word',
'excel': 'fa-file-excel',
'spreadsheet': 'fa-file-excel',
'zip': 'fa-file-archive',
'archive': 'fa-file-archive'
};
for (const key in iconMap) {
if (mimeType.includes(key)) return `<i class="fas ${iconMap[key]} fa-fw"></i>`;
}
return '<i class="fas fa-file fa-fw"></i>';
}
function isPreviewable(mimeType) {
return mimeType.startsWith('image/') || mimeType === 'application/pdf';
}
async function fetchFileMetaData(fileId, description) {
try {
const response = await fetch(`/File/getById?id=${fileId}`);
if (!response.ok) return null;
const data = await response.json();
const mimetype = guessMimeType(data.filename);
return {
id: fileId,
fileId: fileId,
fileName: data.filename,
description: description,
mimetype: mimetype
};
} catch (error) {
return null;
}
}
async function loadDocuments() {
listLoader.show();
listContainer.hide();
emptyState.hide();
listTbody.empty();
let fileObjects = [];
try {
const rawData = tabPane.data('file-objects');
if (typeof rawData === 'string') {
const decodedData = htmlDecode(rawData);
fileObjects = JSON.parse(decodedData);
} else if (Array.isArray(rawData)) {
fileObjects = rawData;
}
} catch (e) {
console.error("Error parsing file objects:", e, tabPane.data('file-objects'));
fileObjects = [];
}
if (fileObjects.length === 0) {
emptyState.show();
listLoader.hide();
return;
}
try {
const fetchPromises = fileObjects.map(obj => fetchFileMetaData(obj.id, obj.description));
const documents = (await Promise.all(fetchPromises)).filter(doc => doc !== null);
if (documents.length === 0) {
emptyState.show();
} else {
documents.forEach(addDocumentRow);
listContainer.show();
}
} catch (error) {
emptyState.find('h5').text('Fehler beim Laden');
emptyState.find('p').text('Die Dokumente konnten nicht geladen werden.');
emptyState.show();
} finally {
listLoader.hide();
}
}
function addDocumentRow(docData, prepend = false) {
const rowClone = listRowTemplate.content.cloneNode(true);
const $row = $(rowClone).find('tr');
$row.attr('data-doc-id', docData.id);
$row.find('.doc-row-icon').html(getFileIcon(docData.mimetype));
$row.find('.doc-row-filename').text(docData.fileName);
$row.find('.doc-row-description').text(docData.description || '');
const previewUrl = `/File/show?id=${docData.fileId}`;
const downloadUrl = `/File/download?id=${docData.fileId}`;
$row.find('.doc-preview-btn').data({
url: previewUrl,
type: docData.mimetype,
filename: docData.fileName
});
$row.find('.doc-download-btn').attr({
href: downloadUrl,
download: docData.fileName
});
if (!isPreviewable(docData.mimetype)) {
$row.find('.doc-preview-btn').addClass('disabled').attr('title', 'Vorschau nicht verfügbar');
}
prepend ? listTbody.prepend($row) : listTbody.append($row);
emptyState.hide();
listContainer.show();
}
tabPane.data('loadDocumentsFunction', loadDocuments);
function handleFiles(files) {
for (const file of files) {
const fileId = `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
stagedFiles.set(fileId, file);
const itemClone = stagingTemplate.content.cloneNode(true);
const item = $(itemClone).find('.document-staging-item');
item.attr('data-file-id', fileId);
item.find('.doc-staging-filename').text(file.name);
item.find('.doc-staging-filesize').text(formatFileSize(file.size));
item.find('.doc-staging-icon').html(getFileIcon(file.type));
stagingArea.append(item).show();
}
updateUploadButton();
}
function updateUploadButton() {
if (stagedFiles.size > 0) {
uploadBtnText.text(`${stagedFiles.size} Datei(en) hochladen`);
uploadBtn.show();
} else {
uploadBtn.hide();
stagingArea.hide();
}
}
dropzone.on('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
dropzone.addClass('active');
});
dropzone.on('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
dropzone.removeClass('active');
});
dropzone.on('drop', (e) => {
e.preventDefault();
e.stopPropagation();
dropzone.removeClass('active');
handleFiles(e.originalEvent.dataTransfer.files);
});
// --- START FIX 2: Modify click handlers ---
browseBtn.on('click', (e) => {
e.preventDefault();
e.stopPropagation(); // Stop this click from bubbling up to the dropzone
fileInput.click();
});
dropzone.on('click', function(e) {
// If the click target was the button or the file input, do nothing.
if ($(e.target).closest('.doc-browse-btn').length > 0 || $(e.target).hasClass('doc-file-input')) {
return;
}
// Otherwise, trigger the file input click.
fileInput.click();
});
// --- END FIX 2 ---
fileInput.on('change', (e) => {
handleFiles(e.target.files);
fileInput.val(null);
});
stagingArea.on('click', '.doc-staging-remove', function() {
const item = $(this).closest('.document-staging-item');
const fileId = item.data('file-id');
stagedFiles.delete(fileId);
item.fadeOut(300, function() {
$(this).remove();
updateUploadButton();
});
});
uploadBtn.on('click', async function() {
let allValid = true;
const formData = new FormData();
formData.append('preorderId', preorderId);
stagingArea.find('.document-staging-item').each(function() {
const item = $(this);
const fileId = item.data('file-id');
const file = stagedFiles.get(fileId);
const descriptionInput = item.find('.doc-staging-description');
const description = descriptionInput.val().trim();
if (!description) {
descriptionInput.addClass('is-invalid');
allValid = false;
} else {
descriptionInput.removeClass('is-invalid');
formData.append('files[]', file, file.name);
formData.append('descriptions[]', description);
}
});
if (!check) {
$(item).remove();
if (!allValid) {
if (window.notify) window.notify('error', 'Bitte füllen Sie alle Beschreibungsfelder aus.');
return;
}
uploadBtn.prop('disabled', true);
uploadSpinner.show();
try {
const response = await fetch('/Preorder/uploadDocuments', {
method: 'POST',
body: formData
});
const result = await response.json();
if (response.ok && result.success) {
if (window.notify) window.notify('success', result.message || 'Dateien erfolgreich hochgeladen.');
stagedFiles.clear();
stagingArea.empty().hide();
updateUploadButton();
if (result.fileObjects) tabPane.data('file-objects', result.fileObjects);
loadDocuments();
} else {
throw new Error(result.message || 'Unbekannter Fehler beim Upload.');
}
} catch (err) {
if (window.notify) window.notify('error', `Upload fehlgeschlagen: ${err.message}`);
} finally {
uploadBtn.prop('disabled', false);
uploadSpinner.hide();
}
});
listTbody.on('click', '.doc-preview-btn', function() {
if ($(this).hasClass('disabled')) return;
const url = $(this).data('url');
const type = $(this).data('type');
const filename = $(this).data('filename');
modalTitle.text(filename);
modalBody.empty();
if (type.startsWith('image/')) {
modalBody.html(`<img src="${url}" class="img-fluid" alt="Vorschau von ${filename}">`);
} else if (type === 'application/pdf') {
window.open(url, '_blank');
return;
} else {
modalBody.html(`<div class="text-center p-5"><i class="fas fa-file-excel fa-3x text-muted mb-3"></i><p>Keine Vorschau für den Dateityp "${type}" verfügbar.</p></div>`);
}
modal.modal('show');
});
});
}
$('a[data-toggle="tab"]').on('shown.bs.tab', function(e) {
const targetId = $(e.target).attr('href');
if (!targetId || !targetId.endsWith('-documents')) return;
const tabPane = $(targetId);
if (tabPane.hasClass('preorder-documents-tab') && tabPane.data('loaded') === false) {
const loaderFunc = tabPane.data('loadDocumentsFunction');
if (typeof loaderFunc === 'function') {
loaderFunc();
tabPane.data('loaded', true);
}
}
});
</script>
initPreorderDocumentTabs();
});
</script>
<!-- start page title -->
<div class="row">