Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5052dd9de6 | |||
| bf31ee2d16 | |||
| b847930500 | |||
| 756ff55b4c | |||
| df5a3265a1 | |||
| 2c6da44af8 | |||
| d6d6858002 | |||
| 0cf0bd8dec | |||
| af9826547c | |||
| 87027cf3c9 |
+590
-800
File diff suppressed because it is too large
Load Diff
@@ -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'])
|
||||
|
||||
@@ -17,4 +17,5 @@ cryptography>=42.0.0
|
||||
pywebpush
|
||||
py-vapid>=1.9.0
|
||||
beautifulsoup4
|
||||
pywebpush
|
||||
pywebpush
|
||||
pandas
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Batch Upload - CSV & Bilder</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #4a90e2;
|
||||
--background-color: #f4f7f6;
|
||||
--text-color: #333;
|
||||
--border-radius: 8px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.upload-container {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
color: var(--primary-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px dashed #ccc;
|
||||
border-radius: var(--border-radius);
|
||||
background: #fafafa;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-submit:hover {
|
||||
background-color: #357abd;
|
||||
}
|
||||
|
||||
.btn-submit:disabled {
|
||||
background-color: #a0c4e8;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Status & Feedback Messages */
|
||||
#status-message {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
display: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.loading {
|
||||
background-color: #e2e3e5;
|
||||
color: #383d41;
|
||||
border: 1px solid #d6d8db;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border: 3px solid rgba(0,0,0,0.1);
|
||||
border-radius: 50%;
|
||||
border-top-color: var(--primary-color);
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
vertical-align: middle;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="upload-container">
|
||||
<h2>Inventar Batch Upload</h2>
|
||||
|
||||
<form id="uploadForm">
|
||||
<div class="form-group">
|
||||
<label for="csv_file">1. items.csv Datei auswählen</label>
|
||||
<!-- Akzeptiert nur CSV Dateien -->
|
||||
<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>
|
||||
<!-- multiple erlaubt das Auswählen mehrerer Bilder gleichzeitig -->
|
||||
<input type="file" id="images" name="images" accept="image/*" multiple required>
|
||||
<small style="color: #666; display: block; margin-top: 5px;">Du kannst mehrere Bilder markieren (Strg/Cmd gedrückt halten).</small>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submitBtn" class="btn-submit">Daten hochladen</button>
|
||||
</form>
|
||||
|
||||
<div id="status-message"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('uploadForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault(); // Verhindert das Neuladen der Seite
|
||||
|
||||
const form = e.target;
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const statusDiv = document.getElementById('status-message');
|
||||
|
||||
// UI auf "Laden" setzen
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerText = 'Wird verarbeitet...';
|
||||
statusDiv.className = 'loading';
|
||||
statusDiv.style.display = 'block';
|
||||
statusDiv.innerHTML = '<div class="spinner"></div> Lade Dateien hoch und verarbeite Bilder... Bitte warten.';
|
||||
|
||||
// FormData sammelt alle Inputs aus dem Formular (csv_file und images)
|
||||
const formData = new FormData(form);
|
||||
|
||||
try {
|
||||
// Sende die Daten an den Flask-Endpoint
|
||||
const response = await fetch('/upload_csv_batch', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
let result;
|
||||
try {
|
||||
// Versuche, die Antwort als JSON zu lesen
|
||||
result = await response.json();
|
||||
} catch (jsonError) {
|
||||
// Wenn der Server kein JSON, sondern HTML (z.B. bei einem Python-Crash) sendet
|
||||
const errorText = await response.text();
|
||||
console.error("Server hat kein JSON gesendet. Antwort war:", errorText);
|
||||
throw new Error("Der Server hat einen HTML-Fehler zurückgegeben (Python-Crash oder falscher Pfad). Siehe Konsole.");
|
||||
}
|
||||
|
||||
if (response.ok && result.success) {
|
||||
// Erfolgreicher Upload
|
||||
statusDiv.className = 'success';
|
||||
statusDiv.innerHTML = `
|
||||
<strong>Erfolg!</strong><br>
|
||||
${result.message}
|
||||
`;
|
||||
form.reset(); // Formular nach Erfolg leeren
|
||||
} else {
|
||||
// Fehler vom Server (mit JSON-Fehlermeldung)
|
||||
statusDiv.className = 'error';
|
||||
statusDiv.innerHTML = `<strong>Fehler:</strong> ${result.message || 'Ein unbekannter Fehler ist aufgetreten.'}`;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
// Netzwerkfehler oder abgefangener Server-Fehler
|
||||
statusDiv.className = 'error';
|
||||
statusDiv.innerHTML = `<strong>Fehler:</strong> ${error.message}`;
|
||||
console.error('Upload Error:', error);
|
||||
} finally {
|
||||
// UI wieder freigeben
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerText = 'Daten hochladen';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
+2
-1
@@ -17,4 +17,5 @@ cryptography>=42.0.0
|
||||
pywebpush
|
||||
py-vapid>=1.9.0
|
||||
beautifulsoup4
|
||||
pywebpush
|
||||
pywebpush
|
||||
pandas
|
||||
Reference in New Issue
Block a user