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) {
|
||||
<script>
|
||||
$(function() {
|
||||
if (window.matchMedia('(min-width: 576px)').matches) return;
|
||||
|
||||
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();
|
||||
|
||||
const $keep = $().add($first).add($firstNext).add($last).add($lastPrev).add($prev).add($next).add($disabled);
|
||||
|
||||
$pagination.find(".page-item").not($keep).remove();
|
||||
}
|
||||
});
|
||||
|
||||
$(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;
|
||||
}
|
||||
|
||||
// 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
|
||||
try {
|
||||
const fetchPromises = fileObjects.map(obj => fetchFileMetaData(obj.id, obj.description));
|
||||
const documents = (await Promise.all(fetchPromises)).filter(doc => doc !== null);
|
||||
|
||||
const pagination = $("ul.pagination");
|
||||
const disabled = pagination.find(".page-item.disabled.points");
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
function addDocumentRow(docData, prepend = false) {
|
||||
const rowClone = listRowTemplate.content.cloneNode(true);
|
||||
const $row = $(rowClone).find('tr');
|
||||
|
||||
// loop through pagination.find(".page-item") and remove all but the first, last, prev, next, firstNext, lastNext
|
||||
$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 || '');
|
||||
|
||||
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;
|
||||
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();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
initPreorderDocumentTabs();
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- start page title -->
|
||||
<div class="row">
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="#preorder-detail-<?=$preorder->id?>-history" data-toggle="tab" aria-expanded="false">History</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#preorder-detail-<?=$preorder->id?>-emails" data-toggle="tab" aria-expanded="false">E-Mails</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#preorder-detail-<?=$preorder->id?>-documents" data-toggle="tab" aria-expanded="false">Dokumente</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -1162,6 +1163,125 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="preorder-detail-<?=$preorder->id?>-documents"
|
||||
class="tab-pane preorder-documents-tab"
|
||||
data-preorder-id="<?=$preorder->id?>"
|
||||
data-file-objects="<?=htmlentities($preorder->files ?? '[]')?>"
|
||||
data-loaded="false">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-lg-8">
|
||||
<div class="card-header bg-info text-white pl-2 pr-2 pt-1 pb-1">Hochgeladene Dokumente</div>
|
||||
<div class="document-list-wrapper">
|
||||
<div class="document-list-loader text-center p-5" style="display: none;">
|
||||
<div class="doc-spinner"></div>
|
||||
<p class="mt-3 text-muted">Lade Dokumente...</p>
|
||||
</div>
|
||||
|
||||
<div class="document-empty-state text-center p-5" style="display: none;">
|
||||
<i class="fas fa-file-excel fa-3x text-muted mb-3"></i>
|
||||
<h5>Keine Dokumente gefunden</h5>
|
||||
<p class="text-muted">Für diese Bestellung wurden noch keine Dokumente hochgeladen.</p>
|
||||
</div>
|
||||
|
||||
<div class="document-list-container table-responsive" style="display: none;">
|
||||
<table class="table table-sm table-striped table-hover">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th style="width: 5%;">Typ</th>
|
||||
<th>Dateiname</th>
|
||||
<th>Beschreibung</th>
|
||||
<th style="width: 15%;" class="text-right">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="doc-list-tbody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4 mt-3 mt-lg-0">
|
||||
<div class="card-header bg-secondary text-white pl-2 pr-2 pt-1 pb-1">Neuer Upload</div>
|
||||
<div class="document-upload-wrapper p-3">
|
||||
<div class="doc-dropzone document-dropzone">
|
||||
<div class="document-dropzone-internal">
|
||||
<i class="fas fa-cloud-upload-alt fa-2x text-muted mb-2"></i>
|
||||
<p class="mb-2">Dateien hier ablegen oder</p>
|
||||
<button type="button" class="btn btn-primary btn-sm doc-browse-btn">Datei auswählen</button>
|
||||
<input type="file" class="doc-file-input" multiple style="display: none;" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="doc-staging-area document-staging-area mt-3" style="display: none;">
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success btn-block mt-2 doc-upload-btn" style="display: none;">
|
||||
<i class="fas fa-fw fa-upload"></i> <span class="doc-upload-btn-text">Hochladen</span>
|
||||
<i class="fas fa-spinner fa-spin ml-1 doc-upload-spinner" style="display: none;"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template class="doc-staging-item-template">
|
||||
<div class="document-staging-item">
|
||||
<div class="doc-staging-icon">
|
||||
<i class="fas fa-file-alt text-muted"></i>
|
||||
</div>
|
||||
<div class="doc-staging-details">
|
||||
<div class="doc-staging-filename">filename.jpg</div>
|
||||
<div class="doc-staging-filesize text-muted small">1.2 MB</div>
|
||||
<input type="text" class="form-control form-control-sm mt-1 doc-staging-description" placeholder="Beschreibung (erforderlich)" required />
|
||||
<div class="invalid-feedback">Beschreibung fehlt.</div>
|
||||
</div>
|
||||
<div class="doc-staging-actions">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger doc-staging-remove" title="Entfernen">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template class="doc-list-row-template">
|
||||
<tr class="document-list-row">
|
||||
<td class="doc-row-icon text-center align-middle">
|
||||
</td>
|
||||
<td class="doc-row-filename align-middle">filename.pdf</td>
|
||||
<td class="doc-row-description align-middle small">Beschreibung...</td>
|
||||
<td class="doc-row-actions text-right align-middle">
|
||||
<button type="button" class="btn btn-sm btn-outline-info doc-preview-btn" title="Vorschau">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
<a href="#" class="btn btn-sm btn-outline-secondary doc-download-btn" title="Download" download>
|
||||
<i class="fas fa-download"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<div class="modal fade doc-preview-modal" id="doc-preview-modal-<?=$preorder->id?>" tabindex="-1" role="dialog" aria-labelledby="doc-preview-modal-title-<?=$preorder->id?>" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title doc-preview-modal-title" id="doc-preview-modal-title-<?=$preorder->id?>">Vorschau</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body doc-preview-modal-body" id="doc-preview-modal-body-<?=$preorder->id?>" style="min-height: 70vh; display: flex; justify-content: center; align-items: center;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,7 @@ class FileController extends mfBaseController {
|
||||
}
|
||||
|
||||
if(preg_match('/\.([^.]+)/',$filename,$m)) {
|
||||
if (!isset($ext)) $ext = '';
|
||||
$ext .= $m[1];
|
||||
} else {
|
||||
throw new Exception("File not found", 4042);
|
||||
|
||||
@@ -1985,4 +1985,56 @@ class PreorderController extends mfBaseController {
|
||||
unlink($filename);
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function uploadDocumentsAction() {
|
||||
if (empty($_FILES['files']) || empty($_POST['preorderId']) || empty($_POST['descriptions'])) {
|
||||
self::sendError('Erforderliche Daten fehlen (Dateien, preorderId oder Beschreibungen).');
|
||||
}
|
||||
|
||||
$preorderId = $_POST['preorderId'];
|
||||
$descriptions = $_POST['descriptions'];
|
||||
|
||||
if (count($_FILES['files']['name']) !== count($descriptions)) {
|
||||
self::sendError('Anzahl der Dateien und Beschreibungen stimmt nicht überein.');
|
||||
}
|
||||
|
||||
$preorder = new Preorder($preorderId);
|
||||
if (!$preorder->id) {
|
||||
self::sendError('Bestellung nicht gefunden.');
|
||||
}
|
||||
|
||||
$fileObjects = json_decode($preorder->files, true);
|
||||
if (!is_array($fileObjects)) $fileObjects = [];
|
||||
|
||||
foreach ($_FILES['files']['name'] as $index => $name) {
|
||||
if ($_FILES['files']['error'][$index] !== UPLOAD_ERR_OK) continue;
|
||||
|
||||
$_FILES['file'] = [
|
||||
'name' => $name,
|
||||
'type' => $_FILES['files']['type'][$index],
|
||||
'tmp_name' => $_FILES['files']['tmp_name'][$index],
|
||||
'error' => $_FILES['files']['error'][$index],
|
||||
'size' => $_FILES['files']['size'][$index]
|
||||
];
|
||||
|
||||
$description = trim($descriptions[$index]);
|
||||
if (empty($description)) continue;
|
||||
|
||||
try {
|
||||
$uploaded = mfUpload::handleFormUpload("file", false, "/PreorderDocuments");
|
||||
$fileObjects[] = ["id" => $uploaded->id, "description" => $description];
|
||||
} catch (Exception $e) {}
|
||||
}
|
||||
|
||||
$db = FronkDB::singleton();
|
||||
$escapedFilesJson = $db->escape(json_encode($fileObjects));
|
||||
$sql = "UPDATE `" . FRONKDB_DBNAME . "`.Preorder SET files = '$escapedFilesJson' WHERE id = " . (int)$preorder->id;
|
||||
$db->query($sql);
|
||||
|
||||
self::returnJson([
|
||||
'success' => true,
|
||||
'message' => "Datei(en) erfolgreich hochgeladen.",
|
||||
'fileObjects' => $fileObjects
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
20
db/migrations/20251024091253_preorder_add_files.php
Normal file
20
db/migrations/20251024091253_preorder_add_files.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class PreorderAddFiles extends AbstractMigration {
|
||||
public function up(): void {
|
||||
if($this->getEnvironment() == "thetool") {
|
||||
$Preorder = $this->table("Preorder");
|
||||
$Preorder->addColumn("files", "text", ['null' => true, 'after' => 'notes']);
|
||||
$Preorder->update();
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void {
|
||||
if($this->getEnvironment() == "thetool") {
|
||||
$this->table("Preorder")->removeColumn("files")->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user