Berücksichtige lokale Datumswerte bei der Berechnung von Abwesenheiten und verbessere die Testabdeckung für Wochenansichten und Arbeitszeiten.
All checks were successful
Deploy production / deploy (push) Successful in 53s
All checks were successful
Deploy production / deploy (push) Successful in 53s
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
const database = require('../config/database');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
// DATEONLY-Werte als Kalendertage behandeln: lokale setDate-Schritte auf
|
||||
// UTC-Mitternacht verschieben beim Sommerzeitwechsel den letzten Tag.
|
||||
const localDateKey = date => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
|
||||
|
||||
/**
|
||||
* Repository für Worklog-Datenbankzugriff
|
||||
* Verwendet Sequelize ORM
|
||||
@@ -171,6 +176,8 @@ class WorklogRepository {
|
||||
async getVacationsByUserInDateRange(userId, startDate, endDate) {
|
||||
const { Vacation } = database.getModels();
|
||||
|
||||
const startKey = localDateKey(startDate);
|
||||
const endKey = localDateKey(endDate);
|
||||
try {
|
||||
// Hole alle Vacation-Einträge, die sich mit dem Datumsbereich überschneiden
|
||||
const vacations = await Vacation.findAll({
|
||||
@@ -179,18 +186,18 @@ class WorklogRepository {
|
||||
[Op.or]: [
|
||||
{
|
||||
first_day: {
|
||||
[Op.between]: [startDate, endDate]
|
||||
[Op.between]: [startKey, endKey]
|
||||
}
|
||||
},
|
||||
{
|
||||
last_day: {
|
||||
[Op.between]: [startDate, endDate]
|
||||
[Op.between]: [startKey, endKey]
|
||||
}
|
||||
},
|
||||
{
|
||||
[Op.and]: [
|
||||
{ first_day: { [Op.lte]: startDate } },
|
||||
{ last_day: { [Op.gte]: endDate } }
|
||||
{ first_day: { [Op.lte]: startKey } },
|
||||
{ last_day: { [Op.gte]: endKey } }
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -207,11 +214,11 @@ class WorklogRepository {
|
||||
const last = new Date(vac.last_day);
|
||||
|
||||
// Iteriere über alle Tage im Urlaubsbereich
|
||||
for (let d = new Date(first); d <= last; d.setDate(d.getDate() + 1)) {
|
||||
for (let d = new Date(first); d <= last; d.setUTCDate(d.getUTCDate() + 1)) {
|
||||
// Nur Tage hinzufügen, die im gewünschten Bereich liegen
|
||||
if (d >= startDate && d <= endDate) {
|
||||
if (d.toISOString().slice(0, 10) >= startKey && d.toISOString().slice(0, 10) <= endKey) {
|
||||
// Überspringe Wochenenden (Samstag=6, Sonntag=0)
|
||||
const dayOfWeek = d.getDay();
|
||||
const dayOfWeek = d.getUTCDay();
|
||||
if (dayOfWeek !== 0 && dayOfWeek !== 6) {
|
||||
expandedVacations.push({
|
||||
date: new Date(d),
|
||||
@@ -240,6 +247,8 @@ class WorklogRepository {
|
||||
async getSickByUserInDateRange(userId, startDate, endDate) {
|
||||
const { Sick, SickType } = database.getModels();
|
||||
|
||||
const startKey = localDateKey(startDate);
|
||||
const endKey = localDateKey(endDate);
|
||||
try {
|
||||
// Hole alle Sick-Einträge, die sich mit dem Datumsbereich überschneiden
|
||||
const sickEntries = await Sick.findAll({
|
||||
@@ -248,18 +257,18 @@ class WorklogRepository {
|
||||
[Op.or]: [
|
||||
{
|
||||
first_day: {
|
||||
[Op.between]: [startDate, endDate]
|
||||
[Op.between]: [startKey, endKey]
|
||||
}
|
||||
},
|
||||
{
|
||||
last_day: {
|
||||
[Op.between]: [startDate, endDate]
|
||||
[Op.between]: [startKey, endKey]
|
||||
}
|
||||
},
|
||||
{
|
||||
[Op.and]: [
|
||||
{ first_day: { [Op.lte]: startDate } },
|
||||
{ last_day: { [Op.gte]: endDate } }
|
||||
{ first_day: { [Op.lte]: startKey } },
|
||||
{ last_day: { [Op.gte]: endKey } }
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -281,11 +290,11 @@ class WorklogRepository {
|
||||
const sickTypeDesc = sick.sickType?.description || 'self';
|
||||
|
||||
// Iteriere über alle Tage im Krankheitsbereich
|
||||
for (let d = new Date(first); d <= last; d.setDate(d.getDate() + 1)) {
|
||||
for (let d = new Date(first); d <= last; d.setUTCDate(d.getUTCDate() + 1)) {
|
||||
// Nur Tage hinzufügen, die im gewünschten Bereich liegen
|
||||
if (d >= startDate && d <= endDate) {
|
||||
if (d.toISOString().slice(0, 10) >= startKey && d.toISOString().slice(0, 10) <= endKey) {
|
||||
// Überspringe Wochenenden (Samstag=6, Sonntag=0)
|
||||
const dayOfWeek = d.getDay();
|
||||
const dayOfWeek = d.getUTCDay();
|
||||
if (dayOfWeek !== 0 && dayOfWeek !== 6) {
|
||||
expandedSick.push({
|
||||
date: new Date(d),
|
||||
@@ -358,7 +367,7 @@ class WorklogRepository {
|
||||
where: {
|
||||
user_id: userId,
|
||||
tstamp: {
|
||||
[Op.between]: [startDate, endDate]
|
||||
[Op.between]: [startKey, endKey]
|
||||
}
|
||||
},
|
||||
order: [['tstamp', 'ASC']],
|
||||
@@ -429,7 +438,7 @@ class WorklogRepository {
|
||||
where: {
|
||||
user_id: userId,
|
||||
tstamp: {
|
||||
[Op.between]: [startDate, endDate]
|
||||
[Op.between]: [startKey, endKey]
|
||||
}
|
||||
},
|
||||
order: [['tstamp', 'ASC']] // Aufsteigend, damit Start vor Stop kommt
|
||||
|
||||
@@ -275,7 +275,6 @@ class TimeEntryService {
|
||||
const userDailyHours = user?.daily_hours || 8;
|
||||
|
||||
let currentlyWorked = null;
|
||||
let allEntries = [];
|
||||
|
||||
// Hole laufenden Eintrag
|
||||
const runningEntry = await this.getRunningEntry(uid);
|
||||
@@ -284,13 +283,18 @@ class TimeEntryService {
|
||||
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0);
|
||||
const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
|
||||
|
||||
const todayBlocks = this._buildEffectiveWorkBlocks(await this._getEffectiveEntries(uid))
|
||||
.filter(block => {
|
||||
const start = new Date(block.start.effectiveTime);
|
||||
return start >= todayStart && start <= todayEnd;
|
||||
});
|
||||
|
||||
if (runningEntry) {
|
||||
// Arbeitszeit wird aus der korrigierten Ereignisfolge gebildet. Damit
|
||||
// darf eine nachträglich umtypisierte Buchung keine Phantompause mehr
|
||||
// erzeugen.
|
||||
const blocks = this._buildEffectiveWorkBlocks(await this._getEffectiveEntries(uid));
|
||||
let totalWorkedMs = 0;
|
||||
for (const block of blocks) {
|
||||
for (const block of todayBlocks) {
|
||||
const start = new Date(block.start.effectiveTime);
|
||||
if (start < todayStart || start > todayEnd) continue;
|
||||
const endMs = block.end ? new Date(block.end.effectiveTime).getTime() : now.getTime();
|
||||
@@ -419,38 +423,11 @@ class TimeEntryService {
|
||||
requiredBreakMinutes = 30;
|
||||
}
|
||||
|
||||
// Berechne ALLE genommenen Pausen von heute (verwende bereits geladene Einträge)
|
||||
if (allEntries.length > 0) {
|
||||
// Sammle alle abgeschlossenen Pausen
|
||||
const pauseStarts = {};
|
||||
let totalPauseMs = 0;
|
||||
|
||||
allEntries.forEach(entry => {
|
||||
let state = entry.state;
|
||||
if (typeof state === 'string') {
|
||||
try {
|
||||
state = JSON.parse(state);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const action = state?.action || state;
|
||||
|
||||
if (action === 'start pause') {
|
||||
pauseStarts[entry.id] = entry;
|
||||
} else if (action === 'stop pause' && entry.relatedTo_id) {
|
||||
const startPause = pauseStarts[entry.relatedTo_id];
|
||||
if (startPause) {
|
||||
const pauseDuration = new Date(entry.tstamp).getTime() - new Date(startPause.tstamp).getTime();
|
||||
totalPauseMs += pauseDuration;
|
||||
delete pauseStarts[entry.relatedTo_id];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
alreadyTakenBreakMinutes = Math.floor(totalPauseMs / (1000 * 60));
|
||||
}
|
||||
const takenBreakMs = todayBlocks.reduce((sum, block) => sum +
|
||||
block.pauses.reduce((pauseSum, pause) => pauseSum +
|
||||
new Date(pause.end.effectiveTime).getTime() - new Date(pause.start.effectiveTime).getTime(), 0) +
|
||||
(block.runningPause ? now - new Date(block.runningPause.effectiveTime) : 0), 0);
|
||||
alreadyTakenBreakMinutes = Math.floor(takenBreakMs / 60000);
|
||||
}
|
||||
|
||||
// Fehlende Pausenzeit
|
||||
@@ -596,8 +573,13 @@ class TimeEntryService {
|
||||
const overtime = `${overtimeSign}${overtimeHours}:${overtimeMins.toString().padStart(2, '0')}`;
|
||||
|
||||
// Wochenarbeitszeit
|
||||
const weekWorktimeHours = Math.floor(weekIstMinutes / 60);
|
||||
const weekWorktimeMins = weekIstMinutes % 60;
|
||||
const weekWorkMinutes = weekData.days.reduce((sum, day) => {
|
||||
if (!day.netWorkTime) return sum;
|
||||
const [h, m] = day.netWorkTime.split(':').map(Number);
|
||||
return sum + h * 60 + m;
|
||||
}, 0);
|
||||
const weekWorktimeHours = Math.floor(weekWorkMinutes / 60);
|
||||
const weekWorktimeMins = weekWorkMinutes % 60;
|
||||
const weekWorktime = `${weekWorktimeHours}:${weekWorktimeMins.toString().padStart(2, '0')}`;
|
||||
|
||||
// Berechne "Offen für Woche"
|
||||
@@ -718,7 +700,7 @@ class TimeEntryService {
|
||||
const openH = parseInt(openParts[0]);
|
||||
const openM = parseInt(openParts[1]);
|
||||
const openS = parseInt(openParts[2] || 0);
|
||||
const openMinutes = openH * 60 + openM;
|
||||
const openMinutes = openH * 60 + openM + openS / 60;
|
||||
|
||||
// Aktuelle Zeit
|
||||
const currentTime = new Date();
|
||||
@@ -1311,6 +1293,7 @@ class TimeEntryService {
|
||||
}))
|
||||
.sort((a, b) => new Date(a.effectiveTime) - new Date(b.effectiveTime) || a.id - b.id);
|
||||
const pairs = this._buildEffectiveWorkBlocks(effectiveEntries).map(block => ({
|
||||
effectiveBlock: block,
|
||||
id: block.start.id,
|
||||
end_id: block.end?.id || null,
|
||||
start_time: block.start.tstamp,
|
||||
@@ -1738,6 +1721,18 @@ class TimeEntryService {
|
||||
};
|
||||
});
|
||||
|
||||
const block = pair.effectiveBlock;
|
||||
const elapsedMs = Math.max(0, now - new Date(block.start.effectiveTime));
|
||||
const pauseMs = block.pauses.reduce((sum, pause) => sum +
|
||||
new Date(pause.end.effectiveTime).getTime() - new Date(pause.start.effectiveTime).getTime(), 0) +
|
||||
(block.runningPause ? now - new Date(block.runningPause.effectiveTime) : 0);
|
||||
const formatDuration = ms => {
|
||||
const minutes = Math.floor(Math.max(0, ms) / 60000);
|
||||
return `${Math.floor(minutes / 60)}:${String(minutes % 60).padStart(2, '0')}`;
|
||||
};
|
||||
workBlock.totalWorkTime = formatDuration(elapsedMs);
|
||||
workBlock.netWorkTime = formatDuration(elapsedMs - pauseMs);
|
||||
|
||||
// Füge laufenden Block zum Tag hinzu
|
||||
day.workBlocks.push(workBlock);
|
||||
|
||||
@@ -1822,12 +1817,7 @@ class TimeEntryService {
|
||||
};
|
||||
events.push(sickLabels[sick.sick_type] || 'Krank');
|
||||
|
||||
// Bei Krankheit: Lösche geloggte Arbeitszeit
|
||||
day.workTime = null;
|
||||
day.pauses = [];
|
||||
day.totalWorkTime = null;
|
||||
day.pauseTimes = [];
|
||||
day.netWorkTime = null;
|
||||
// Tatsächlich gebuchte Arbeit bleibt zusätzlich sichtbar.
|
||||
}
|
||||
|
||||
// Prüfe auf Feiertag
|
||||
@@ -1886,11 +1876,11 @@ class TimeEntryService {
|
||||
}
|
||||
});
|
||||
|
||||
// Berechne die reguläre Arbeitszeit ohne arbeitsfreie Tage. Der
|
||||
// Urlaubs-/Krankheits-/Feiertagsanteil wird weiter unten separat ergänzt.
|
||||
// Tatsächliche Arbeitszeit einschließlich laufender Blöcke.
|
||||
// Abwesenheitsgutschriften werden separat ergänzt.
|
||||
let workMinutes = 0;
|
||||
Object.values(dayData).forEach(day => {
|
||||
if (day.netWorkTime && !day.vacation && !day.sick && !day.holiday) {
|
||||
if (day.netWorkTime) {
|
||||
const [hours, minutes] = day.netWorkTime.split(':').map(Number);
|
||||
workMinutes += hours * 60 + minutes;
|
||||
}
|
||||
@@ -1902,7 +1892,7 @@ class TimeEntryService {
|
||||
const nonWorkingDetails = [];
|
||||
|
||||
Object.values(dayData).forEach(day => {
|
||||
if (day.vacation && day.vacation.hours > 0) {
|
||||
if (!day.sick && !day.holiday && day.vacation && day.vacation.hours > 0) {
|
||||
nonWorkingMinutes += day.vacation.hours * 60;
|
||||
nonWorkingDays++;
|
||||
nonWorkingDetails.push({
|
||||
@@ -1920,7 +1910,7 @@ class TimeEntryService {
|
||||
hours: day.sick.hours
|
||||
});
|
||||
}
|
||||
if (day.holiday && day.holiday.hours > 0) {
|
||||
if (!day.sick && day.holiday && day.holiday.hours > 0) {
|
||||
nonWorkingMinutes += day.holiday.hours * 60;
|
||||
nonWorkingDays++;
|
||||
nonWorkingDetails.push({
|
||||
@@ -1935,10 +1925,8 @@ class TimeEntryService {
|
||||
const nonWorkingMins = nonWorkingMinutes % 60;
|
||||
const nonWorkingTotal = `${nonWorkingHours}:${nonWorkingMins.toString().padStart(2, '0')}`;
|
||||
|
||||
// Die Wochensumme enthält die reguläre Arbeitszeit und die Gutschrift für
|
||||
// arbeitsfreie Tage. Die Gesamtsumme weist den Anteil arbeitsfreier Tage
|
||||
// zusätzlich aus.
|
||||
const weekTotalMinutes = workMinutes + nonWorkingMinutes;
|
||||
// Wochensumme = tatsächliche Arbeit; Gesamtsumme = Arbeit + Gutschriften.
|
||||
const weekTotalMinutes = workMinutes;
|
||||
const weekTotalHours = Math.floor(weekTotalMinutes / 60);
|
||||
const weekTotalMins = weekTotalMinutes % 60;
|
||||
const weekTotal = `${weekTotalHours}:${weekTotalMins.toString().padStart(2, '0')}`;
|
||||
@@ -1949,8 +1937,8 @@ class TimeEntryService {
|
||||
const totalAll = `${totalAllHours}:${totalAllMins.toString().padStart(2, '0')}`;
|
||||
|
||||
return {
|
||||
weekStart: weekStart.toISOString().split('T')[0],
|
||||
weekEnd: weekEnd.toISOString().split('T')[0],
|
||||
weekStart: `${weekStart.getFullYear()}-${String(weekStart.getMonth() + 1).padStart(2, '0')}-${String(weekStart.getDate()).padStart(2, '0')}`,
|
||||
weekEnd: `${weekEnd.getFullYear()}-${String(weekEnd.getMonth() + 1).padStart(2, '0')}-${String(weekEnd.getDate()).padStart(2, '0')}`,
|
||||
weekOffset,
|
||||
days: Object.values(dayData).sort((a, b) => a.date.localeCompare(b.date)),
|
||||
weekTotal,
|
||||
|
||||
103
backend/test/time-displays.test.js
Normal file
103
backend/test/time-displays.test.js
Normal file
@@ -0,0 +1,103 @@
|
||||
process.env.TZ = 'Europe/Berlin';
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const database = require('../src/config/database');
|
||||
const repository = require('../src/repositories/WorklogRepository');
|
||||
const service = require('../src/services/TimeEntryService');
|
||||
const RealDate = Date;
|
||||
|
||||
function freezeTime(t, time = '2026-10-27T14:00:00') {
|
||||
global.Date = class extends RealDate {
|
||||
constructor(...args) { super(...(args.length ? args : [time])); }
|
||||
static now() { return new RealDate(time).getTime(); }
|
||||
};
|
||||
t.after(() => { global.Date = RealDate; });
|
||||
}
|
||||
|
||||
function mock(t, object, key, value) {
|
||||
const original = object[key];
|
||||
object[key] = value;
|
||||
t.after(() => { object[key] = original; });
|
||||
}
|
||||
|
||||
for (const [name, model, method] of [
|
||||
['Krankheit', 'Sick', 'getSickByUserInDateRange'],
|
||||
['Urlaub', 'Vacation', 'getVacationsByUserInDateRange']
|
||||
]) {
|
||||
test(`${name}: letzter Tag nach Zeitumstellung wird berücksichtigt`, async t => {
|
||||
mock(t, database, 'getModels', () => ({
|
||||
[model]: { findAll: async () => [{ first_day: '2026-10-06', last_day: '2026-10-26', vacation_type: 2 }] }
|
||||
}));
|
||||
const days = await repository[method](1, new Date('2026-10-26T00:00:00'), new Date('2026-11-01T23:59:59'));
|
||||
assert.deepEqual(days.map(day => day.date.toISOString().slice(0, 10)), ['2026-10-26']);
|
||||
});
|
||||
test(`${name}: eintägige Abwesenheit an der Wochengrenze`, async t => {
|
||||
mock(t, database, 'getModels', () => ({
|
||||
[model]: { findAll: async () => [{ first_day: '2026-10-26', last_day: '2026-10-26', vacation_type: 1 }] }
|
||||
}));
|
||||
const days = await repository[method](1, new Date('2026-10-26T00:00:00'), new Date('2026-11-01T23:59:59'));
|
||||
assert.equal(days.length, 1);
|
||||
assert.equal(days[0].date.toISOString().slice(0, 10), '2026-10-26');
|
||||
});
|
||||
}
|
||||
|
||||
function setupWeek(t, { pause = false, sick = true, halfVacation = false } = {}) {
|
||||
freezeTime(t);
|
||||
const entries = [
|
||||
{ id: 1, state: 'start work', tstamp: '2026-10-27 08:00:00' },
|
||||
{ id: 2, state: 'start pause', tstamp: '2026-10-27 10:00:00', relatedTo_id: 1 },
|
||||
...(!pause ? [{ id: 3, state: 'stop pause', tstamp: '2026-10-27 10:30:00', relatedTo_id: 2 }] : [])
|
||||
];
|
||||
mock(t, repository, 'findByDateRange', async () => entries);
|
||||
mock(t, repository, 'findByUser', async () => entries);
|
||||
mock(t, repository, 'getTimefixesByWorklogIds', async () => new Map());
|
||||
mock(t, repository, 'getSickByUserInDateRange', async () => sick ? [{ date: new Date('2026-10-26'), sick_type: 'self' }] : []);
|
||||
mock(t, repository, 'getVacationsByUserInDateRange', async () => halfVacation ? [{ date: new Date('2026-10-27'), half_day: 1 }] : []);
|
||||
mock(t, repository, 'getHolidaysInDateRange', async () => []);
|
||||
}
|
||||
|
||||
test('Wochenansicht: lokale Wochengrenzen, laufende Arbeit und Krankheit genau einmal', async t => {
|
||||
setupWeek(t);
|
||||
const week = await service.getWeekOverview(1);
|
||||
assert.equal(week.weekStart, '2026-10-26');
|
||||
assert.equal(week.weekEnd, '2026-11-01');
|
||||
assert.equal(week.days[0].sick.hours, 8);
|
||||
assert.equal(week.days[1].workBlocks[0].netWorkTime, '5:30');
|
||||
assert.equal(week.weekTotal, '5:30');
|
||||
assert.equal(week.nonWorkingTotal, '8:00');
|
||||
assert.equal(week.totalAll, '13:30');
|
||||
});
|
||||
|
||||
test('Wochenansicht: laufende Pause friert Nettozeit ein', async t => {
|
||||
setupWeek(t, { pause: true });
|
||||
const week = await service.getWeekOverview(1);
|
||||
assert.equal(week.days[1].workBlocks[0].netWorkTime, '2:00');
|
||||
assert.equal(week.weekTotal, '2:00');
|
||||
});
|
||||
|
||||
test('Wochenansicht: Arbeit am halben Urlaubstag geht nicht verloren', async t => {
|
||||
setupWeek(t, { sick: false, halfVacation: true });
|
||||
const week = await service.getWeekOverview(1);
|
||||
assert.equal(week.weekTotal, '5:30');
|
||||
assert.equal(week.nonWorkingTotal, '4:00');
|
||||
assert.equal(week.totalAll, '9:30');
|
||||
});
|
||||
|
||||
test('Status: keine Fehlzeit für kranken Montag, Pausen angerechnet, laufende Wochenzeit', async t => {
|
||||
setupWeek(t);
|
||||
mock(t, database, 'getModels', () => ({
|
||||
User: { findByPk: async () => ({ daily_hours: 8 }) },
|
||||
Timewish: { findOne: async () => null, findAll: async () => [] }
|
||||
}));
|
||||
mock(t, service, '_calculateTotalOvertime', async () => ({ minutes: 349, formatted: '+5:49' }));
|
||||
const stats = await service.getStatistics(1);
|
||||
assert.equal(stats.currentlyWorked, '05:30:00');
|
||||
assert.equal(stats.overtime, '+0:00');
|
||||
assert.equal(stats.weekWorktime, '5:30');
|
||||
assert.equal(stats.nonWorkingHours, '8:00');
|
||||
assert.equal(stats.missingBreakMinutes, 0);
|
||||
assert.equal(stats.open, '02:30:00');
|
||||
assert.equal(stats.openForWeek, '2:30');
|
||||
assert.equal(stats.adjustedEndTodayWeek, '16:30');
|
||||
assert.equal(stats.totalOvertime, '+5:49');
|
||||
});
|
||||
@@ -88,6 +88,8 @@ const fetchStats = async () => {
|
||||
missingBreakMinutes.value = 0
|
||||
}
|
||||
|
||||
updateCurrentlyWorkedTime()
|
||||
updateOpenTime()
|
||||
isReady.value = true
|
||||
}
|
||||
|
||||
@@ -164,7 +166,7 @@ const updateCurrentlyWorkedTime = () => {
|
||||
}
|
||||
|
||||
// Verwende Server-Werte nur, wenn NICHT gearbeitet wird (oder kein laufender Start vorhanden)
|
||||
const shouldUseServerSummary = !workStartTime.value
|
||||
const shouldUseServerSummary = Boolean(serverWorkedTime.value)
|
||||
|
||||
if (shouldUseServerSummary && serverWorkedTime.value && serverTimestamp.value) {
|
||||
// Parse Server-Zeit (HH:MM:SS)
|
||||
@@ -406,11 +408,9 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
|
||||
const displayRows = computed(() => {
|
||||
// Verwende Server-Wert für "Derzeit gearbeitet" wenn verfügbar (zeigt gesamte Tagesarbeitszeit)
|
||||
const workedDisplay = (stats.value?.currentlyWorked && stats.value.currentlyWorked !== '—')
|
||||
? stats.value.currentlyWorked + ' h'
|
||||
: (currentlyWorkedTime.value === '—' ? currentlyWorkedTime.value : currentlyWorkedTime.value + ' h');
|
||||
|
||||
const workedDisplay = currentlyWorkedTime.value === '—'
|
||||
? '—' : currentlyWorkedTime.value + ' h';
|
||||
|
||||
const rows = {
|
||||
'Derzeit gearbeitet': workedDisplay,
|
||||
'Offen': openTime.value === '—' || openTime.value.includes('erreicht') ? openTime.value : openTime.value + ' h',
|
||||
|
||||
@@ -196,6 +196,7 @@ async function createSick() {
|
||||
|
||||
// Liste neu laden
|
||||
await loadSickEntries()
|
||||
window.dispatchEvent(new CustomEvent('worklog-updated'))
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen des Krankheitseintrags:', error)
|
||||
await alert(`Fehler: ${error.message}`, 'Fehler')
|
||||
@@ -227,6 +228,7 @@ async function deleteSick(id) {
|
||||
}
|
||||
|
||||
await loadSickEntries()
|
||||
window.dispatchEvent(new CustomEvent('worklog-updated'))
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen des Krankheitseintrags:', error)
|
||||
await alert(`Fehler: ${error.message}`, 'Fehler')
|
||||
|
||||
@@ -181,6 +181,7 @@ async function createVacation() {
|
||||
|
||||
resetForm()
|
||||
await loadVacations()
|
||||
window.dispatchEvent(new CustomEvent('worklog-updated'))
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen des Urlaubseintrags:', error)
|
||||
await alert(`Fehler: ${error.message}`, 'Fehler')
|
||||
@@ -210,6 +211,7 @@ async function deleteVacation(id) {
|
||||
}
|
||||
|
||||
await loadVacations()
|
||||
window.dispatchEvent(new CustomEvent('worklog-updated'))
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen des Urlaubseintrags:', error)
|
||||
await alert(`Fehler: ${error.message}`, 'Fehler')
|
||||
|
||||
@@ -103,8 +103,7 @@
|
||||
<div v-if="block.totalWorkTime" class="total-work-time">
|
||||
{{ block.totalWorkTime }}
|
||||
</div>
|
||||
<!-- Laufende Blöcke haben keine Gesamtarbeitszeit. Der Platzhalter hält
|
||||
die anschließenden Pausensummen auf Höhe der Pausenzeit. -->
|
||||
<!-- Platzhalter für ältere API-Antworten ohne laufende Dauer. -->
|
||||
<div v-else-if="block.pauseTimes && block.pauseTimes.length > 0" class="total-work-time" aria-hidden="true">
|
||||
|
||||
</div>
|
||||
@@ -204,6 +203,7 @@ const authStore = useAuthStore()
|
||||
const loading = ref(true)
|
||||
const weekData = ref(null)
|
||||
const weekOffset = ref(0)
|
||||
let refreshInterval = null
|
||||
|
||||
const weekDays = computed(() => {
|
||||
if (!weekData.value) return []
|
||||
@@ -237,9 +237,9 @@ const formatDate = (dateString) => {
|
||||
})
|
||||
}
|
||||
|
||||
const loadWeekData = async () => {
|
||||
const loadWeekData = async (background = false) => {
|
||||
try {
|
||||
loading.value = true
|
||||
if (!background) loading.value = true
|
||||
|
||||
const response = await fetch(`${API_URL}/week-overview?weekOffset=${weekOffset.value}`, {
|
||||
cache: 'no-store',
|
||||
@@ -381,12 +381,14 @@ const handleWorklogUpdate = () => {
|
||||
|
||||
onMounted(() => {
|
||||
loadWeekData()
|
||||
refreshInterval = setInterval(() => loadWeekData(true), 60000)
|
||||
|
||||
// Event-Listener für Worklog-Updates
|
||||
window.addEventListener('worklog-updated', handleWorklogUpdate)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (refreshInterval) clearInterval(refreshInterval)
|
||||
window.removeEventListener('worklog-updated', handleWorklogUpdate)
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user