chore: initialize public repository
CI / checks (push) Has been cancelled

This commit is contained in:
maddin
2026-03-22 12:57:09 +00:00
commit 6fbd1bb3c2
142 changed files with 19826 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
export function initCsrf() {
const tokenInput = document.querySelector('input[name="csrf_token"]');
const token = tokenInput ? tokenInput.value : null;
if (!token) {
return;
}
document.querySelectorAll('form[method="post"]').forEach((form) => {
if (!form.querySelector('input[name="csrf_token"]')) {
const hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.name = 'csrf_token';
hidden.value = token;
form.appendChild(hidden);
}
});
}
+153
View File
@@ -0,0 +1,153 @@
function isInteractiveTouchTarget(target) {
if (!target || typeof target.closest !== 'function') {
return false;
}
return Boolean(target.closest('a, button, input, select, textarea, summary, details, label, form'));
}
function attachSwipeNavigation(target, prevUrl, nextUrl) {
if (!target || !prevUrl || !nextUrl) {
return;
}
const minSwipeDistance = 60;
const maxVerticalRatio = 1.25;
const maxSwipeDuration = 900;
let startX = 0;
let startY = 0;
let startAt = 0;
let tracking = false;
let navigating = false;
target.addEventListener('touchstart', (event) => {
if (event.touches.length !== 1 || isInteractiveTouchTarget(event.target)) {
tracking = false;
return;
}
const touch = event.touches[0];
startX = touch.clientX;
startY = touch.clientY;
startAt = Date.now();
tracking = true;
}, { passive: true });
target.addEventListener('touchend', (event) => {
if (!tracking || navigating || event.changedTouches.length !== 1) {
tracking = false;
return;
}
tracking = false;
const touch = event.changedTouches[0];
const deltaX = touch.clientX - startX;
const deltaY = touch.clientY - startY;
const absX = Math.abs(deltaX);
const absY = Math.abs(deltaY);
const duration = Date.now() - startAt;
if (duration > maxSwipeDuration || absX < minSwipeDistance || absX <= absY * maxVerticalRatio) {
return;
}
navigating = true;
if (deltaX < 0) {
window.location.assign(nextUrl);
} else {
window.location.assign(prevUrl);
}
}, { passive: true });
}
function initSwipeNavigation() {
if (!window.matchMedia('(pointer: coarse)').matches && !('ontouchstart' in window)) {
return;
}
document.querySelectorAll('[data-component="swipe-nav"]').forEach((node) => {
attachSwipeNavigation(node, node.dataset.prevUrl, node.dataset.nextUrl);
});
}
function initWarningBanner() {
const warningBanner = document.querySelector('[data-component="workhours-warning"]');
if (!warningBanner) {
return;
}
const warningKey = warningBanner.getAttribute('data-workhours-warning') || '';
const storageKey = warningKey ? `workhours-warning-dismissed:${warningKey}` : '';
if (storageKey && window.localStorage.getItem(storageKey) === '1') {
warningBanner.remove();
return;
}
const closeButton = warningBanner.querySelector('[data-action="warning-close"]');
if (closeButton) {
closeButton.addEventListener('click', () => {
warningBanner.remove();
if (storageKey) {
window.localStorage.setItem(storageKey, '1');
}
});
}
}
function initWeeklyTargetEditor() {
const form = document.querySelector('.weekly-target-form');
const editor = document.querySelector('[data-component="weekly-target-editor"]');
const toggleButtons = document.querySelectorAll('.js-toggle-weekly-target-editor');
if (form && toggleButtons.length && editor) {
toggleButtons.forEach((toggleButton) => {
toggleButton.addEventListener('click', () => {
editor.classList.toggle('is-hidden');
});
});
}
if (!form) {
return;
}
form.addEventListener('submit', (event) => {
const scopeSelect = form.querySelector("select[name='scope']");
const hoursInput = form.querySelector("input[name='weekly_target_hours']");
if (!scopeSelect || !hoursInput) {
return;
}
const scope = scopeSelect.value;
const hours = hoursInput.value;
let scopeText = '';
if (scope === 'current_week') {
scopeText = 'Nur die aktuell ausgewählte Woche';
} else if (scope === 'all_weeks') {
scopeText = 'Alle Wochen (Vergangenheit und Zukunft)';
} else if (scope === 'from_current_week') {
scopeText = 'Aktuelle Woche und alle zukünftigen Wochen';
}
if (!scopeText) {
return;
}
const confirmed = window.confirm(`Wochen-Soll wirklich ändern?\nNeuer Wert: ${hours} h\nGültigkeit: ${scopeText}`);
if (!confirmed) {
event.preventDefault();
return;
}
if (editor) {
editor.classList.add('is-hidden');
}
});
}
export function initDashboard() {
initSwipeNavigation();
initWarningBanner();
initWeeklyTargetEditor();
}
+10
View File
@@ -0,0 +1,10 @@
export function initFlash() {
document.querySelectorAll('[data-component="flash"]').forEach((flash) => {
flash.addEventListener('click', (event) => {
const target = event.target;
if (target instanceof HTMLElement && target.dataset.action === 'flash-close') {
flash.remove();
}
});
});
}
+255
View File
@@ -0,0 +1,255 @@
async function refreshCurrentViewPreservingScroll() {
const scrollX = window.scrollX;
const scrollY = window.scrollY;
const response = await fetch(window.location.href, {
credentials: 'same-origin',
headers: { 'X-Requested-With': 'fetch' },
});
if (!response.ok) {
throw new Error(`refresh_failed_${response.status}`);
}
const html = await response.text();
const nextDocument = new DOMParser().parseFromString(html, 'text/html');
['.site-header', '.app-page-actions-wrap', 'main.page'].forEach((selector) => {
const currentNode = document.querySelector(selector);
const nextNode = nextDocument.querySelector(selector);
if (currentNode && nextNode) {
currentNode.replaceWith(nextNode);
}
});
window.scrollTo({ left: scrollX, top: scrollY });
if (typeof window.__stundenfuchsInitApp === 'function') {
window.__stundenfuchsInitApp();
}
}
function parseTimeToMinutes(value) {
const match = /^(\d{2}):(\d{2})$/.exec(value || '');
if (!match) {
return null;
}
return Number(match[1]) * 60 + Number(match[2]);
}
function formatMinutesToTime(value) {
const minutes = Math.max(0, Math.min(24 * 60 - 1, Number(value) || 0));
const hoursPart = String(Math.floor(minutes / 60)).padStart(2, '0');
const minutesPart = String(minutes % 60).padStart(2, '0');
return `${hoursPart}:${minutesPart}`;
}
function requiredBreakMinutesForSpan(spanMinutes) {
if (spanMinutes > 9 * 60) {
return 45;
}
if (spanMinutes > 6 * 60) {
return 30;
}
return 0;
}
function requiredBreakMinutesForNetMinutes(netMinutes) {
if (netMinutes > (9 * 60 - 45)) {
return 45;
}
if (netMinutes > (6 * 60 - 30)) {
return 30;
}
return 0;
}
function initFullDayButtons() {
document.querySelectorAll('[data-action="entry-apply-full-day"]').forEach((button) => {
if (!(button instanceof HTMLButtonElement) || button.dataset.fullDayBound === 'true') {
return;
}
button.dataset.fullDayBound = 'true';
const form = button.closest('form[data-component="break-rules-form"]');
if (!(form instanceof HTMLFormElement)) {
return;
}
const startInput = form.querySelector('[data-break-input="start"]');
const endInput = form.querySelector('[data-break-input="end"]');
if (!(startInput instanceof HTMLInputElement) || !(endInput instanceof HTMLInputElement)) {
return;
}
button.addEventListener('click', () => {
const netMinutes = Number(form.dataset.fullDayNetMinutes || '');
if (!Number.isFinite(netMinutes) || netMinutes <= 0) {
return;
}
const defaultStartValue = form.dataset.defaultStartTime || '08:30';
const startMinutes = parseTimeToMinutes(startInput.value) ?? parseTimeToMinutes(defaultStartValue);
if (startMinutes === null) {
return;
}
const autoBreakEnabled = form.dataset.autoBreakEnabled === 'true';
const configuredBreakMinutes = Number(form.dataset.defaultBreakMinutes || '0');
const breakMinutes = autoBreakEnabled
? requiredBreakMinutesForNetMinutes(netMinutes)
: Math.max(0, configuredBreakMinutes);
const endMinutes = startMinutes + netMinutes + breakMinutes;
startInput.value = formatMinutesToTime(startMinutes);
endInput.value = formatMinutesToTime(endMinutes);
});
});
}
function initBreakRuleForms() {
document.querySelectorAll('form[data-component="break-rules-form"]').forEach((form) => {
if (form.dataset.breakBound === 'true') {
return;
}
form.dataset.breakBound = 'true';
const autoBreakEnabled = form.dataset.autoBreakEnabled === 'true';
const modeInput = form.querySelector('[data-break-mode]');
const startInput = form.querySelector('[data-break-input="start"]');
const endInput = form.querySelector('[data-break-input="end"]');
const breakInput = form.querySelector('[data-break-input="minutes"]');
const statusNode = form.querySelector('[data-break-status]');
const resetButton = form.querySelector('[data-action="break-reset-auto"]');
if (!(modeInput instanceof HTMLInputElement) || !(startInput instanceof HTMLInputElement) || !(endInput instanceof HTMLInputElement)) {
return;
}
const updateStatus = () => {
if (!statusNode) {
return;
}
statusNode.textContent = modeInput.value === 'manual'
? 'Pause manuell gesetzt. Gesetzliche Mindestpause wird nicht automatisch überschrieben.'
: 'Gesetzliche Mindestpause nach deutschem Arbeitsrecht wird automatisch vorgeschlagen.';
};
const applyAutoBreak = () => {
if (!(breakInput instanceof HTMLInputElement)) {
return;
}
const startMinutes = parseTimeToMinutes(startInput.value);
const endMinutes = parseTimeToMinutes(endInput.value);
if (startMinutes === null || endMinutes === null || endMinutes <= startMinutes) {
return;
}
modeInput.value = 'auto';
breakInput.value = String(requiredBreakMinutesForSpan(endMinutes - startMinutes));
updateStatus();
};
const setManualMode = () => {
modeInput.value = 'manual';
updateStatus();
};
startInput.addEventListener('input', () => {
if (modeInput.value === 'auto') {
applyAutoBreak();
}
});
endInput.addEventListener('input', () => {
if (modeInput.value === 'auto') {
applyAutoBreak();
}
});
if (breakInput instanceof HTMLInputElement) {
breakInput.addEventListener('input', setManualMode);
}
if (resetButton) {
resetButton.addEventListener('click', applyAutoBreak);
}
if (!autoBreakEnabled) {
return;
}
if (!modeInput.value) {
modeInput.value = 'auto';
}
if (modeInput.value === 'auto') {
applyAutoBreak();
} else {
updateStatus();
}
});
}
function initAsyncRefreshForms() {
document.querySelectorAll('form[data-async-refresh="view"]').forEach((form) => {
if (form.dataset.asyncBound === 'true') {
return;
}
form.dataset.asyncBound = 'true';
form.addEventListener('submit', async (event) => {
event.preventDefault();
const submitter = event.submitter instanceof HTMLElement ? event.submitter : null;
if (submitter) {
submitter.setAttribute('disabled', 'disabled');
}
try {
const response = await fetch(form.action, {
method: 'POST',
body: new FormData(form),
credentials: 'same-origin',
headers: { 'X-Requested-With': 'fetch' },
});
if (!response.ok) {
throw new Error(`submit_failed_${response.status}`);
}
await refreshCurrentViewPreservingScroll();
} catch (error) {
window.location.assign(window.location.href);
} finally {
if (submitter) {
submitter.removeAttribute('disabled');
}
}
});
});
}
function initBreakSettingsForms() {
document.querySelectorAll('form[data-component="break-settings-form"]').forEach((form) => {
if (form.dataset.breakSettingsBound === 'true') {
return;
}
form.dataset.breakSettingsBound = 'true';
const toggle = form.querySelector('[data-break-settings-toggle]');
const minutesInput = form.querySelector('[data-break-settings-minutes]');
if (!(toggle instanceof HTMLInputElement) || !(minutesInput instanceof HTMLInputElement)) {
return;
}
const syncDisabledState = () => {
minutesInput.disabled = toggle.checked;
};
toggle.addEventListener('change', syncDisabledState);
syncDisabledState();
});
}
export function initForms() {
document.querySelectorAll('form[data-confirm]').forEach((form) => {
form.addEventListener('submit', (event) => {
const message = form.getAttribute('data-confirm') || 'Aktion wirklich ausführen?';
if (!window.confirm(message)) {
event.preventDefault();
}
});
});
initAsyncRefreshForms();
initFullDayButtons();
initBreakRuleForms();
initBreakSettingsForms();
}
+27
View File
@@ -0,0 +1,27 @@
export function initModal() {
const modals = document.querySelectorAll('[data-component="modal"]');
modals.forEach((modal) => {
modal.addEventListener('click', (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
if (target.dataset.action === 'modal-close') {
modal.setAttribute('hidden', 'hidden');
}
});
});
document.querySelectorAll('[data-action="modal-open"]').forEach((trigger) => {
trigger.addEventListener('click', () => {
const id = trigger.getAttribute('data-target');
if (!id) {
return;
}
const modal = document.getElementById(id);
if (modal) {
modal.removeAttribute('hidden');
}
});
});
}
@@ -0,0 +1,59 @@
const STORAGE_KEY = 'stundenfuchs:settingsSection';
const DESKTOP_SYNC_MEDIA_QUERY = '(min-width: 51.26em)';
function shouldSyncGroups() {
return window.matchMedia(DESKTOP_SYNC_MEDIA_QUERY).matches;
}
function syncGroupState(section, isOpen) {
const groupName = section.dataset.syncGroup || '';
if (!groupName || !shouldSyncGroups()) {
return;
}
document.querySelectorAll(`[data-component="settings-section"][data-sync-group="${groupName}"]`).forEach((peer) => {
if (peer instanceof HTMLDetailsElement && peer !== section) {
peer.open = isOpen;
}
});
}
function openSectionById(sectionId) {
if (!sectionId) {
return;
}
const target = document.getElementById(sectionId);
if (!(target instanceof HTMLDetailsElement)) {
return;
}
target.open = true;
syncGroupState(target, true);
}
export function initSettingsSections() {
const sections = Array.from(document.querySelectorAll('[data-component="settings-section"]'));
if (sections.length === 0) {
return;
}
const hashTarget = window.location.hash ? window.location.hash.slice(1) : '';
const storedTarget = window.sessionStorage.getItem(STORAGE_KEY) || '';
openSectionById(hashTarget || storedTarget);
if (storedTarget) {
window.sessionStorage.removeItem(STORAGE_KEY);
}
sections.forEach((section) => {
if (!(section instanceof HTMLDetailsElement) || !section.id) {
return;
}
section.addEventListener('toggle', () => {
syncGroupState(section, section.open);
});
section.querySelectorAll('form').forEach((form) => {
form.addEventListener('submit', () => {
window.sessionStorage.setItem(STORAGE_KEY, section.id);
});
});
});
}