Compare commits
3 Commits
9e1921a198
...
90666a246c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90666a246c | ||
|
|
e1be63b1ca | ||
|
|
c20f6d9dff |
@@ -258,6 +258,19 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Details/Summary chevron rotation */
|
||||
details[open] .details-chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
details summary {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
details summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Blink animation for running timer icon */
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
@@ -402,8 +415,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div class="flex flex-wrap gap-4 items-end">
|
||||
<!-- CSV Filter & Export Section (Collapsible) -->
|
||||
<details class="mt-4 pt-4 border-t border-gray-700">
|
||||
<summary class="cursor-pointer text-gray-300 hover:text-gray-100 font-medium flex items-center gap-2 select-none">
|
||||
<i data-lucide="chevron-down" class="w-5 h-5 transition-transform details-chevron"></i>
|
||||
<i data-lucide="filter" class="w-5 h-5 text-blue-400"></i>
|
||||
<span>CSV Filter & Export</span>
|
||||
</summary>
|
||||
<div class="mt-4 flex flex-wrap gap-4 items-end">
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label for="filterFrom" class="block text-sm font-medium text-gray-300 mb-1">Von</label>
|
||||
<input type="text" id="filterFrom"
|
||||
@@ -440,13 +459,27 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Settings Section (Collapsible) -->
|
||||
<details class="mt-4 pt-4 border-t border-gray-700">
|
||||
<summary class="cursor-pointer text-gray-300 hover:text-gray-100 font-medium text-sm flex items-center gap-2">
|
||||
<span class="text-lg">⚙️</span>
|
||||
<summary class="cursor-pointer text-gray-300 hover:text-gray-100 font-medium flex items-center gap-2 select-none">
|
||||
<i data-lucide="chevron-down" class="w-5 h-5 transition-transform details-chevron"></i>
|
||||
<i data-lucide="settings" class="w-5 h-5 text-purple-400"></i>
|
||||
<span>Einstellungen</span>
|
||||
</summary>
|
||||
<div class="mt-4 flex flex-wrap gap-4 items-center">
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label for="employeeName" class="block text-sm font-medium text-gray-300 mb-1">Mitarbeitername</label>
|
||||
<input type="text" id="employeeName" placeholder="Max Mustermann"
|
||||
class="w-full px-4 py-2 border border-gray-600 bg-gray-700 text-gray-100 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
||||
</div>
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label for="employeeId" class="block text-sm font-medium text-gray-300 mb-1">Personalnummer</label>
|
||||
<input type="text" id="employeeId" placeholder="12345"
|
||||
class="w-full px-4 py-2 border border-gray-600 bg-gray-700 text-gray-100 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex flex-wrap gap-4 items-center">
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label for="bundeslandSelect" class="block text-sm font-medium text-gray-300 mb-1">Bundesland (Feiertage)</label>
|
||||
@@ -548,8 +581,9 @@
|
||||
|
||||
<!-- Month Navigation -->
|
||||
<div class="mb-8 glass-card rounded-xl p-5 border border-gray-700/50">
|
||||
<div class="flex items-center justify-between flex-wrap gap-4">
|
||||
<div class="flex gap-3 flex-wrap">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 items-center gap-4">
|
||||
<!-- Left: Action Buttons (hidden on mobile, shown on desktop at start) -->
|
||||
<div class="hidden lg:flex gap-3 flex-wrap justify-start order-2 lg:order-1">
|
||||
<button id="btnToggleBulkEdit"
|
||||
class="btn-elevated inline-flex items-center gap-2 px-5 py-3 bg-gradient-to-r from-gray-700 to-gray-600 text-gray-100 rounded-xl font-semibold" title="Mehrfachauswahl aktivieren">
|
||||
<i data-lucide="check-square" class="w-5 h-5"></i>
|
||||
@@ -562,7 +596,8 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="monthNavigation" class="flex items-center gap-4">
|
||||
<!-- Center: Month Navigation (always visible and centered) -->
|
||||
<div id="monthNavigation" class="flex items-center justify-center gap-4 order-1 lg:order-2">
|
||||
<button id="btnPrevMonth"
|
||||
class="btn-elevated inline-flex items-center justify-center w-12 h-12 bg-gradient-to-br from-gray-700 to-gray-600 text-gray-100 rounded-xl">
|
||||
<i data-lucide="chevron-left" class="w-6 h-6"></i>
|
||||
@@ -576,7 +611,33 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="w-48"></div> <!-- Spacer for alignment -->
|
||||
<!-- Right: Export Button (hidden on mobile, shown on desktop at end) -->
|
||||
<div class="hidden lg:flex justify-end order-3">
|
||||
<button id="btnExportPDF"
|
||||
class="btn-elevated inline-flex items-center gap-2 px-5 py-3 bg-gradient-to-r from-red-600 to-red-500 text-white rounded-xl font-semibold" title="Monat als PDF exportieren">
|
||||
<i data-lucide="file-text" class="w-5 h-5"></i>
|
||||
<span>PDF Export</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Action Buttons (shown only on mobile, below navigation) -->
|
||||
<div class="flex lg:hidden gap-3 flex-wrap justify-center order-3">
|
||||
<button onclick="document.getElementById('btnToggleBulkEdit').click()"
|
||||
class="btn-elevated inline-flex items-center gap-2 px-5 py-3 bg-gradient-to-r from-gray-700 to-gray-600 text-gray-100 rounded-xl font-semibold" title="Mehrfachauswahl aktivieren">
|
||||
<i data-lucide="check-square" class="w-5 h-5"></i>
|
||||
<span>Auswahl</span>
|
||||
</button>
|
||||
<button onclick="document.getElementById('btnAutoFill').click()"
|
||||
class="btn-elevated inline-flex items-center gap-2 px-5 py-3 bg-gradient-to-r from-indigo-600 to-indigo-500 text-white rounded-xl font-semibold" title="Monat ausfüllen (8h)">
|
||||
<i data-lucide="calendar-check" class="w-5 h-5"></i>
|
||||
<span>Ausfüllen</span>
|
||||
</button>
|
||||
<button onclick="document.getElementById('btnExportPDF').click()"
|
||||
class="btn-elevated inline-flex items-center gap-2 px-5 py-3 bg-gradient-to-r from-red-600 to-red-500 text-white rounded-xl font-semibold" title="Monat als PDF exportieren">
|
||||
<i data-lucide="file-text" class="w-5 h-5"></i>
|
||||
<span>PDF Export</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -623,6 +684,11 @@
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
<span>Löschen</span>
|
||||
</button>
|
||||
<button id="btnBulkExportPDF"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-all duration-200 text-sm font-medium shadow-sm" title="Ausgewählte als PDF exportieren">
|
||||
<i data-lucide="file-text" class="w-4 h-4"></i>
|
||||
<span>PDF Export</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -763,8 +829,16 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/de.js"></script>
|
||||
|
||||
<!-- App Logic -->
|
||||
<script src="app.js"></script>
|
||||
<!-- jsPDF for PDF export -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.2/jspdf.plugin.autotable.min.js"></script>
|
||||
|
||||
<!-- App Logic - Load in correct order -->
|
||||
<script src="js/state.js"></script>
|
||||
<script src="js/utils.js"></script>
|
||||
<script src="js/holidays.js"></script>
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
|
||||
<!-- Initialize Lucide Icons -->
|
||||
<script>
|
||||
|
||||
180
public/js/api.js
Normal file
180
public/js/api.js
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* API Functions
|
||||
* Backend communication layer
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fetch entries from backend
|
||||
*/
|
||||
async function fetchEntries(fromDate = null, toDate = null) {
|
||||
try {
|
||||
let url = '/api/entries';
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (fromDate) params.append('from', fromDate);
|
||||
if (toDate) params.append('to', toDate);
|
||||
|
||||
if (params.toString()) {
|
||||
url += '?' + params.toString();
|
||||
}
|
||||
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch entries');
|
||||
}
|
||||
|
||||
const entries = await response.json();
|
||||
return entries;
|
||||
} catch (error) {
|
||||
console.error('Error fetching entries:', error);
|
||||
showNotification('Fehler beim Laden der Einträge', 'error');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new entry
|
||||
*/
|
||||
async function createEntry(date, startTime, endTime, pauseMinutes, location) {
|
||||
try {
|
||||
const body = {
|
||||
date: formatDateISO(date),
|
||||
startTime,
|
||||
endTime,
|
||||
location: location || 'office'
|
||||
};
|
||||
|
||||
// Only include pauseMinutes if explicitly provided (not empty)
|
||||
if (pauseMinutes !== null && pauseMinutes !== undefined && pauseMinutes !== '') {
|
||||
body.pauseMinutes = parseInt(pauseMinutes);
|
||||
}
|
||||
|
||||
const response = await fetch('/api/entries', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to create entry');
|
||||
}
|
||||
|
||||
const entry = await response.json();
|
||||
return entry;
|
||||
} catch (error) {
|
||||
console.error('Error creating entry:', error);
|
||||
showNotification(error.message || 'Fehler beim Erstellen des Eintrags', 'error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing entry
|
||||
*/
|
||||
async function updateEntry(id, date, startTime, endTime, pauseMinutes, location) {
|
||||
try {
|
||||
const body = {
|
||||
date: formatDateISO(date),
|
||||
startTime,
|
||||
endTime,
|
||||
location: location || 'office'
|
||||
};
|
||||
|
||||
// Only include pauseMinutes if explicitly provided (not empty)
|
||||
if (pauseMinutes !== null && pauseMinutes !== undefined && pauseMinutes !== '') {
|
||||
body.pauseMinutes = parseInt(pauseMinutes);
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/entries/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to update entry');
|
||||
}
|
||||
|
||||
const entry = await response.json();
|
||||
return entry;
|
||||
} catch (error) {
|
||||
console.error('Error updating entry:', error);
|
||||
showNotification(error.message || 'Fehler beim Aktualisieren des Eintrags', 'error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an entry
|
||||
*/
|
||||
async function deleteEntry(id) {
|
||||
try {
|
||||
const response = await fetch(`/api/entries/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete entry');
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error deleting entry:', error);
|
||||
showNotification('Fehler beim Löschen des Eintrags', 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a setting by key
|
||||
*/
|
||||
async function getSetting(key) {
|
||||
try {
|
||||
const response = await fetch(`/api/settings/${key}`);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return null; // Setting doesn't exist yet
|
||||
}
|
||||
throw new Error('Failed to get setting');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.value;
|
||||
} catch (error) {
|
||||
console.error('Error getting setting:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a setting
|
||||
*/
|
||||
async function setSetting(key, value) {
|
||||
try {
|
||||
const response = await fetch('/api/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ key, value })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to set setting');
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error setting setting:', error);
|
||||
showNotification('Fehler beim Speichern der Einstellung', 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
154
public/js/holidays.js
Normal file
154
public/js/holidays.js
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Holiday Functions
|
||||
* German public holidays calculation and checking
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if date is weekend
|
||||
*/
|
||||
function isWeekend(date) {
|
||||
const day = date.getDay();
|
||||
return day === 0 || day === 6; // Sunday or Saturday
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Easter Sunday for a given year (Gauss algorithm)
|
||||
*/
|
||||
function getEasterSunday(year) {
|
||||
const a = year % 19;
|
||||
const b = Math.floor(year / 100);
|
||||
const c = year % 100;
|
||||
const d = Math.floor(b / 4);
|
||||
const e = b % 4;
|
||||
const f = Math.floor((b + 8) / 25);
|
||||
const g = Math.floor((b - f + 1) / 3);
|
||||
const h = (19 * a + b - d - g + 15) % 30;
|
||||
const i = Math.floor(c / 4);
|
||||
const k = c % 4;
|
||||
const l = (32 + 2 * e + 2 * i - h - k) % 7;
|
||||
const m = Math.floor((a + 11 * h + 22 * l) / 451);
|
||||
const month = Math.floor((h + l - 7 * m + 114) / 31);
|
||||
const day = ((h + l - 7 * m + 114) % 31) + 1;
|
||||
return new Date(year, month - 1, day);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all public holidays for a given year and Bundesland
|
||||
*/
|
||||
function getPublicHolidays(year, bundesland) {
|
||||
const holidays = [];
|
||||
|
||||
// Fixed holidays (all states)
|
||||
holidays.push({ date: new Date(year, 0, 1), name: 'Neujahr' });
|
||||
holidays.push({ date: new Date(year, 4, 1), name: 'Tag der Arbeit' });
|
||||
holidays.push({ date: new Date(year, 9, 3), name: 'Tag der Deutschen Einheit' });
|
||||
holidays.push({ date: new Date(year, 11, 25), name: '1. Weihnachtstag' });
|
||||
holidays.push({ date: new Date(year, 11, 26), name: '2. Weihnachtstag' });
|
||||
|
||||
// Heilige Drei Könige (BW, BY, ST)
|
||||
if (['BW', 'BY', 'ST'].includes(bundesland)) {
|
||||
holidays.push({ date: new Date(year, 0, 6), name: 'Heilige Drei Könige' });
|
||||
}
|
||||
|
||||
// Internationaler Frauentag (BE, MV since 2023)
|
||||
if (['BE'].includes(bundesland) || (bundesland === 'MV' && year >= 2023)) {
|
||||
holidays.push({ date: new Date(year, 2, 8), name: 'Internationaler Frauentag' });
|
||||
}
|
||||
|
||||
// Weltkindertag (TH since 2019)
|
||||
if (bundesland === 'TH' && year >= 2019) {
|
||||
holidays.push({ date: new Date(year, 8, 20), name: 'Weltkindertag' });
|
||||
}
|
||||
|
||||
// Reformationstag (BB, MV, SN, ST, TH, + HB, HH, NI, SH since 2018)
|
||||
const reformationstagStates = ['BB', 'MV', 'SN', 'ST', 'TH'];
|
||||
if (year >= 2018) {
|
||||
reformationstagStates.push('HB', 'HH', 'NI', 'SH');
|
||||
}
|
||||
if (reformationstagStates.includes(bundesland)) {
|
||||
holidays.push({ date: new Date(year, 9, 31), name: 'Reformationstag' });
|
||||
}
|
||||
|
||||
// Allerheiligen (BW, BY, NW, RP, SL)
|
||||
if (['BW', 'BY', 'NW', 'RP', 'SL'].includes(bundesland)) {
|
||||
holidays.push({ date: new Date(year, 10, 1), name: 'Allerheiligen' });
|
||||
}
|
||||
|
||||
// Buß- und Bettag (only SN)
|
||||
if (bundesland === 'SN') {
|
||||
// Buß- und Bettag is the Wednesday before November 23
|
||||
let bussbettag = new Date(year, 10, 23);
|
||||
while (bussbettag.getDay() !== 3) { // 3 = Wednesday
|
||||
bussbettag.setDate(bussbettag.getDate() - 1);
|
||||
}
|
||||
bussbettag.setDate(bussbettag.getDate() - 7); // One week before
|
||||
holidays.push({ date: bussbettag, name: 'Buß- und Bettag' });
|
||||
}
|
||||
|
||||
// Easter-dependent holidays
|
||||
const easter = getEasterSunday(year);
|
||||
|
||||
// Karfreitag (Good Friday) - 2 days before Easter (all states)
|
||||
const goodFriday = new Date(easter);
|
||||
goodFriday.setDate(easter.getDate() - 2);
|
||||
holidays.push({ date: goodFriday, name: 'Karfreitag' });
|
||||
|
||||
// Ostermontag (Easter Monday) - 1 day after Easter (all states)
|
||||
const easterMonday = new Date(easter);
|
||||
easterMonday.setDate(easter.getDate() + 1);
|
||||
holidays.push({ date: easterMonday, name: 'Ostermontag' });
|
||||
|
||||
// Christi Himmelfahrt (Ascension Day) - 39 days after Easter (all states)
|
||||
const ascension = new Date(easter);
|
||||
ascension.setDate(easter.getDate() + 39);
|
||||
holidays.push({ date: ascension, name: 'Christi Himmelfahrt' });
|
||||
|
||||
// Pfingstmontag (Whit Monday) - 50 days after Easter (all states)
|
||||
const whitMonday = new Date(easter);
|
||||
whitMonday.setDate(easter.getDate() + 50);
|
||||
holidays.push({ date: whitMonday, name: 'Pfingstmontag' });
|
||||
|
||||
// Fronleichnam (Corpus Christi) - 60 days after Easter (BW, BY, HE, NW, RP, SL, + some communities in SN, TH)
|
||||
if (['BW', 'BY', 'HE', 'NW', 'RP', 'SL'].includes(bundesland)) {
|
||||
const corpusChristi = new Date(easter);
|
||||
corpusChristi.setDate(easter.getDate() + 60);
|
||||
holidays.push({ date: corpusChristi, name: 'Fronleichnam' });
|
||||
}
|
||||
|
||||
// Mariä Himmelfahrt (Assumption of Mary) - August 15 (BY in some communities, SL)
|
||||
if (['SL'].includes(bundesland)) {
|
||||
holidays.push({ date: new Date(year, 7, 15), name: 'Mariä Himmelfahrt' });
|
||||
}
|
||||
|
||||
return holidays;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if date is a public holiday
|
||||
* Returns the holiday name or null
|
||||
*/
|
||||
function getHolidayName(date) {
|
||||
const year = date.getFullYear();
|
||||
const holidays = getPublicHolidays(year, currentBundesland);
|
||||
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
const holiday = holidays.find(h => {
|
||||
return h.date.toISOString().split('T')[0] === dateStr;
|
||||
});
|
||||
|
||||
return holiday ? holiday.name : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if date is a public holiday
|
||||
*/
|
||||
function isPublicHoliday(date) {
|
||||
return getHolidayName(date) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if date is weekend or public holiday
|
||||
*/
|
||||
function isWeekendOrHoliday(date) {
|
||||
return isWeekend(date) || isPublicHoliday(date);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
126
public/js/utils.js
Normal file
126
public/js/utils.js
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Utility Functions
|
||||
* Date/Time formatting, rounding, duration formatting
|
||||
*/
|
||||
|
||||
/**
|
||||
* Format date from YYYY-MM-DD to DD.MM.YYYY
|
||||
*/
|
||||
function formatDateDisplay(dateStr) {
|
||||
const [year, month, day] = dateStr.split('-');
|
||||
return `${day}.${month}.${year}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date from DD.MM.YYYY to YYYY-MM-DD
|
||||
*/
|
||||
function formatDateISO(dateStr) {
|
||||
const [day, month, year] = dateStr.split('.');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get today's date in YYYY-MM-DD format
|
||||
*/
|
||||
function getTodayISO() {
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Round time down to nearest 15 minutes
|
||||
*/
|
||||
function roundDownTo15Min(date) {
|
||||
const minutes = date.getMinutes();
|
||||
const roundedMinutes = Math.floor(minutes / 15) * 15;
|
||||
date.setMinutes(roundedMinutes);
|
||||
date.setSeconds(0);
|
||||
date.setMilliseconds(0);
|
||||
return date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Round time up to nearest 15 minutes
|
||||
*/
|
||||
function roundUpTo15Min(date) {
|
||||
const minutes = date.getMinutes();
|
||||
const roundedMinutes = Math.ceil(minutes / 15) * 15;
|
||||
date.setMinutes(roundedMinutes);
|
||||
date.setSeconds(0);
|
||||
date.setMilliseconds(0);
|
||||
return date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time as HH:MM
|
||||
*/
|
||||
function formatTime(date) {
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format seconds to HH:MM:SS
|
||||
*/
|
||||
function formatDuration(seconds) {
|
||||
const hrs = Math.floor(seconds / 3600);
|
||||
const mins = Math.floor((seconds % 3600) / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${String(hrs).padStart(2, '0')}:${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show toast notification
|
||||
*/
|
||||
function showNotification(message, type = 'info') {
|
||||
const container = document.getElementById('toastContainer');
|
||||
|
||||
// Create toast element
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
|
||||
// Icon based on type
|
||||
const icons = {
|
||||
success: '✓',
|
||||
error: '✕',
|
||||
info: 'ℹ'
|
||||
};
|
||||
|
||||
toast.innerHTML = `
|
||||
<span class="toast-icon">${icons[type] || 'ℹ'}</span>
|
||||
<span>${message}</span>
|
||||
`;
|
||||
|
||||
container.appendChild(toast);
|
||||
|
||||
// Auto-remove after 3 seconds
|
||||
setTimeout(() => {
|
||||
toast.classList.add('hiding');
|
||||
setTimeout(() => {
|
||||
container.removeChild(toast);
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get day of week abbreviation in German
|
||||
*/
|
||||
function getDayOfWeek(date) {
|
||||
const days = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
|
||||
return days[date.getDay()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get month name in German
|
||||
*/
|
||||
function getMonthName(month) {
|
||||
const months = [
|
||||
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
||||
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'
|
||||
];
|
||||
return months[month];
|
||||
}
|
||||
Reference in New Issue
Block a user