diff --git a/public/css/style.css b/public/css/style.css index 1323f4c..64a7b02 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -964,6 +964,58 @@ table input[type="text"] { width: 100%; } +/* Floating Overlay für Projektsuche im Dashboard */ +.project-search-overlay { + position: fixed; + z-index: 9000; + max-height: 70vh; + min-width: 520px; + max-width: 1100px; + overflow-y: auto; + overflow-x: hidden; + background: #fff; + border: 1px solid #ccc; + border-radius: 8px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); +} + +.project-search-overlay-inner { + padding: 12px; +} + +.project-search-overlay-status { + padding: 8px 0; + color: #666; + font-size: 14px; +} + +.project-search-overlay-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.project-search-overlay-table th { + text-align: left; + padding: 8px 10px; + border-bottom: 1px solid #ccc; + background: #f5f5f5; + white-space: nowrap; +} + +.project-search-overlay-table td { + padding: 8px 10px; + border-bottom: 1px solid #eee; +} + +.project-search-overlay-table tbody tr { + cursor: pointer; +} + +.project-search-overlay-table tbody tr:hover { + background: #e8f4fc; +} + .overtime-vacation-controls { margin-top: 15px; padding-top: 15px; diff --git a/public/js/dashboard.js b/public/js/dashboard.js index f295d62..91481a3 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -9,6 +9,12 @@ let weekendPercentages = { saturday: 100, sunday: 100 }; // Wochenend-Prozentsä let defaultBreakMinutes = 30; // Standard-Pausenzeit des Mitarbeiters (Vorbelegung) let latestSubmittedTimesheetId = null; // ID der neuesten eingereichten Version +// Projektsuche-Overlay im Dashboard +let activeProjectSearchInput = null; +let projectSearchDebounceTimer = null; +const PROJECT_SEARCH_DEBOUNCE_MS = 300; +const PROJECT_SEARCH_CLOSE_DELAY_MS = 200; + // Wochenend-Prozentsätze laden async function loadWeekendPercentages() { try { @@ -38,10 +44,10 @@ function getWeekendPercentage(date) { return 100; // Kein Wochenende = 100% (normal) } -/** Plausibilitätsprüfung Projektnummer: 7 Ziffern, beginnt mit 5, dann YY (Jahr), dann 4 Ziffern (z.B. 5260001). */ +/** Plausibilitätsprüfung Projektnummer: beginnt mit 5, dann 6–7 Ziffern (z.B. 5260001 oder 52600001). */ function isValidProjectNumber(value) { if (value === null || value === undefined || String(value).trim() === '') return true; - return /^5\d{6}$/.test(String(value).trim()); + return /^5\d{6,7}$/.test(String(value).trim()); } function validateActivityProjectNumbers(activities) { @@ -699,7 +705,7 @@ function renderWeek() { data-field="activity${idx + 1}_project_number" value="${activity.projectNumber || ''}" placeholder="z. B. 5260001" - title="7 Ziffern: 5 + Jahr (YY) + 4 Ziffern" + title="Mit 5 beginnen, dann 6–7 Ziffern (z.B. 5260001)" ${timeFieldsDisabled} ${disabled} onblur="saveEntry(this)" class="activity-project-input"> @@ -1301,7 +1307,6 @@ async function saveEntry(input) { if (activities[idx - 1]) { activities[idx - 1].projectNumber = null; } - alert(`Ungültige Projektnummer in Tätigkeit ${projectNumberCheck.activityIndex}: Die Projektnummer muss 7 Ziffern haben, mit 5 beginnen, gefolgt vom Jahr (YY) und 4 Ziffern (z.B. 5260001). Die Eingabe wurde geleert.`); } // Aktualisiere currentEntries mit den DOM-Werten @@ -2450,3 +2455,169 @@ window.savePingIP = async function() { alert('Fehler beim Speichern der IP-Adresse'); } }; + +// --- Projektsuche-Overlay im Dashboard --- +(function initProjectSearchOverlay() { + const overlay = document.getElementById('projectSearchOverlay'); + const statusEl = document.getElementById('projectSearchOverlayStatus'); + const tableEl = document.getElementById('projectSearchOverlayTable'); + const tbody = tableEl ? tableEl.querySelector('tbody') : null; + let projectSearchBlurCloseTimer = null; + + if (!overlay || !statusEl || !tableEl || !tbody) return; + + function positionOverlayAboveInput(input) { + // Breite/Position an der Tätigkeitsbeschreibung (activity-input) ausrichten + let anchor = null; + const row = input.closest('.activity-row'); + if (row) { + anchor = row.querySelector('.activity-input') || input; + } else { + anchor = input; + } + + const rect = anchor.getBoundingClientRect(); + const minLeft = 20; + const width = Math.max(520, Math.min(1100, rect.width)); + overlay.style.width = width + 'px'; + overlay.style.left = Math.max(minLeft, rect.left) + 'px'; + overlay.style.bottom = 'auto'; + const overlayHeight = overlay.offsetHeight || (window.innerHeight * 0.7); + let top = rect.top - overlayHeight - 8; // Overlay über dem Activity-Input platzieren + if (top < 10) { + top = 10; // nicht über den oberen Rand hinaus + } + overlay.style.top = top + 'px'; + } + + function showOverlay(input) { + activeProjectSearchInput = input; + overlay.style.display = 'block'; + overlay.setAttribute('aria-hidden', 'false'); + positionOverlayAboveInput(input); + const term = (input.value || '').trim(); + if (term) { + runProjectSearch(term); + } else { + statusEl.textContent = 'Suchbegriff eingeben (Projektnummer oder Beschreibung).'; + tableEl.style.display = 'none'; + } + } + + function hideOverlay() { + overlay.style.display = 'none'; + overlay.setAttribute('aria-hidden', 'true'); + activeProjectSearchInput = null; + } + + function runProjectSearch(term) { + statusEl.textContent = 'Suche…'; + tableEl.style.display = 'none'; + tbody.innerHTML = ''; + + if (!term) { + statusEl.textContent = 'Suchbegriff eingeben (Projektnummer oder Beschreibung).'; + return; + } + + fetch('/api/projects/search?term=' + encodeURIComponent(term), { headers: { 'Accept': 'application/json' } }) + .then(function (res) { return res.json(); }) + .then(function (result) { + const rows = result.results || []; + if (rows.length === 0) { + statusEl.textContent = 'Keine Projekte gefunden.'; + return; + } + statusEl.textContent = ''; + tableEl.style.display = ''; + rows.forEach(function (row) { + const tr = document.createElement('tr'); + tr.setAttribute('data-auftrag', row.auftrag || ''); + tr.innerHTML = + '' + (row.auftrag || '') + '' + + '' + (row.proj || '') + '' + + '' + (row.such || '') + '' + + '' + (row.bez || '') + ''; + tbody.appendChild(tr); + }); + }) + .catch(function (err) { + console.error('Projektsuche:', err); + statusEl.textContent = 'Projektsuche ist aktuell nicht verfügbar. Bitte später erneut versuchen.'; + }); + } + + function onProjectInputFocus(ev) { + const input = ev.target; + if (!input.classList.contains('activity-project-input')) return; + if (projectSearchDebounceTimer) { + clearTimeout(projectSearchDebounceTimer); + projectSearchDebounceTimer = null; + } + showOverlay(input); + } + + function onProjectInputInput(ev) { + const input = ev.target; + if (!input.classList.contains('activity-project-input')) return; + if (!activeProjectSearchInput || activeProjectSearchInput !== input) showOverlay(input); + + if (projectSearchDebounceTimer) clearTimeout(projectSearchDebounceTimer); + const term = (input.value || '').trim(); + projectSearchDebounceTimer = setTimeout(function () { + projectSearchDebounceTimer = null; + if (term) { + runProjectSearch(term); + } else { + statusEl.textContent = 'Suchbegriff eingeben (Projektnummer oder Beschreibung).'; + tableEl.style.display = 'none'; + tbody.innerHTML = ''; + } + }, PROJECT_SEARCH_DEBOUNCE_MS); + } + + function onProjectInputBlur() { + if (projectSearchBlurCloseTimer) clearTimeout(projectSearchBlurCloseTimer); + projectSearchBlurCloseTimer = setTimeout(function () { + projectSearchBlurCloseTimer = null; + if (activeProjectSearchInput && document.activeElement && overlay.contains(document.activeElement)) return; + hideOverlay(); + }, PROJECT_SEARCH_CLOSE_DELAY_MS); + } + + overlay.addEventListener('mousedown', function (ev) { + var tr = ev.target.closest('tbody tr'); + if (!tr) return; + var inputToUpdate = activeProjectSearchInput; + if (!inputToUpdate) return; + ev.preventDefault(); + ev.stopPropagation(); + var auftrag = tr.getAttribute('data-auftrag'); + if (auftrag === null || auftrag === undefined) return; + auftrag = String(auftrag).trim(); + if (!auftrag) return; + if (projectSearchBlurCloseTimer) { + clearTimeout(projectSearchBlurCloseTimer); + projectSearchBlurCloseTimer = null; + } + inputToUpdate.value = auftrag; + inputToUpdate.dispatchEvent(new Event('input', { bubbles: true })); + inputToUpdate.dispatchEvent(new Event('change', { bubbles: true })); + if (typeof saveEntry === 'function') saveEntry(inputToUpdate); + hideOverlay(); + inputToUpdate.focus(); + }); + + document.addEventListener('focusin', onProjectInputFocus); + document.addEventListener('input', onProjectInputInput); + document.addEventListener('focusout', function (ev) { + if (ev.target.classList.contains('activity-project-input')) onProjectInputBlur(); + }); + + document.addEventListener('click', function (ev) { + if (!overlay.style.display || overlay.style.display === 'none') return; + if (overlay.contains(ev.target)) return; + if (ev.target.classList.contains('activity-project-input')) return; + hideOverlay(); + }); +})(); diff --git a/views/dashboard.ejs b/views/dashboard.ejs index 734e0b8..d9bccf2 100644 --- a/views/dashboard.ejs +++ b/views/dashboard.ejs @@ -197,6 +197,24 @@ + + +