Compare commits

...

27 Commits

Author SHA1 Message Date
Aiirondev_dev f0b5edff79 fix of the json upload format 2026-08-04 21:11:13 +02:00
Aiirondev_dev 5b95c5202e fix of the json upload format 2026-08-04 21:03:08 +02:00
Aiirondev_dev 51c17d11f2 fix of the json upload format 2026-08-04 20:54:03 +02:00
Aiirondev_dev 91ddc6e864 fix of the json upload format 2026-08-04 20:44:15 +02:00
Aiirondev_dev 88f124c991 fix of the json upload format 2026-08-04 20:42:06 +02:00
Aiirondev_dev 33c65f21b6 fix of the json upload format 2026-08-03 23:55:30 +02:00
Aiirondev_dev fd242a6a0a fix of the json upload format 2026-08-03 23:45:59 +02:00
Aiirondev_dev 2892024969 fix of the json upload format 2026-08-03 23:26:51 +02:00
Aiirondev_dev 27f5280bbf fix of the json upload format 2026-08-03 19:36:56 +02:00
Aiirondev_dev 82898e34cb fix of the json upload format 2026-08-03 19:16:32 +02:00
Aiirondev_dev 44392c2c31 fix of the json upload format 2026-08-03 18:51:47 +02:00
Aiirondev_dev 58d94b716f fix of the json upload format 2026-08-03 18:34:27 +02:00
Aiirondev_dev 579a0ddb75 fix of the json upload format 2026-08-03 18:22:27 +02:00
Aiirondev_dev 0f21e8d9ca fix of the json upload format 2026-08-03 01:02:40 +02:00
Aiirondev_dev 12f7240cd2 fix of the json upload format 2026-08-03 00:52:39 +02:00
Aiirondev_dev 54d8d61358 fix of the json upload format 2026-08-03 00:28:53 +02:00
Aiirondev_dev 5052dd9de6 fix of the json upload format 2026-08-03 00:20:53 +02:00
Aiirondev_dev bf31ee2d16 feat: add batch CSV and image upload logic
- Created '/upload_csv_batch' endpoint to handle CSV parsing and multiple image uploads
- Added automatic WebP conversion and GridFS storage for batch images
- Implemented grouping logic via 'series_group_id' based on item name
- Created '/batch_upload' route and 'upload_batch.html' for a seamless async frontend
2026-08-03 00:06:48 +02:00
Aiirondev_dev b847930500 fix of the username decryption for the logs 2026-08-02 21:32:26 +02:00
Aiirondev_dev 756ff55b4c Chaces to the Deleted Status, to reflekt the real active bookings in the menu 2026-08-02 15:14:46 +02:00
Aiirondev_dev df5a3265a1 fix of the file upload in the edet_item funktions 2026-08-01 18:35:28 +02:00
Aiirondev_dev 2c6da44af8 fix of the file upload 2026-08-01 18:02:01 +02:00
Aiirondev_dev d6d6858002 new implementation of file uploading 2026-08-01 10:34:59 +02:00
Aiirondev_dev 0cf0bd8dec new implementation of file uploading 2026-08-01 00:00:18 +02:00
Aiirondev_dev af9826547c Incorperation of the right fallback path for the image folders 2026-07-31 22:43:48 +02:00
Aiirondev_dev 87027cf3c9 Changes to the damaged Items 2026-07-31 22:12:19 +02:00
Aiirondev_dev 46f79b002b Changes to the admin_damaged_items 2026-07-31 21:40:20 +02:00
8 changed files with 938 additions and 5496 deletions
+661 -788
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -319,15 +319,15 @@ THUMBNAIL_FOLDER = _get(_conf, ['upload', 'thumbnail_folder'], DEFAULTS['upload'
PREVIEW_FOLDER = _get(_conf, ['upload', 'preview_folder'], DEFAULTS['upload']['preview_folder'])
QR_CODE_FOLDER = _get(_conf, ['upload', 'qrcode_folder'], DEFAULTS['upload']['qrcode_folder'])
# Normalize to absolute paths to avoid cwd issues
# This guarantees exact matching with Docker volume mounts
if not os.path.isabs(UPLOAD_FOLDER):
UPLOAD_FOLDER = os.path.join(BASE_DIR, os.path.relpath(UPLOAD_FOLDER, BASE_DIR))
UPLOAD_FOLDER = os.path.abspath(os.path.join(BASE_DIR, "uploads"))
if not os.path.isabs(THUMBNAIL_FOLDER):
THUMBNAIL_FOLDER = os.path.join(BASE_DIR, os.path.relpath(THUMBNAIL_FOLDER, BASE_DIR))
THUMBNAIL_FOLDER = os.path.abspath(os.path.join(BASE_DIR, "thumbnails"))
if not os.path.isabs(PREVIEW_FOLDER):
PREVIEW_FOLDER = os.path.join(BASE_DIR, os.path.relpath(PREVIEW_FOLDER, BASE_DIR))
PREVIEW_FOLDER = os.path.abspath(os.path.join(BASE_DIR, "previews"))
if not os.path.isabs(QR_CODE_FOLDER):
QR_CODE_FOLDER = os.path.join(BASE_DIR, os.path.relpath(QR_CODE_FOLDER, BASE_DIR))
QR_CODE_FOLDER = os.path.abspath(os.path.join(BASE_DIR, "QRCodes"))
MAX_UPLOAD_MB = _get(_conf, ['upload', 'max_size_mb'], DEFAULTS['upload']['max_size_mb'])
IMAGE_MAX_UPLOAD_MB = _get(_conf, ['upload', 'image_max_size_mb'], DEFAULTS['upload']['image_max_size_mb'])
VIDEO_MAX_UPLOAD_MB = _get(_conf, ['upload', 'video_max_size_mb'], DEFAULTS['upload']['video_max_size_mb'])
+2 -1
View File
@@ -17,4 +17,5 @@ cryptography>=42.0.0
pywebpush
py-vapid>=1.9.0
beautifulsoup4
pywebpush
pywebpush
pandas
+14 -3
View File
@@ -7,7 +7,7 @@
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:12px; flex-wrap:wrap; margin-bottom:16px;">
<div>
<h1 style="margin:0;">Defekte Items</h1>
<p style="margin:6px 0 0; color:#64748b;">Eigenes Verwaltungsfenster fuer gemeldete Defekte mit schneller Reparatur-Funktion.</p>
<p style="margin:6px 0 0; color:#64748b;">Eigenes Verwaltungsfenster für gemeldete Defekte mit schneller Reparatur-Funktion.</p>
</div>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<a class="btn btn-outline-secondary" href="{{ url_for('admin_borrowings') }}">Ausleihen</a>
@@ -42,7 +42,13 @@
{% if item.isbn %}<div style="font-size:0.82rem; color:#64748b;">ISBN: {{ item.isbn }}</div>{% endif %}
</td>
<td style="padding:11px; border-bottom:1px solid #eef2f7; vertical-align:top;">
<div style="display:inline-block; padding:3px 9px; border-radius:999px; background:#fee2e2; color:#991b1b; font-size:0.75rem; font-weight:700;">Defekt</div>
<!-- HIER IST DIE ÄNDERUNG: Dynamischer Status-Badge -->
<div class="badge bg-{{ item.status_color }} rounded-pill" style="font-size:0.75rem; font-weight:700; padding:4px 10px;">
{{ item.status_text }}
</div>
<!-- ENDE DER ÄNDERUNG -->
<div style="font-size:0.84rem; color:#475569; margin-top:6px;">Meldungen: {{ item.damage_count }}</div>
<div style="font-size:0.84rem; color:#475569;">Condition: {{ item.condition or '-' }}</div>
<div style="font-size:0.84rem; color:#475569;">Verfuegbar: {{ 'Ja' if item.available else 'Nein' }}</div>
@@ -66,7 +72,12 @@
{% endif %}
</td>
<td style="padding:11px; border-bottom:1px solid #eef2f7; vertical-align:top;">
<!-- Button wird ausgeblendet, falls das Item bereits repariert ist -->
{% if item.condition not in ['repaired', 'fixed'] %}
<button class="btn btn-success btn-sm" onclick="repairDamage('{{ item.id }}', this)">Als repariert markieren</button>
{% else %}
<span class="text-success" style="font-size: 0.85rem;"><i class="bi bi-check-circle"></i> Repariert</span>
{% endif %}
</td>
</tr>
{% endfor %}
@@ -118,4 +129,4 @@ function repairDamage(itemId, button) {
});
}
</script>
{% endblock %}
{% endblock %}
File diff suppressed because it is too large Load Diff
+1 -6
View File
@@ -4569,12 +4569,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
<div class="detail-label">Code:</div>
<div class="detail-value">${escapeHtml(item.Code_4 || '-')}</div>
</div>
<div class="detail-group">
<div class="detail-label">Anzahl:</div>
<div class="detail-value">${escapeHtml(String(item.GroupedDisplayCount || 1))}</div>
</div>
${isGroupedItem ? `
<div class="detail-group">
<div class="detail-label">Verfügbar:</div>
+253
View File
@@ -0,0 +1,253 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Batch Upload</title>
<!-- CSRF-Token für JavaScript bereitstellen -->
<meta name="csrf-token" content="{{ session.get('_csrf_token', '') }}">
<style>
body { font-family: sans-serif; padding: 20px; background: #f4f7f6; }
.upload-container { background: white; padding: 20px; border-radius: 8px; max-width: 500px; margin: 0 auto; }
.form-group { margin-bottom: 15px; }
label { display: block; font-weight: bold; margin-bottom: 5px; }
input[type="file"] { width: 100%; padding: 8px; box-sizing: border-box; }
.btn-submit { width: 100%; padding: 10px; background: #4a90e2; color: white; border: none; border-radius: 4px; font-weight: bold; cursor: pointer; }
.btn-submit:disabled { background: #ccc; }
#uploadProgress { margin-top: 20px; display: none; }
progress { width: 100%; height: 20px; }
#logList { background: #fafafa; border: 1px solid #eee; padding: 10px; max-height: 150px; overflow-y: auto; font-size: 0.85em; list-style: none; }
</style>
</head>
<body>
<div class="upload-container">
<h2>Inventar Batch Upload</h2>
<form id="batchUploadForm">
<div class="form-group">
<label for="csv_file">1. items.csv auswählen</label>
<input type="file" id="csv_file" name="csv_file" accept=".csv" required>
</div>
<div class="form-group">
<label for="images">2. Bilder auswählen</label>
<input type="file" id="images" name="images" accept="image/*" multiple required>
</div>
<button type="submit" id="uploadBtn" class="btn-submit">Daten hochladen</button>
</form>
<div id="uploadProgress">
<div id="progressText">Bereite Upload vor...</div>
<progress id="progressBar" value="0" max="100"></progress>
<ul id="logList"></ul>
</div>
</div>
<script>
// 1. Ein echter CSV-Parser, der Zeilenumbrüche und Kommas in Texten korrekt ignoriert
function parseCSV(csvString) {
const rows = [];
let currentRow = [];
let currentCell = '';
let insideQuotes = false;
for (let i = 0; i < csvString.length; i++) {
const char = csvString[i];
const nextChar = csvString[i + 1];
if (char === '"' && insideQuotes && nextChar === '"') {
currentCell += '"';
i++; // Escaped Quotes ("") überspringen
} else if (char === '"') {
insideQuotes = !insideQuotes;
} else if (char === ',' && !insideQuotes) {
currentRow.push(currentCell);
currentCell = '';
} else if ((char === '\n' || char === '\r') && !insideQuotes) {
if (char === '\r' && nextChar === '\n') i++; // Windows Umbrüche überspringen
currentRow.push(currentCell);
// Leere Zeilen ignorieren
if (currentRow.length > 1 || currentRow[0] !== '') {
rows.push(currentRow);
}
currentRow = [];
currentCell = '';
} else {
currentCell += char;
}
}
if (currentCell !== '' || currentRow.length > 0) {
currentRow.push(currentCell);
if (currentRow.length > 1 || currentRow[0] !== '') {
rows.push(currentRow);
}
}
return rows;
}
// 2. Baut das Array wieder sicher zu einer sauberen CSV-Zeile für das Backend zusammen
function rowToCSV(rowArray) {
return rowArray.map(cell => {
if (cell === null || cell === undefined) return '';
let cellStr = String(cell);
// Wenn kritische Zeichen drin sind, sauber in Anführungszeichen verpacken
if (cellStr.includes(',') || cellStr.includes('"') || cellStr.includes('\n') || cellStr.includes('\r')) {
return '"' + cellStr.replace(/"/g, '""') + '"';
}
return cellStr;
}).join(',');
}
// 3. Bildnamen extrahieren (bleibt wie es war)
function extractImageNames(cellValue) {
if (!cellValue) return [];
let raw = String(cellValue).replace(/^["']|["']$/g, '').trim();
if (raw.startsWith('[') && raw.endsWith(']')) {
try {
const jsonValid = raw.replace(/'/g, '"');
return JSON.parse(jsonValid);
} catch (e) {
const matches = raw.match(/['"]([^'"]+)['"]/g);
if (matches) return matches.map(m => m.replace(/['"]/g, ''));
}
}
return raw ? [raw] : [];
}
document.getElementById('batchUploadForm').addEventListener('submit', async function(e) {
e.preventDefault();
const csvInput = document.getElementById('csv_file');
const imageInput = document.getElementById('images');
const uploadBtn = document.getElementById('uploadBtn');
const progressContainer = document.getElementById('uploadProgress');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const logList = document.getElementById('logList');
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
if (!csvInput.files.length) {
alert("Bitte wähle eine CSV-Datei aus.");
return;
}
uploadBtn.disabled = true;
progressContainer.style.display = 'block';
logList.innerHTML = '';
const log = (msg) => {
const li = document.createElement('li');
li.textContent = msg;
logList.appendChild(li);
logList.scrollTop = logList.scrollHeight;
};
const csvFile = csvInput.files[0];
const allImages = Array.from(imageInput.files);
const imageMap = new Map();
allImages.forEach(file => {
imageMap.set(file.name.toLowerCase(), file);
});
const BATCH_SIZE = 50;
try {
const csvText = await csvFile.text();
// Hier rufen wir jetzt unseren sicheren Parser auf!
const allRows = parseCSV(csvText);
if (allRows.length <= 1) {
throw new Error("CSV-Datei ist leer oder enthält nur Kopfzeilen.");
}
const headerRow = allRows[0];
const dataRows = allRows.slice(1);
log(`${dataRows.length} Einträge gefunden. Bereite Batches vor...`);
// Spalten-Index von "Images" sicher ermitteln
const imagesColIndex = headerRow.findIndex(h => h.toLowerCase().trim() === 'images');
// In Batches aufteilen
const batches = [];
for (let i = 0; i < dataRows.length; i += BATCH_SIZE) {
batches.push(dataRows.slice(i, i + BATCH_SIZE));
}
progressBar.max = batches.length;
progressBar.value = 0;
for (let b = 0; b < batches.length; b++) {
const batchRows = batches[b];
progressText.textContent = `Lade Batch ${b + 1} von ${batches.length} hoch...`;
const requiredImagesForBatch = new Set();
if (imagesColIndex !== -1) {
batchRows.forEach(rowCols => {
if (rowCols[imagesColIndex]) {
const imgNames = extractImageNames(rowCols[imagesColIndex]);
imgNames.forEach(name => {
const fileMatch = imageMap.get(name.toLowerCase());
if (fileMatch) {
requiredImagesForBatch.add(fileMatch);
}
});
}
});
}
// Sicherer Zusammenbau des CSV-Batches
const batchCsvArray = [headerRow, ...batchRows];
const batchCsvText = batchCsvArray.map(rowToCSV).join('\n');
const batchCsvBlob = new Blob([batchCsvText], { type: 'text/csv' });
const formData = new FormData();
formData.append('csv_file', batchCsvBlob, `batch_${b + 1}.csv`);
if (csrfToken) {
formData.append('csrf_token', csrfToken);
}
requiredImagesForBatch.forEach(imgFile => {
formData.append('images', imgFile);
});
log(`Batch ${b + 1}: ${batchRows.length} Items & ${requiredImagesForBatch.size} zugehörige Bilder.`);
const response = await fetch('/upload_csv_batch', {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': csrfToken || ''
}
});
const result = await response.json();
if (!response.ok || !result.success) {
throw new Error(result.message || `Server-Fehler ${response.status}`);
}
log(`Batch ${b + 1} abgeschlossen.`);
progressBar.value = b + 1;
}
progressText.textContent = "Upload erfolgreich beendet! Keine verschobenen Spalten mehr.";
uploadBtn.disabled = false;
} catch (err) {
alert("Upload abgebrochen: " + err.message);
log("Fehler: " + err.message);
uploadBtn.disabled = false;
}
});
</script>
</body>
</html>
+2 -1
View File
@@ -17,4 +17,5 @@ cryptography>=42.0.0
pywebpush
py-vapid>=1.9.0
beautifulsoup4
pywebpush
pywebpush
pandas