added file uploads
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user