feat: Enhance item detail views with damage history and improved loading logic
This commit is contained in:
@@ -4064,6 +4064,9 @@ def get_items():
|
|||||||
'Filter3': 1,
|
'Filter3': 1,
|
||||||
'Ort': 1,
|
'Ort': 1,
|
||||||
'User': 1,
|
'User': 1,
|
||||||
|
'BlockedNow': 1,
|
||||||
|
'Reservierbar': 1,
|
||||||
|
'HasDamage': 1,
|
||||||
'ItemType': 1,
|
'ItemType': 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+101
-10
@@ -799,10 +799,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadItems(offset = 0, append = false) {
|
function loadItems(offset = 0, append = false) {
|
||||||
// Für Pages nach der ersten: Explizit vollständige Daten laden (light_mode=false)
|
// Keep list payload lightweight; full details are fetched on-demand in openItemQuick.
|
||||||
// Erste Page: light_mode wird automatisch enablet
|
return fetch(`{{ url_for('get_items') }}?offset=${offset}&limit=${MAIN_ITEMS_PAGE_SIZE}&light_mode=true`)
|
||||||
const lightModeParam = offset > 0 ? '&light_mode=false' : '';
|
|
||||||
return fetch(`{{ url_for('get_items') }}?offset=${offset}&limit=${MAIN_ITEMS_PAGE_SIZE}${lightModeParam}`)
|
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
const itemsContainer = document.querySelector('#items-container');
|
const itemsContainer = document.querySelector('#items-container');
|
||||||
@@ -1073,12 +1071,9 @@
|
|||||||
|
|
||||||
// Stop event from bubbling up
|
// Stop event from bubbling up
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
// Get full item data with correct image paths
|
// Always load full, up-to-date item details for the modal.
|
||||||
const modalItemData = {...item};
|
openItemQuick(item._id);
|
||||||
modalItemData.Images = item.Images ? item.Images.map(img => "{{ url_for('uploaded_file', filename='') }}" + img) : [];
|
|
||||||
|
|
||||||
openItemModal(modalItemData);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1414,6 +1409,76 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openItemModal(item) {
|
function openItemModal(item) {
|
||||||
|
const escapeHtml = (value) => String(value ?? '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
|
||||||
|
const formatHistoryDate = (value) => {
|
||||||
|
if (!value) return '-';
|
||||||
|
const dt = new Date(value);
|
||||||
|
if (Number.isNaN(dt.getTime())) {
|
||||||
|
return escapeHtml(value);
|
||||||
|
}
|
||||||
|
return dt.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const damageReports = Array.isArray(item.DamageReports) ? item.DamageReports : [];
|
||||||
|
const damageRepairs = Array.isArray(item.DamageRepairs) ? item.DamageRepairs : [];
|
||||||
|
const damageHistoryEntries = [];
|
||||||
|
|
||||||
|
damageReports.forEach((report) => {
|
||||||
|
damageHistoryEntries.push({
|
||||||
|
type: 'report',
|
||||||
|
rawDate: report?.reported_at || null,
|
||||||
|
dateLabel: formatHistoryDate(report?.reported_at),
|
||||||
|
actor: escapeHtml(report?.reported_by || '-'),
|
||||||
|
description: escapeHtml(report?.description || 'Schaden gemeldet'),
|
||||||
|
meta: '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
damageRepairs.forEach((repair) => {
|
||||||
|
const resolvedReports = Array.isArray(repair?.resolved_reports) ? repair.resolved_reports : [];
|
||||||
|
damageHistoryEntries.push({
|
||||||
|
type: 'repair',
|
||||||
|
rawDate: repair?.repaired_at || null,
|
||||||
|
dateLabel: formatHistoryDate(repair?.repaired_at),
|
||||||
|
actor: escapeHtml(repair?.repaired_by || '-'),
|
||||||
|
description: 'Als repariert markiert',
|
||||||
|
meta: `${resolvedReports.length} Meldung(en) abgeschlossen`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
damageHistoryEntries.sort((a, b) => {
|
||||||
|
const ta = a.rawDate ? new Date(a.rawDate).getTime() : 0;
|
||||||
|
const tb = b.rawDate ? new Date(b.rawDate).getTime() : 0;
|
||||||
|
return (Number.isNaN(tb) ? 0 : tb) - (Number.isNaN(ta) ? 0 : ta);
|
||||||
|
});
|
||||||
|
|
||||||
|
const damageHistoryHtml = damageHistoryEntries.length
|
||||||
|
? damageHistoryEntries.map((entry) => {
|
||||||
|
const badgeStyle = entry.type === 'repair'
|
||||||
|
? 'background:#dcfce7;color:#166534;'
|
||||||
|
: 'background:#fee2e2;color:#991b1b;';
|
||||||
|
const badgeText = entry.type === 'repair' ? 'Repariert' : 'Schaden';
|
||||||
|
const metaLine = entry.meta ? `<div style="font-size:0.84rem;color:#4b5563;">${escapeHtml(entry.meta)}</div>` : '';
|
||||||
|
return `
|
||||||
|
<div style="border:1px solid #dbe3ee;border-radius:8px;padding:10px;background:#fff;display:grid;gap:6px;">
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;">
|
||||||
|
<span style="display:inline-block;padding:2px 8px;border-radius:999px;font-size:0.75rem;font-weight:700;${badgeStyle}">${badgeText}</span>
|
||||||
|
<span style="font-size:0.84rem;color:#475569;">${entry.dateLabel}</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:0.9rem;color:#0f172a;"><strong>Von:</strong> ${entry.actor}</div>
|
||||||
|
<div style="font-size:0.92rem;color:#1f2937;">${entry.description}</div>
|
||||||
|
${metaLine}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('')
|
||||||
|
: '<div style="font-size:0.92rem;color:#64748b;">Keine Beschädigungs-Historie vorhanden.</div>';
|
||||||
|
|
||||||
// Get modal elements
|
// Get modal elements
|
||||||
const modal = document.getElementById('item-modal');
|
const modal = document.getElementById('item-modal');
|
||||||
const modalContent = document.getElementById('modal-content-wrapper');
|
const modalContent = document.getElementById('modal-content-wrapper');
|
||||||
@@ -1571,6 +1636,19 @@
|
|||||||
<div class="detail-value">${item.Beschreibung || '-'}</div>
|
<div class="detail-value">${item.Beschreibung || '-'}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-group full-width" style="margin-top:12px;">
|
||||||
|
<div class="detail-label" style="font-weight:600; color:#374151;">Beschädigungs-Historie</div>
|
||||||
|
<div class="detail-value">
|
||||||
|
<button id="toggle-damage-history" class="calendar-toggle-btn" style="margin-bottom:12px; padding:10px 16px; border-radius:6px; background:#f3f4f6; border:1px solid #d1d5db; font-weight:500; cursor:pointer; display:inline-flex; align-items:center; gap:8px; transition:all 0.2s ease;">
|
||||||
|
<span>🛠️</span>
|
||||||
|
<span id="toggle-damage-history-text">Historie anzeigen</span>
|
||||||
|
</button>
|
||||||
|
<div id="damage-history-panel" style="display:none; margin-top:8px; border:1px solid #e7edf5; border-radius:10px; padding:12px; background:#f8fafc;">
|
||||||
|
<div style="display:grid; gap:10px;">${damageHistoryHtml}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="detail-group full-width" style="margin-top:14px; padding:12px; border:1px solid #e5e7eb; border-radius:10px; background:#fbfbfd; box-shadow: 0 1px 1px rgba(0,0,0,0.03);">
|
<div class="detail-group full-width" style="margin-top:14px; padding:12px; border:1px solid #e5e7eb; border-radius:10px; background:#fbfbfd; box-shadow: 0 1px 1px rgba(0,0,0,0.03);">
|
||||||
<div class="detail-label" style="font-weight:600; color:#374151;">Verfügbarkeit prüfen</div>
|
<div class="detail-label" style="font-weight:600; color:#374151;">Verfügbarkeit prüfen</div>
|
||||||
<div class="detail-value">
|
<div class="detail-value">
|
||||||
@@ -1687,6 +1765,9 @@
|
|||||||
const detailsPanel = document.getElementById('calendar-day-details');
|
const detailsPanel = document.getElementById('calendar-day-details');
|
||||||
const detailsDate = document.getElementById('cal-details-date');
|
const detailsDate = document.getElementById('cal-details-date');
|
||||||
const detailsList = document.getElementById('cal-details-list');
|
const detailsList = document.getElementById('cal-details-list');
|
||||||
|
const damageToggleBtn = document.getElementById('toggle-damage-history');
|
||||||
|
const damageHistoryPanel = document.getElementById('damage-history-panel');
|
||||||
|
const damageToggleText = document.getElementById('toggle-damage-history-text');
|
||||||
|
|
||||||
let bookings = [];
|
let bookings = [];
|
||||||
let currentDate = new Date();
|
let currentDate = new Date();
|
||||||
@@ -1847,6 +1928,16 @@
|
|||||||
renderCalendar();
|
renderCalendar();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
damageToggleBtn?.addEventListener('click', () => {
|
||||||
|
const shouldOpen = damageHistoryPanel.style.display === 'none';
|
||||||
|
damageHistoryPanel.style.display = shouldOpen ? 'block' : 'none';
|
||||||
|
if (damageToggleText) {
|
||||||
|
damageToggleText.textContent = shouldOpen ? 'Historie verbergen' : 'Historie anzeigen';
|
||||||
|
}
|
||||||
|
damageToggleBtn.style.background = shouldOpen ? '#e0e7ff' : '#f3f4f6';
|
||||||
|
damageToggleBtn.style.borderColor = shouldOpen ? '#818cf8' : '#d1d5db';
|
||||||
|
});
|
||||||
|
|
||||||
// Availability checker (user)
|
// Availability checker (user)
|
||||||
const availDate = document.getElementById('avail-date');
|
const availDate = document.getElementById('avail-date');
|
||||||
const availStart = document.getElementById('avail-start');
|
const availStart = document.getElementById('avail-start');
|
||||||
|
|||||||
@@ -3415,10 +3415,8 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadItems(offset = 0, append = false) {
|
function loadItems(offset = 0, append = false) {
|
||||||
// Für Pages nach der ersten: Explizit vollständige Daten laden (light_mode=false)
|
// Keep list payload lightweight; full details are fetched on-demand in openItemQuick.
|
||||||
// Erste Page: light_mode wird automatisch enablet
|
return fetch(`{{ url_for('get_items') }}?offset=${offset}&limit=${MAIN_ADMIN_ITEMS_PAGE_SIZE}&light_mode=true`)
|
||||||
const lightModeParam = offset > 0 ? '&light_mode=false' : '';
|
|
||||||
return fetch(`{{ url_for('get_items') }}?offset=${offset}&limit=${MAIN_ADMIN_ITEMS_PAGE_SIZE}${lightModeParam}`)
|
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
const itemsContainer = document.querySelector('#items-container');
|
const itemsContainer = document.querySelector('#items-container');
|
||||||
@@ -3592,6 +3590,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const damageCount = Array.isArray(item.DamageReports) ? item.DamageReports.length : 0;
|
const damageCount = Array.isArray(item.DamageReports) ? item.DamageReports.length : 0;
|
||||||
|
const hasDamage = Boolean(item.HasDamage) || damageCount > 0;
|
||||||
const groupedCount = Number(item.GroupedDisplayCount || 1);
|
const groupedCount = Number(item.GroupedDisplayCount || 1);
|
||||||
const availableGroupedCount = Number(item.AvailableGroupedCount ?? (item.Verfuegbar ? 1 : 0));
|
const availableGroupedCount = Number(item.AvailableGroupedCount ?? (item.Verfuegbar ? 1 : 0));
|
||||||
const isGroupedItem = groupedCount > 1;
|
const isGroupedItem = groupedCount > 1;
|
||||||
@@ -3608,7 +3607,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
<p class="item-col-filter3"><strong>Thema:</strong> ${filter3Display}${filter3More}</p>
|
<p class="item-col-filter3"><strong>Thema:</strong> ${filter3Display}${filter3More}</p>
|
||||||
<p class="item-col-code"><strong>Barcode:</strong> ${item.Code_4 || '-'}</p>
|
<p class="item-col-code"><strong>Barcode:</strong> ${item.Code_4 || '-'}</p>
|
||||||
<p class="item-col-count"><strong>Anzahl:</strong> ${groupedCount}</p>
|
<p class="item-col-count"><strong>Anzahl:</strong> ${groupedCount}</p>
|
||||||
${damageCount > 0 ? `<div class="damage-badge">Schäden gemeldet: ${damageCount}</div>` : ''}
|
${hasDamage ? `<div class="damage-badge">${damageCount > 0 ? `Schäden gemeldet: ${damageCount}` : 'Schäden gemeldet'}</div>` : ''}
|
||||||
<div class="image-container">
|
<div class="image-container">
|
||||||
${imagesHtml}
|
${imagesHtml}
|
||||||
</div>
|
</div>
|
||||||
@@ -3680,11 +3679,8 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
const modalItemData = {...item};
|
openItemQuick(item._id);
|
||||||
modalItemData.Images = item.Images ? item.Images.map(img => "{{ url_for('uploaded_file', filename='') }}" + img) : [];
|
|
||||||
|
|
||||||
openItemModal(modalItemData);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -4303,14 +4299,58 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
const damageReports = Array.isArray(item.DamageReports) ? item.DamageReports : [];
|
const damageReports = Array.isArray(item.DamageReports) ? item.DamageReports : [];
|
||||||
const damageInfoHtml = damageReports.length > 0
|
const damageRepairs = Array.isArray(item.DamageRepairs) ? item.DamageRepairs : [];
|
||||||
? `<ul class="damage-list">${damageReports.map(report => {
|
const damageHistoryEntries = [];
|
||||||
const desc = escapeHtml(report?.description || '-');
|
|
||||||
const by = escapeHtml(report?.reported_by || 'Unbekannt');
|
damageReports.forEach(report => {
|
||||||
const at = escapeHtml(formatDamageTimestamp(report?.reported_at));
|
damageHistoryEntries.push({
|
||||||
return `<li><strong>${at}</strong> durch ${by}<br>${desc}</li>`;
|
type: 'report',
|
||||||
}).join('')}</ul>`
|
timestamp: report?.reported_at || null,
|
||||||
: 'Keine Schäden erfasst.';
|
dateLabel: escapeHtml(formatDamageTimestamp(report?.reported_at)),
|
||||||
|
actor: escapeHtml(report?.reported_by || 'Unbekannt'),
|
||||||
|
description: escapeHtml(report?.description || 'Schaden gemeldet'),
|
||||||
|
meta: '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
damageRepairs.forEach(repair => {
|
||||||
|
const resolvedReports = Array.isArray(repair?.resolved_reports) ? repair.resolved_reports : [];
|
||||||
|
damageHistoryEntries.push({
|
||||||
|
type: 'repair',
|
||||||
|
timestamp: repair?.repaired_at || null,
|
||||||
|
dateLabel: escapeHtml(formatDamageTimestamp(repair?.repaired_at)),
|
||||||
|
actor: escapeHtml(repair?.repaired_by || 'Unbekannt'),
|
||||||
|
description: 'Als repariert markiert',
|
||||||
|
meta: `${resolvedReports.length} Meldung(en) abgeschlossen`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
damageHistoryEntries.sort((a, b) => {
|
||||||
|
const ta = a.timestamp ? new Date(a.timestamp).getTime() : 0;
|
||||||
|
const tb = b.timestamp ? new Date(b.timestamp).getTime() : 0;
|
||||||
|
return (Number.isNaN(tb) ? 0 : tb) - (Number.isNaN(ta) ? 0 : ta);
|
||||||
|
});
|
||||||
|
|
||||||
|
const damageHistoryHtml = damageHistoryEntries.length > 0
|
||||||
|
? damageHistoryEntries.map(entry => {
|
||||||
|
const badgeStyle = entry.type === 'repair'
|
||||||
|
? 'background:#dcfce7;color:#166534;'
|
||||||
|
: 'background:#fee2e2;color:#991b1b;';
|
||||||
|
const badgeText = entry.type === 'repair' ? 'Repariert' : 'Schaden';
|
||||||
|
const metaLine = entry.meta ? `<div style="font-size:0.84rem;color:#4b5563;">${escapeHtml(entry.meta)}</div>` : '';
|
||||||
|
return `
|
||||||
|
<div style="border:1px solid #dbe3ee;border-radius:8px;padding:10px;background:#fff;display:grid;gap:6px;">
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;">
|
||||||
|
<span style="display:inline-block;padding:2px 8px;border-radius:999px;font-size:0.75rem;font-weight:700;${badgeStyle}">${badgeText}</span>
|
||||||
|
<span style="font-size:0.84rem;color:#475569;">${entry.dateLabel}</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:0.9rem;color:#0f172a;"><strong>Von:</strong> ${entry.actor}</div>
|
||||||
|
<div style="font-size:0.92rem;color:#1f2937;">${entry.description}</div>
|
||||||
|
${metaLine}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('')
|
||||||
|
: '<div style="font-size:0.92rem;color:#64748b;">Keine Beschädigungs-Historie vorhanden.</div>';
|
||||||
|
|
||||||
modalContent.innerHTML = `
|
modalContent.innerHTML = `
|
||||||
<h2>${item.Name}</h2>
|
<h2>${item.Name}</h2>
|
||||||
@@ -4385,9 +4425,17 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
<div class="detail-value">${item.Beschreibung || '-'}</div>
|
<div class="detail-value">${item.Beschreibung || '-'}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="detail-group full-width">
|
<div class="detail-group full-width" style="margin-top:12px;">
|
||||||
<div class="detail-label">Schäden:</div>
|
<div class="detail-label" style="font-weight:600; color:#374151;">Beschädigungs-Historie</div>
|
||||||
<div class="detail-value">${damageInfoHtml}</div>
|
<div class="detail-value">
|
||||||
|
<button id="toggle-damage-history" class="calendar-toggle-btn" style="margin-bottom:12px; padding:10px 16px; border-radius:6px; background:#f3f4f6; border:1px solid #d1d5db; font-weight:500; cursor:pointer; display:inline-flex; align-items:center; gap:8px; transition:all 0.2s ease;">
|
||||||
|
<span>🛠️</span>
|
||||||
|
<span id="toggle-damage-history-text">Historie anzeigen</span>
|
||||||
|
</button>
|
||||||
|
<div id="damage-history-panel" style="display:none; margin-top:8px; border:1px solid #e7edf5; border-radius:10px; padding:12px; background:#f8fafc;">
|
||||||
|
<div style="display:grid; gap:10px;">${damageHistoryHtml}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="detail-group full-width" style="margin-top:12px; padding:10px; border:1px solid #e3e3e3; border-radius:8px;">
|
<div class="detail-group full-width" style="margin-top:12px; padding:10px; border:1px solid #e3e3e3; border-radius:8px;">
|
||||||
@@ -4486,6 +4534,9 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
const detailsPanel = document.getElementById('calendar-day-details');
|
const detailsPanel = document.getElementById('calendar-day-details');
|
||||||
const detailsDate = document.getElementById('cal-details-date');
|
const detailsDate = document.getElementById('cal-details-date');
|
||||||
const detailsList = document.getElementById('cal-details-list');
|
const detailsList = document.getElementById('cal-details-list');
|
||||||
|
const damageToggleBtn = document.getElementById('toggle-damage-history');
|
||||||
|
const damageHistoryPanel = document.getElementById('damage-history-panel');
|
||||||
|
const damageToggleText = document.getElementById('toggle-damage-history-text');
|
||||||
|
|
||||||
let bookings = [];
|
let bookings = [];
|
||||||
let currentDate = new Date();
|
let currentDate = new Date();
|
||||||
@@ -4645,6 +4696,16 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
renderCalendar();
|
renderCalendar();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
damageToggleBtn?.addEventListener('click', () => {
|
||||||
|
const shouldOpen = damageHistoryPanel.style.display === 'none';
|
||||||
|
damageHistoryPanel.style.display = shouldOpen ? 'block' : 'none';
|
||||||
|
if (damageToggleText) {
|
||||||
|
damageToggleText.textContent = shouldOpen ? 'Historie verbergen' : 'Historie anzeigen';
|
||||||
|
}
|
||||||
|
damageToggleBtn.style.background = shouldOpen ? '#e0e7ff' : '#f3f4f6';
|
||||||
|
damageToggleBtn.style.borderColor = shouldOpen ? '#818cf8' : '#d1d5db';
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// Availability checker
|
// Availability checker
|
||||||
const availDate = document.getElementById('avail-date');
|
const availDate = document.getElementById('avail-date');
|
||||||
|
|||||||
Reference in New Issue
Block a user