Styling
This commit is contained in:
@@ -47,7 +47,7 @@ checkinApp.get('/api/checkin/:userId', (req, res) => {
|
|||||||
const currentTime = getCurrentTime();
|
const currentTime = getCurrentTime();
|
||||||
|
|
||||||
// Prüfe ob User existiert
|
// Prüfe ob User existiert
|
||||||
db.get('SELECT id FROM users WHERE id = ?', [userId], (err, user) => {
|
db.get('SELECT id, default_break_minutes FROM users WHERE id = ?', [userId], (err, user) => {
|
||||||
if (err || !user) {
|
if (err || !user) {
|
||||||
return sendResponse(req, res, false, { error: 'Benutzer nicht gefunden', status: 404 });
|
return sendResponse(req, res, false, { error: 'Benutzer nicht gefunden', status: 404 });
|
||||||
}
|
}
|
||||||
@@ -61,10 +61,14 @@ checkinApp.get('/api/checkin/:userId', (req, res) => {
|
|||||||
|
|
||||||
const successTitle = 'Hallo, du wurdest erfolgreich eingecheckt';
|
const successTitle = 'Hallo, du wurdest erfolgreich eingecheckt';
|
||||||
|
|
||||||
|
const userDefaultBreakMinutes = Number.isInteger(user?.default_break_minutes) && user.default_break_minutes >= 0
|
||||||
|
? user.default_break_minutes
|
||||||
|
: 30;
|
||||||
|
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
// Kein Eintrag existiert → Erstelle neuen mit start_time
|
// Kein Eintrag existiert → Erstelle neuen mit start_time
|
||||||
db.run(`INSERT INTO timesheet_entries (user_id, date, start_time, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)`,
|
db.run(`INSERT INTO timesheet_entries (user_id, date, start_time, break_minutes, updated_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
||||||
[userId, currentDate, currentTime], (err) => {
|
[userId, currentDate, currentTime, userDefaultBreakMinutes], (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return sendResponse(req, res, false, { error: 'Fehler beim Erstellen des Eintrags', status: 500 });
|
return sendResponse(req, res, false, { error: 'Fehler beim Erstellen des Eintrags', status: 500 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -840,6 +840,17 @@ table input[type="text"] {
|
|||||||
color: #7f8c8d;
|
color: #7f8c8d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Überstunden-Farbklassen (global genutzt, z. B. Verwaltung & Auswertung) */
|
||||||
|
.overtime-positive {
|
||||||
|
color: #27ae60;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overtime-negative {
|
||||||
|
color: #e74c3c;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
/* Activities/Tätigkeiten */
|
/* Activities/Tätigkeiten */
|
||||||
.activities-row {
|
.activities-row {
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
@@ -864,7 +875,7 @@ table input[type="text"] {
|
|||||||
|
|
||||||
.activity-row {
|
.activity-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 150px 120px;
|
grid-template-columns: 1fr 150px 150px;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -893,7 +904,7 @@ table input[type="text"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.activity-hours-input {
|
.activity-hours-input {
|
||||||
width: 80px;
|
width: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.activity-hours-label {
|
.activity-hours-label {
|
||||||
@@ -901,6 +912,11 @@ table input[type="text"] {
|
|||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.activity-hours-hh-input,
|
||||||
|
.activity-hours-mm-input {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.activity-project {
|
.activity-project {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -374,6 +374,52 @@ function getFullDayHours() {
|
|||||||
return userWochenstunden && userArbeitstage ? (userWochenstunden / userArbeitstage) : 8;
|
return userWochenstunden && userArbeitstage ? (userWochenstunden / userArbeitstage) : 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function decimalHoursToParts(decimalHours) {
|
||||||
|
const parsed = Number(decimalHours);
|
||||||
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||||
|
return { hh: '', mm: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalMinutes = Math.round(parsed * 60);
|
||||||
|
const hh = Math.floor(totalMinutes / 60);
|
||||||
|
const mm = totalMinutes % 60;
|
||||||
|
return {
|
||||||
|
hh: String(hh),
|
||||||
|
mm: String(mm).padStart(2, '0')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseActivityHoursFromInputs(hoursInput, minutesInput, fallbackValue) {
|
||||||
|
const hoursRaw = hoursInput ? hoursInput.value.trim() : '';
|
||||||
|
const minutesRaw = minutesInput ? minutesInput.value.trim() : '';
|
||||||
|
|
||||||
|
if (hoursRaw === '' && minutesRaw === '') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedHours = parseInt(hoursRaw, 10);
|
||||||
|
const parsedMinutes = parseInt(minutesRaw, 10);
|
||||||
|
|
||||||
|
const safeHours = Number.isFinite(parsedHours) && parsedHours >= 0 ? parsedHours : 0;
|
||||||
|
const safeMinutes = Number.isFinite(parsedMinutes) && parsedMinutes >= 0 ? parsedMinutes : 0;
|
||||||
|
const totalMinutes = (safeHours * 60) + safeMinutes;
|
||||||
|
|
||||||
|
if (!Number.isFinite(totalMinutes) || totalMinutes < 0) {
|
||||||
|
return Number.isFinite(fallbackValue) ? fallbackValue : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedHours = Math.floor(totalMinutes / 60);
|
||||||
|
const normalizedMinutes = totalMinutes % 60;
|
||||||
|
if (hoursInput) {
|
||||||
|
hoursInput.value = totalMinutes > 0 ? String(normalizedHours) : '';
|
||||||
|
}
|
||||||
|
if (minutesInput) {
|
||||||
|
minutesInput.value = totalMinutes > 0 ? String(normalizedMinutes).padStart(2, '0') : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalMinutes / 60;
|
||||||
|
}
|
||||||
|
|
||||||
// Woche laden
|
// Woche laden
|
||||||
async function loadWeek() {
|
async function loadWeek() {
|
||||||
try {
|
try {
|
||||||
@@ -603,7 +649,9 @@ function renderWeek() {
|
|||||||
<td colspan="6" class="activities-cell">
|
<td colspan="6" class="activities-cell">
|
||||||
<div class="activities-form">
|
<div class="activities-form">
|
||||||
<div class="activities-header"><strong>Tätigkeiten:</strong></div>
|
<div class="activities-header"><strong>Tätigkeiten:</strong></div>
|
||||||
${activities.map((activity, idx) => `
|
${activities.map((activity, idx) => {
|
||||||
|
const timeParts = decimalHoursToParts(activity.hours);
|
||||||
|
return `
|
||||||
<div class="activity-row">
|
<div class="activity-row">
|
||||||
<div class="activity-desc">
|
<div class="activity-desc">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
@@ -626,22 +674,36 @@ function renderWeek() {
|
|||||||
class="activity-project-input">
|
class="activity-project-input">
|
||||||
</div>
|
</div>
|
||||||
<div class="activity-hours">
|
<div class="activity-hours">
|
||||||
<input type="number"
|
<input type="text"
|
||||||
data-date="${dateStr}"
|
data-date="${dateStr}"
|
||||||
data-field="activity${idx + 1}_hours"
|
data-field="activity${idx + 1}_hours_hh"
|
||||||
value="${activity.hours > 0 ? activity.hours.toFixed(2) : ''}"
|
value="${timeParts.hh}"
|
||||||
min="0"
|
inputmode="numeric"
|
||||||
step="0.25"
|
pattern="[0-9]*"
|
||||||
placeholder="0.00"
|
placeholder="hh"
|
||||||
${timeFieldsDisabled} ${disabled}
|
${timeFieldsDisabled} ${disabled}
|
||||||
onblur="saveEntry(this)"
|
onblur="saveEntry(this)"
|
||||||
oninput="updateOvertimeDisplay();"
|
oninput="updateOvertimeDisplay();"
|
||||||
onchange="updateOvertimeDisplay();"
|
onchange="updateOvertimeDisplay();"
|
||||||
class="activity-hours-input">
|
class="activity-hours-input activity-hours-hh-input">
|
||||||
<span class="activity-hours-label">h</span>
|
<span class="activity-hours-label">h</span>
|
||||||
|
<input type="text"
|
||||||
|
data-date="${dateStr}"
|
||||||
|
data-field="activity${idx + 1}_hours_mm"
|
||||||
|
value="${timeParts.mm}"
|
||||||
|
inputmode="numeric"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
placeholder="mm"
|
||||||
|
${timeFieldsDisabled} ${disabled}
|
||||||
|
onblur="saveEntry(this)"
|
||||||
|
oninput="updateOvertimeDisplay();"
|
||||||
|
onchange="updateOvertimeDisplay();"
|
||||||
|
class="activity-hours-input activity-hours-mm-input">
|
||||||
|
<span class="activity-hours-label">min</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('')}
|
`;
|
||||||
|
}).join('')}
|
||||||
</div>
|
</div>
|
||||||
<div class="overtime-vacation-controls" style="margin-top: 15px; display: flex; gap: 15px; align-items: center;">
|
<div class="overtime-vacation-controls" style="margin-top: 15px; display: flex; gap: 15px; align-items: center;">
|
||||||
<div class="overtime-control">
|
<div class="overtime-control">
|
||||||
@@ -1038,7 +1100,8 @@ function handleOvertimeChange(dateStr, overtimeHours) {
|
|||||||
// (Überstunden werden nur im PDF angezeigt, nicht als Tätigkeit)
|
// (Überstunden werden nur im PDF angezeigt, nicht als Tätigkeit)
|
||||||
for (let i = 1; i <= 5; i++) {
|
for (let i = 1; i <= 5; i++) {
|
||||||
const descInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_desc"]`);
|
const descInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_desc"]`);
|
||||||
const hoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_hours"]`);
|
const hoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_hours_hh"]`);
|
||||||
|
const minutesInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_hours_mm"]`);
|
||||||
|
|
||||||
if (descInput && descInput.value && descInput.value.trim().toLowerCase() === 'überstunden') {
|
if (descInput && descInput.value && descInput.value.trim().toLowerCase() === 'überstunden') {
|
||||||
descInput.value = '';
|
descInput.value = '';
|
||||||
@@ -1047,6 +1110,10 @@ function handleOvertimeChange(dateStr, overtimeHours) {
|
|||||||
hoursInput.value = '';
|
hoursInput.value = '';
|
||||||
saveEntry(hoursInput);
|
saveEntry(hoursInput);
|
||||||
}
|
}
|
||||||
|
if (minutesInput) {
|
||||||
|
minutesInput.value = '';
|
||||||
|
saveEntry(minutesInput);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1155,7 +1222,18 @@ async function saveEntry(input) {
|
|||||||
// Wichtig: Leere Strings werden zu null konvertiert, aber ein Wert sollte vorhanden sein
|
// Wichtig: Leere Strings werden zu null konvertiert, aber ein Wert sollte vorhanden sein
|
||||||
const start_time = actualStartTime;
|
const start_time = actualStartTime;
|
||||||
const end_time = actualEndTime;
|
const end_time = actualEndTime;
|
||||||
let break_minutes = breakInput && breakInput.value ? (parseInt(breakInput.value) || 0) : (parseInt(currentEntries[date].break_minutes) || 0);
|
let break_minutes = defaultBreakMinutes;
|
||||||
|
if (breakInput && breakInput.value !== '') {
|
||||||
|
const parsedBreak = parseInt(breakInput.value, 10);
|
||||||
|
break_minutes = Number.isFinite(parsedBreak) && parsedBreak >= 0 ? parsedBreak : defaultBreakMinutes;
|
||||||
|
} else if (
|
||||||
|
currentEntries[date].break_minutes !== null &&
|
||||||
|
currentEntries[date].break_minutes !== undefined &&
|
||||||
|
currentEntries[date].break_minutes !== ''
|
||||||
|
) {
|
||||||
|
const parsedStoredBreak = parseInt(currentEntries[date].break_minutes, 10);
|
||||||
|
break_minutes = Number.isFinite(parsedStoredBreak) && parsedStoredBreak >= 0 ? parsedStoredBreak : defaultBreakMinutes;
|
||||||
|
}
|
||||||
|
|
||||||
const notes = notesInput ? (notesInput.value || '') : (currentEntries[date].notes || '');
|
const notes = notesInput ? (notesInput.value || '') : (currentEntries[date].notes || '');
|
||||||
const vacation_type = vacationSelect && vacationSelect.value ? vacationSelect.value : (currentEntries[date].vacation_type || null);
|
const vacation_type = vacationSelect && vacationSelect.value ? vacationSelect.value : (currentEntries[date].vacation_type || null);
|
||||||
@@ -1168,12 +1246,16 @@ async function saveEntry(input) {
|
|||||||
const activities = [];
|
const activities = [];
|
||||||
for (let i = 1; i <= 5; i++) {
|
for (let i = 1; i <= 5; i++) {
|
||||||
const descInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_desc"]`);
|
const descInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_desc"]`);
|
||||||
const hoursInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_hours"]`);
|
const hoursInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_hours_hh"]`);
|
||||||
|
const minutesInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_hours_mm"]`);
|
||||||
const projectInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_project_number"]`);
|
const projectInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_project_number"]`);
|
||||||
|
const fallbackHours = parseFloat(currentEntries[date][`activity${i}_hours`]) || 0;
|
||||||
|
|
||||||
activities.push({
|
activities.push({
|
||||||
desc: descInput ? (descInput.value || null) : (currentEntries[date][`activity${i}_desc`] || null),
|
desc: descInput ? (descInput.value || null) : (currentEntries[date][`activity${i}_desc`] || null),
|
||||||
hours: hoursInput ? (parseFloat(hoursInput.value) || 0) : (parseFloat(currentEntries[date][`activity${i}_hours`]) || 0),
|
hours: (hoursInput || minutesInput)
|
||||||
|
? parseActivityHoursFromInputs(hoursInput, minutesInput, fallbackHours)
|
||||||
|
: fallbackHours,
|
||||||
projectNumber: projectInput ? (projectInput.value || null) : (currentEntries[date][`activity${i}_project_number`] || null)
|
projectNumber: projectInput ? (projectInput.value || null) : (currentEntries[date][`activity${i}_project_number`] || null)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1337,21 +1419,27 @@ async function saveEntry(input) {
|
|||||||
// 5. Bei ganztägigem Urlaub: Setze "Urlaub" als erste Tätigkeit und leere andere
|
// 5. Bei ganztägigem Urlaub: Setze "Urlaub" als erste Tätigkeit und leere andere
|
||||||
if (isFullDayVacation) {
|
if (isFullDayVacation) {
|
||||||
const descInput = document.querySelector(`input[data-date="${date}"][data-field="activity1_desc"]`);
|
const descInput = document.querySelector(`input[data-date="${date}"][data-field="activity1_desc"]`);
|
||||||
const hoursInput = document.querySelector(`input[data-date="${date}"][data-field="activity1_hours"]`);
|
const hoursInput = document.querySelector(`input[data-date="${date}"][data-field="activity1_hours_hh"]`);
|
||||||
|
const minutesInput = document.querySelector(`input[data-date="${date}"][data-field="activity1_hours_mm"]`);
|
||||||
|
|
||||||
if (descInput) {
|
if (descInput) {
|
||||||
descInput.value = 'Urlaub';
|
descInput.value = 'Urlaub';
|
||||||
currentEntries[date].activity1_desc = 'Urlaub';
|
currentEntries[date].activity1_desc = 'Urlaub';
|
||||||
}
|
}
|
||||||
if (hoursInput) {
|
if (hoursInput) {
|
||||||
hoursInput.value = fullDayHours.toFixed(2);
|
const fullDayParts = decimalHoursToParts(fullDayHours);
|
||||||
|
hoursInput.value = fullDayParts.hh;
|
||||||
|
if (minutesInput) {
|
||||||
|
minutesInput.value = fullDayParts.mm;
|
||||||
|
}
|
||||||
currentEntries[date].activity1_hours = fullDayHours;
|
currentEntries[date].activity1_hours = fullDayHours;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leere andere Tätigkeiten
|
// Leere andere Tätigkeiten
|
||||||
for (let i = 2; i <= 5; i++) {
|
for (let i = 2; i <= 5; i++) {
|
||||||
const descInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_desc"]`);
|
const descInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_desc"]`);
|
||||||
const hoursInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_hours"]`);
|
const hoursInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_hours_hh"]`);
|
||||||
|
const minutesInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_hours_mm"]`);
|
||||||
const projectInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_project_number"]`);
|
const projectInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_project_number"]`);
|
||||||
|
|
||||||
if (descInput) {
|
if (descInput) {
|
||||||
@@ -1360,6 +1448,9 @@ async function saveEntry(input) {
|
|||||||
}
|
}
|
||||||
if (hoursInput) {
|
if (hoursInput) {
|
||||||
hoursInput.value = '';
|
hoursInput.value = '';
|
||||||
|
if (minutesInput) {
|
||||||
|
minutesInput.value = '';
|
||||||
|
}
|
||||||
currentEntries[date][`activity${i}_hours`] = 0;
|
currentEntries[date][`activity${i}_hours`] = 0;
|
||||||
}
|
}
|
||||||
if (projectInput) {
|
if (projectInput) {
|
||||||
@@ -1371,7 +1462,8 @@ async function saveEntry(input) {
|
|||||||
// Bei Abwahl von Urlaub (nicht full): Alle Tätigkeitsfelder leeren
|
// Bei Abwahl von Urlaub (nicht full): Alle Tätigkeitsfelder leeren
|
||||||
for (let i = 1; i <= 5; i++) {
|
for (let i = 1; i <= 5; i++) {
|
||||||
const descInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_desc"]`);
|
const descInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_desc"]`);
|
||||||
const hoursInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_hours"]`);
|
const hoursInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_hours_hh"]`);
|
||||||
|
const minutesInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_hours_mm"]`);
|
||||||
const projectInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_project_number"]`);
|
const projectInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_project_number"]`);
|
||||||
|
|
||||||
if (descInput) {
|
if (descInput) {
|
||||||
@@ -1380,6 +1472,9 @@ async function saveEntry(input) {
|
|||||||
}
|
}
|
||||||
if (hoursInput) {
|
if (hoursInput) {
|
||||||
hoursInput.value = '';
|
hoursInput.value = '';
|
||||||
|
if (minutesInput) {
|
||||||
|
minutesInput.value = '';
|
||||||
|
}
|
||||||
currentEntries[date][`activity${i}_hours`] = 0;
|
currentEntries[date][`activity${i}_hours`] = 0;
|
||||||
}
|
}
|
||||||
if (projectInput) {
|
if (projectInput) {
|
||||||
@@ -2118,7 +2213,8 @@ function toggleSickStatus(dateStr) {
|
|||||||
// Leere alle Tätigkeitsfelder
|
// Leere alle Tätigkeitsfelder
|
||||||
for (let i = 1; i <= 5; i++) {
|
for (let i = 1; i <= 5; i++) {
|
||||||
const descInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_desc"]`);
|
const descInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_desc"]`);
|
||||||
const hoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_hours"]`);
|
const hoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_hours_hh"]`);
|
||||||
|
const minutesInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_hours_mm"]`);
|
||||||
const projectInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_project_number"]`);
|
const projectInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_project_number"]`);
|
||||||
|
|
||||||
if (descInput) {
|
if (descInput) {
|
||||||
@@ -2127,6 +2223,9 @@ function toggleSickStatus(dateStr) {
|
|||||||
}
|
}
|
||||||
if (hoursInput) {
|
if (hoursInput) {
|
||||||
hoursInput.value = '';
|
hoursInput.value = '';
|
||||||
|
if (minutesInput) {
|
||||||
|
minutesInput.value = '';
|
||||||
|
}
|
||||||
currentEntries[dateStr][`activity${i}_hours`] = 0;
|
currentEntries[dateStr][`activity${i}_hours`] = 0;
|
||||||
}
|
}
|
||||||
if (projectInput) {
|
if (projectInput) {
|
||||||
@@ -2137,7 +2236,8 @@ function toggleSickStatus(dateStr) {
|
|||||||
} else {
|
} else {
|
||||||
// Bei Aktivierung: Setze "Krank" als erste Tätigkeit und leere andere
|
// Bei Aktivierung: Setze "Krank" als erste Tätigkeit und leere andere
|
||||||
const descInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity1_desc"]`);
|
const descInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity1_desc"]`);
|
||||||
const hoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity1_hours"]`);
|
const hoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity1_hours_hh"]`);
|
||||||
|
const minutesInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity1_hours_mm"]`);
|
||||||
const fullDayHours = getFullDayHours();
|
const fullDayHours = getFullDayHours();
|
||||||
|
|
||||||
if (descInput) {
|
if (descInput) {
|
||||||
@@ -2145,14 +2245,19 @@ function toggleSickStatus(dateStr) {
|
|||||||
currentEntries[dateStr].activity1_desc = 'Krank';
|
currentEntries[dateStr].activity1_desc = 'Krank';
|
||||||
}
|
}
|
||||||
if (hoursInput) {
|
if (hoursInput) {
|
||||||
hoursInput.value = fullDayHours.toFixed(2);
|
const fullDayParts = decimalHoursToParts(fullDayHours);
|
||||||
|
hoursInput.value = fullDayParts.hh;
|
||||||
|
if (minutesInput) {
|
||||||
|
minutesInput.value = fullDayParts.mm;
|
||||||
|
}
|
||||||
currentEntries[dateStr].activity1_hours = fullDayHours;
|
currentEntries[dateStr].activity1_hours = fullDayHours;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leere andere Tätigkeiten
|
// Leere andere Tätigkeiten
|
||||||
for (let i = 2; i <= 5; i++) {
|
for (let i = 2; i <= 5; i++) {
|
||||||
const descInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_desc"]`);
|
const descInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_desc"]`);
|
||||||
const hoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_hours"]`);
|
const hoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_hours_hh"]`);
|
||||||
|
const minutesInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_hours_mm"]`);
|
||||||
const projectInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_project_number"]`);
|
const projectInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_project_number"]`);
|
||||||
|
|
||||||
if (descInput) {
|
if (descInput) {
|
||||||
@@ -2161,6 +2266,9 @@ function toggleSickStatus(dateStr) {
|
|||||||
}
|
}
|
||||||
if (hoursInput) {
|
if (hoursInput) {
|
||||||
hoursInput.value = '';
|
hoursInput.value = '';
|
||||||
|
if (minutesInput) {
|
||||||
|
minutesInput.value = '';
|
||||||
|
}
|
||||||
currentEntries[dateStr][`activity${i}_hours`] = 0;
|
currentEntries[dateStr][`activity${i}_hours`] = 0;
|
||||||
}
|
}
|
||||||
if (projectInput) {
|
if (projectInput) {
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ function registerTimesheetRoutes(app) {
|
|||||||
overtime_taken_hours, vacation_type, sick_status, weekend_travel
|
overtime_taken_hours, vacation_type, sick_status, weekend_travel
|
||||||
} = req.body;
|
} = req.body;
|
||||||
const userId = req.session.userId;
|
const userId = req.session.userId;
|
||||||
|
const hasExplicitBreakMinutes = break_minutes !== undefined && break_minutes !== null && break_minutes !== '';
|
||||||
|
const parsedRequestedBreakMinutes = hasExplicitBreakMinutes ? parseInt(break_minutes, 10) : null;
|
||||||
|
const requestedBreakMinutes = Number.isFinite(parsedRequestedBreakMinutes) && parsedRequestedBreakMinutes >= 0
|
||||||
|
? parsedRequestedBreakMinutes
|
||||||
|
: null;
|
||||||
|
|
||||||
// Normalisiere end_time: Leere Strings werden zu null
|
// Normalisiere end_time: Leere Strings werden zu null
|
||||||
const normalizedEndTime = (end_time && typeof end_time === 'string' && end_time.trim() !== '') ? end_time.trim() : (end_time || null);
|
const normalizedEndTime = (end_time && typeof end_time === 'string' && end_time.trim() !== '') ? end_time.trim() : (end_time || null);
|
||||||
@@ -55,7 +60,7 @@ function registerTimesheetRoutes(app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// User-Daten laden (für Überstunden-Berechnung)
|
// User-Daten laden (für Überstunden-Berechnung)
|
||||||
db.get('SELECT wochenstunden, arbeitstage FROM users WHERE id = ?', [userId], (err, user) => {
|
db.get('SELECT wochenstunden, arbeitstage, default_break_minutes FROM users WHERE id = ?', [userId], (err, user) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Fehler beim Laden der User-Daten:', err);
|
console.error('Fehler beim Laden der User-Daten:', err);
|
||||||
return res.status(500).json({ error: 'Fehler beim Laden der User-Daten' });
|
return res.status(500).json({ error: 'Fehler beim Laden der User-Daten' });
|
||||||
@@ -63,6 +68,10 @@ function registerTimesheetRoutes(app) {
|
|||||||
|
|
||||||
const wochenstunden = user?.wochenstunden || 0;
|
const wochenstunden = user?.wochenstunden || 0;
|
||||||
const arbeitstage = user?.arbeitstage || 5;
|
const arbeitstage = user?.arbeitstage || 5;
|
||||||
|
const defaultBreakMinutes = Number.isInteger(user?.default_break_minutes) && user.default_break_minutes >= 0
|
||||||
|
? user.default_break_minutes
|
||||||
|
: 30;
|
||||||
|
let effectiveBreakMinutes = requestedBreakMinutes !== null ? requestedBreakMinutes : defaultBreakMinutes;
|
||||||
const overtimeValue = overtime_taken_hours ? parseFloat(overtime_taken_hours) : 0;
|
const overtimeValue = overtime_taken_hours ? parseFloat(overtime_taken_hours) : 0;
|
||||||
const fullDayHours = wochenstunden > 0 && arbeitstage > 0 ? wochenstunden / arbeitstage : 0;
|
const fullDayHours = wochenstunden > 0 && arbeitstage > 0 ? wochenstunden / arbeitstage : 0;
|
||||||
|
|
||||||
@@ -106,7 +115,7 @@ function registerTimesheetRoutes(app) {
|
|||||||
const start = new Date(`2000-01-01T${normalizedStartTime}`);
|
const start = new Date(`2000-01-01T${normalizedStartTime}`);
|
||||||
const end = new Date(`2000-01-01T${normalizedEndTime}`);
|
const end = new Date(`2000-01-01T${normalizedEndTime}`);
|
||||||
const diffMs = end - start;
|
const diffMs = end - start;
|
||||||
total_hours = (diffMs / (1000 * 60 * 60)) - (break_minutes / 60);
|
total_hours = (diffMs / (1000 * 60 * 60)) - (effectiveBreakMinutes / 60);
|
||||||
|
|
||||||
// Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag)
|
// Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag)
|
||||||
if (isWeekend && isWeekendTravel && total_hours > 0 && !isSick && vacation_type !== 'full') {
|
if (isWeekend && isWeekendTravel && total_hours > 0 && !isSick && vacation_type !== 'full') {
|
||||||
@@ -124,9 +133,26 @@ function registerTimesheetRoutes(app) {
|
|||||||
// Sie werden über overtime_taken_hours in der PDF angezeigt
|
// Sie werden über overtime_taken_hours in der PDF angezeigt
|
||||||
|
|
||||||
// Prüfen ob Eintrag existiert - verwende den neuesten Eintrag falls mehrere existieren
|
// Prüfen ob Eintrag existiert - verwende den neuesten Eintrag falls mehrere existieren
|
||||||
db.get('SELECT id, applied_weekend_percentage FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1',
|
db.get('SELECT id, break_minutes, applied_weekend_percentage FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1',
|
||||||
[userId, date], (err, row) => {
|
[userId, date], (err, row) => {
|
||||||
if (row) {
|
if (row) {
|
||||||
|
if (requestedBreakMinutes === null && row.break_minutes !== null && row.break_minutes !== undefined) {
|
||||||
|
effectiveBreakMinutes = row.break_minutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedStartTime && normalizedEndTime && !isSick && vacation_type !== 'full' && !isFullDayOvertime) {
|
||||||
|
const start = new Date(`2000-01-01T${normalizedStartTime}`);
|
||||||
|
const end = new Date(`2000-01-01T${normalizedEndTime}`);
|
||||||
|
const diffMs = end - start;
|
||||||
|
total_hours = (diffMs / (1000 * 60 * 60)) - (effectiveBreakMinutes / 60);
|
||||||
|
if (isWeekend && isWeekendTravel && total_hours > 0) {
|
||||||
|
const weekendPercentage = getWeekendPercentage(date);
|
||||||
|
if (weekendPercentage >= 100) {
|
||||||
|
total_hours = total_hours * (weekendPercentage / 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Wenn bereits ein gespeicherter Prozentsatz existiert, diesen verwenden (historische Einträge bleiben unverändert)
|
// Wenn bereits ein gespeicherter Prozentsatz existiert, diesen verwenden (historische Einträge bleiben unverändert)
|
||||||
let finalAppliedPercentage = appliedWeekendPercentage;
|
let finalAppliedPercentage = appliedWeekendPercentage;
|
||||||
if (row.applied_weekend_percentage !== null && row.applied_weekend_percentage !== undefined) {
|
if (row.applied_weekend_percentage !== null && row.applied_weekend_percentage !== undefined) {
|
||||||
@@ -138,7 +164,7 @@ function registerTimesheetRoutes(app) {
|
|||||||
const start = new Date(`2000-01-01T${normalizedStartTime}`);
|
const start = new Date(`2000-01-01T${normalizedStartTime}`);
|
||||||
const end = new Date(`2000-01-01T${normalizedEndTime}`);
|
const end = new Date(`2000-01-01T${normalizedEndTime}`);
|
||||||
const diffMs = end - start;
|
const diffMs = end - start;
|
||||||
const baseHours = (diffMs / (1000 * 60 * 60)) - (break_minutes / 60);
|
const baseHours = (diffMs / (1000 * 60 * 60)) - (effectiveBreakMinutes / 60);
|
||||||
if (baseHours > 0 && finalAppliedPercentage >= 100) {
|
if (baseHours > 0 && finalAppliedPercentage >= 100) {
|
||||||
total_hours = baseHours * (finalAppliedPercentage / 100);
|
total_hours = baseHours * (finalAppliedPercentage / 100);
|
||||||
}
|
}
|
||||||
@@ -165,7 +191,7 @@ function registerTimesheetRoutes(app) {
|
|||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
[
|
[
|
||||||
finalStartTime, finalEndTime, break_minutes, total_hours, notes,
|
finalStartTime, finalEndTime, effectiveBreakMinutes, total_hours, notes,
|
||||||
finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null,
|
finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null,
|
||||||
finalActivity2Desc || null, parseFloat(activity2_hours) || 0, activity2_project_number || null,
|
finalActivity2Desc || null, parseFloat(activity2_hours) || 0, activity2_project_number || null,
|
||||||
finalActivity3Desc || null, parseFloat(activity3_hours) || 0, activity3_project_number || null,
|
finalActivity3Desc || null, parseFloat(activity3_hours) || 0, activity3_project_number || null,
|
||||||
@@ -197,7 +223,7 @@ function registerTimesheetRoutes(app) {
|
|||||||
overtime_taken_hours, vacation_type, sick_status, weekend_travel, applied_weekend_percentage)
|
overtime_taken_hours, vacation_type, sick_status, weekend_travel, applied_weekend_percentage)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[
|
[
|
||||||
userId, date, finalStartTime, finalEndTime, break_minutes, total_hours, notes,
|
userId, date, finalStartTime, finalEndTime, effectiveBreakMinutes, total_hours, notes,
|
||||||
finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null,
|
finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null,
|
||||||
finalActivity2Desc || null, parseFloat(activity2_hours) || 0, activity2_project_number || null,
|
finalActivity2Desc || null, parseFloat(activity2_hours) || 0, activity2_project_number || null,
|
||||||
finalActivity3Desc || null, parseFloat(activity3_hours) || 0, activity3_project_number || null,
|
finalActivity3Desc || null, parseFloat(activity3_hours) || 0, activity3_project_number || null,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const { db } = require('../database');
|
|||||||
const { getCurrentDate, getCurrentTime, updateTotalHours } = require('../helpers/utils');
|
const { getCurrentDate, getCurrentTime, updateTotalHours } = require('../helpers/utils');
|
||||||
|
|
||||||
// Ping-Funktion für einen User
|
// Ping-Funktion für einen User
|
||||||
async function pingUserIP(userId, ip, currentDate, currentTime) {
|
async function pingUserIP(userId, ip, defaultBreakMinutes, currentDate, currentTime) {
|
||||||
try {
|
try {
|
||||||
const result = await ping.promise.probe(ip, {
|
const result = await ping.promise.probe(ip, {
|
||||||
timeout: 3,
|
timeout: 3,
|
||||||
@@ -31,6 +31,10 @@ async function pingUserIP(userId, ip, currentDate, currentTime) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userDefaultBreakMinutes = Number.isInteger(defaultBreakMinutes) && defaultBreakMinutes >= 0
|
||||||
|
? defaultBreakMinutes
|
||||||
|
: 30;
|
||||||
|
|
||||||
if (isReachable) {
|
if (isReachable) {
|
||||||
// IP ist erreichbar
|
// IP ist erreichbar
|
||||||
if (!pingStatus) {
|
if (!pingStatus) {
|
||||||
@@ -67,9 +71,9 @@ async function pingUserIP(userId, ip, currentDate, currentTime) {
|
|||||||
});
|
});
|
||||||
} else if (!entry) {
|
} else if (!entry) {
|
||||||
// Kein Eintrag existiert → Erstelle neuen mit start_time
|
// Kein Eintrag existiert → Erstelle neuen mit start_time
|
||||||
db.run(`INSERT INTO timesheet_entries (user_id, date, start_time, updated_at)
|
db.run(`INSERT INTO timesheet_entries (user_id, date, start_time, break_minutes, updated_at)
|
||||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP)`,
|
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
||||||
[userId, currentDate, currentTime], (err) => {
|
[userId, currentDate, currentTime, userDefaultBreakMinutes], (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(`Fehler beim Erstellen des Eintrags für User ${userId}:`, err);
|
console.error(`Fehler beim Erstellen des Eintrags für User ${userId}:`, err);
|
||||||
} else {
|
} else {
|
||||||
@@ -161,7 +165,7 @@ function setupPingService() {
|
|||||||
const currentTime = getCurrentTime();
|
const currentTime = getCurrentTime();
|
||||||
|
|
||||||
// Hole alle User mit IP-Adresse
|
// Hole alle User mit IP-Adresse
|
||||||
db.all('SELECT id, ping_ip FROM users WHERE ping_ip IS NOT NULL AND ping_ip != ""', (err, users) => {
|
db.all('SELECT id, ping_ip, default_break_minutes FROM users WHERE ping_ip IS NOT NULL AND ping_ip != ""', (err, users) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Fehler beim Abrufen der User mit IP-Adressen:', err);
|
console.error('Fehler beim Abrufen der User mit IP-Adressen:', err);
|
||||||
return;
|
return;
|
||||||
@@ -173,7 +177,7 @@ function setupPingService() {
|
|||||||
|
|
||||||
// Ping alle User parallel
|
// Ping alle User parallel
|
||||||
users.forEach(user => {
|
users.forEach(user => {
|
||||||
pingUserIP(user.id, user.ping_ip, currentDate, currentTime);
|
pingUserIP(user.id, user.ping_ip, user.default_break_minutes, currentDate, currentTime);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, 60000); // Jede Minute
|
}, 60000); // Jede Minute
|
||||||
|
|||||||
@@ -145,15 +145,11 @@
|
|||||||
<span class="summary-label">Davon genommen:</span>
|
<span class="summary-label">Davon genommen:</span>
|
||||||
<span class="summary-value" id="totalOvertimeTaken">-</span>
|
<span class="summary-value" id="totalOvertimeTaken">-</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-item">
|
|
||||||
<span class="summary-label">Verbleibend:</span>
|
|
||||||
<span class="summary-value" id="remainingOvertime">-</span>
|
|
||||||
</div>
|
|
||||||
<div class="summary-item" id="offsetItem" style="display: none;">
|
<div class="summary-item" id="offsetItem" style="display: none;">
|
||||||
<span class="summary-label">Manuelle Korrektur (Verwaltung):</span>
|
<span class="summary-label">Manuelle Korrektur (Verwaltung):</span>
|
||||||
<span class="summary-value" id="overtimeOffset">-</span>
|
<span class="summary-value" id="overtimeOffset">-</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="correctionsSection" style="margin-top: 15px; display: none;">
|
<div id="correctionsSection" style="margin-top: 15px; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #e0e0e0; display: none;">
|
||||||
<div id="correctionsHeader" class="collapsible-header" style="cursor: pointer; padding: 12px; background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; display: flex; justify-content: space-between; align-items: center;">
|
<div id="correctionsHeader" class="collapsible-header" style="cursor: pointer; padding: 12px; background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; display: flex; justify-content: space-between; align-items: center;">
|
||||||
<span style="font-weight: 600; color: #2c3e50;">Korrekturen durch die Verwaltung</span>
|
<span style="font-weight: 600; color: #2c3e50;">Korrekturen durch die Verwaltung</span>
|
||||||
<span id="correctionsToggleIcon" style="font-size: 16px; transition: transform 0.3s;">▼</span>
|
<span id="correctionsToggleIcon" style="font-size: 16px; transition: transform 0.3s;">▼</span>
|
||||||
@@ -162,6 +158,10 @@
|
|||||||
<ul id="correctionsList" style="margin: 0; padding-left: 18px;"></ul>
|
<ul id="correctionsList" style="margin: 0; padding-left: 18px;"></ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">Verbleibend:</span>
|
||||||
|
<span class="summary-value" id="remainingOvertime">-</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="loading" class="loading">Lade Daten...</div>
|
<div id="loading" class="loading">Lade Daten...</div>
|
||||||
@@ -323,12 +323,16 @@
|
|||||||
corrections.forEach(c => {
|
corrections.forEach(c => {
|
||||||
const dt = parseSqliteDatetime(c.corrected_at);
|
const dt = parseSqliteDatetime(c.corrected_at);
|
||||||
const dateText = dt ? dt.toLocaleDateString('de-DE') : '';
|
const dateText = dt ? dt.toLocaleDateString('de-DE') : '';
|
||||||
const hoursText = formatHoursMin(c.correction_hours);
|
const rawHours = Number(c.correction_hours) || 0;
|
||||||
|
const absHours = Math.abs(rawHours);
|
||||||
|
const signPrefix = rawHours >= 0 ? '+' : '-';
|
||||||
|
const hoursClass = rawHours >= 0 ? 'overtime-positive' : 'overtime-negative';
|
||||||
|
const hoursDisplay = signPrefix + formatHoursMin(absHours);
|
||||||
const reason = (c && c.reason != null) ? String(c.reason).trim() : '';
|
const reason = (c && c.reason != null) ? String(c.reason).trim() : '';
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.textContent = reason
|
li.innerHTML = reason
|
||||||
? `Korrektur am ${dateText} ${hoursText} – ${reason}`
|
? `Korrektur am ${dateText} <span class="${hoursClass}">${hoursDisplay}</span> – ${reason}`
|
||||||
: `Korrektur am ${dateText} ${hoursText}`;
|
: `Korrektur am ${dateText} <span class="${hoursClass}">${hoursDisplay}</span>`;
|
||||||
correctionsListEl.appendChild(li);
|
correctionsListEl.appendChild(li);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -645,12 +645,16 @@
|
|||||||
corrections.forEach(c => {
|
corrections.forEach(c => {
|
||||||
const dt = parseSqliteDatetime(c.corrected_at);
|
const dt = parseSqliteDatetime(c.corrected_at);
|
||||||
const dateText = dt ? dt.toLocaleDateString('de-DE') : '';
|
const dateText = dt ? dt.toLocaleDateString('de-DE') : '';
|
||||||
const hoursText = formatHoursMin(c.correction_hours);
|
const rawHours = Number(c.correction_hours) || 0;
|
||||||
|
const absHours = Math.abs(rawHours);
|
||||||
|
const signPrefix = rawHours >= 0 ? '+' : '-';
|
||||||
|
const hoursClass = rawHours >= 0 ? 'overtime-positive' : 'overtime-negative';
|
||||||
|
const hoursDisplay = signPrefix + formatHoursMin(absHours);
|
||||||
const reason = (c && c.reason != null) ? String(c.reason).trim() : '';
|
const reason = (c && c.reason != null) ? String(c.reason).trim() : '';
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.textContent = reason
|
li.innerHTML = reason
|
||||||
? `Korrektur am ${dateText} ${hoursText} – ${reason}`
|
? `Korrektur am ${dateText} <span class="${hoursClass}">${hoursDisplay}</span> – ${reason}`
|
||||||
: `Korrektur am ${dateText} ${hoursText}`;
|
: `Korrektur am ${dateText} <span class="${hoursClass}">${hoursDisplay}</span>`;
|
||||||
if (listEl) listEl.appendChild(li);
|
if (listEl) listEl.appendChild(li);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user