Implementierung ins Dashboard und Layout anpassungen

This commit is contained in:
2026-03-13 18:03:19 +01:00
parent 1d8ba6a955
commit 2d0bcbbd2f
3 changed files with 245 additions and 4 deletions

View File

@@ -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 67 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 67 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 =
'<td>' + (row.auftrag || '') + '</td>' +
'<td>' + (row.proj || '') + '</td>' +
'<td>' + (row.such || '') + '</td>' +
'<td>' + (row.bez || '') + '</td>';
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();
});
})();