Compare commits
27 Commits
c2c6ca8fe9
...
v3.12.0
| Author | SHA1 | Date | |
|---|---|---|---|
| a2a1151ccb | |||
| 15e480ee9a | |||
| daaed01d02 | |||
| e7d6c482ad | |||
| ed305f656c | |||
| 30e932a874 | |||
| 0a74e07288 | |||
| 4c3de8c942 | |||
| 49d44d9701 | |||
| 96a7acff33 | |||
| 65588686c7 | |||
| fcf9cc3fc9 | |||
| 20091143d2 | |||
| f708d0a2b2 | |||
| 7e9f4409f6 | |||
| 2e63af1aa2 | |||
| 5ebd2d508c | |||
| 9128fbb165 | |||
| 8473221c13 | |||
| e7554fa2cc | |||
| 7c1fabf58a | |||
| 1203b6cbd1 | |||
| 525f2906fb | |||
| b105382abf | |||
| b1cdec7d17 | |||
| f1d512b2e5 | |||
| db21b43120 |
@@ -2,37 +2,18 @@
|
|||||||
name: 🐛 Bug Report
|
name: 🐛 Bug Report
|
||||||
about: Erstelle einen Bericht, um uns zu helfen, das Projekt zu verbessern.
|
about: Erstelle einen Bericht, um uns zu helfen, das Projekt zu verbessern.
|
||||||
title: '[BUG] '
|
title: '[BUG] '
|
||||||
labels: bug
|
labels: Problem
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Beschreibung**
|
**Beschreibung**
|
||||||
Eine klare und prägnante Beschreibung des Fehlers.
|
|
||||||
|
|
||||||
**Reproduktion**
|
**Reproduktion**
|
||||||
Schritte, um den Fehler zu reproduzieren:
|
|
||||||
|
|
||||||
Entweder:
|
|
||||||
1. Gehe zu '...'
|
|
||||||
2. Klicke auf '...'
|
|
||||||
3. Scrolle runter zu '...'
|
|
||||||
4. Siehe Fehler
|
|
||||||
|
|
||||||
Oder Link zur Seite
|
|
||||||
|
|
||||||
**Erwartetes Verhalten**
|
|
||||||
Eine klare Beschreibung dessen, was du erwartet hast.
|
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
Falls zutreffend, füge hier Screenshots oder Gifs hinzu, um das Problem zu verdeutlichen.
|
|
||||||
|
|
||||||
**Achtung: Achte bitte auf Datenschutz deiner Daten sowie der Daten deiner Kunden. Sollten ein Screenshot nur mit Daten möglich sein, schwärze diese bitte vor dem Upload.**
|
**Achtung: Achte bitte auf Datenschutz deiner Daten sowie der Daten deiner Kunden. Sollten ein Screenshot nur mit Daten möglich sein, schwärze diese bitte vor dem Upload.**
|
||||||
|
|
||||||
**Umgebung:**
|
|
||||||
- Betriebssystem: [z.B. Windows, macOS, Linux]
|
|
||||||
- Browser / Version (falls relevant): [z.B. Chrome 120]
|
|
||||||
- Projekt-Version: [z.B. v1.0.2]
|
|
||||||
|
|
||||||
**Zusätzlicher Kontext**
|
|
||||||
Füge hier alle anderen Informationen zum Problem hinzu.
|
|
||||||
@@ -2,19 +2,16 @@
|
|||||||
name: ✨ Feature Request
|
name: ✨ Feature Request
|
||||||
about: Schlage eine Idee für dieses Projekt vor.
|
about: Schlage eine Idee für dieses Projekt vor.
|
||||||
title: '[FEATURE] '
|
title: '[FEATURE] '
|
||||||
labels: enhancement
|
labels: Funktionswunsch
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Ist dein Feature-Wunsch mit einem Problem verbunden?**
|
**Ist dein Feature-Wunsch mit einem Problem verbunden?**
|
||||||
Eine klare Beschreibung des Problems (z.B. "Ich bin immer genervt, wenn...").
|
|
||||||
|
|
||||||
**Lösungsvorschlag**
|
**Lösungsvorschlag**
|
||||||
Eine klare Beschreibung dessen, was du dir wünschst und wie es funktionieren soll.
|
|
||||||
|
|
||||||
**Alternativen**
|
**Alternativen**
|
||||||
Hast du über alternative Lösungen oder Workarounds nachgedacht?
|
|
||||||
|
|
||||||
**Zusätzlicher Kontext**
|
|
||||||
Hier ist Platz für weitere Informationen, Skizzen oder Beispiele von anderen Tools.
|
|
||||||
@@ -10,8 +10,6 @@ import { TimeService, authorize } from '../src/modules/time-v2/service';
|
|||||||
import { TimeMigration, previewLegacy } from '../src/modules/time-v2/migration';
|
import { TimeMigration, previewLegacy } from '../src/modules/time-v2/migration';
|
||||||
import { legacySpans, installTimeCompatibility, legacyEvent } from '../src/modules/time-v2/compatibility';
|
import { legacySpans, installTimeCompatibility, legacyEvent } from '../src/modules/time-v2/compatibility';
|
||||||
import timeV2Routes from '../src/routes/staff/time-v2';
|
import timeV2Routes from '../src/routes/staff/time-v2';
|
||||||
import { timeSpanInput } from '../../frontend/utils/timeSpanInput';
|
|
||||||
import { staffTimeRows, workingMinutes } from '../../frontend/utils/staffTimeSpans';
|
|
||||||
const at = (day: string, time: string) => `${day}T${time}:00Z`;
|
const at = (day: string, time: string) => `${day}T${time}:00Z`;
|
||||||
const work = (start = '2026-10-01T06:00:00Z', end: string | null = '2026-10-01T14:30:00Z') => normalizeSpan({ type: 'work', started_at: start, ended_at: end,
|
const work = (start = '2026-10-01T06:00:00Z', end: string | null = '2026-10-01T14:30:00Z') => normalizeSpan({ type: 'work', started_at: start, ended_at: end,
|
||||||
breaks: end ? [{ started_at: '2026-10-01T10:00:00Z', ended_at: '2026-10-01T10:30:00Z' }] : [] });
|
breaks: end ? [{ started_at: '2026-10-01T10:00:00Z', ended_at: '2026-10-01T10:30:00Z' }] : [] });
|
||||||
@@ -189,19 +187,18 @@ test('HTTP routes: validation, permissions, compatibility and migration conflict
|
|||||||
await f.close();
|
await f.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
test('editing repeated local times preserves the original instant and rejects ambiguous new inputs', () => {
|
test('legacy compatibility keeps paused attendance on both work and pause segments', () => {
|
||||||
assert.equal(timeSpanInput('2026-10-25T02:15', 'Europe/Berlin', '2026-10-25T01:15:00.000Z'), '2026-10-25T01:15:00.000Z');
|
|
||||||
assert.equal(timeSpanInput('2026-10-25T02:15', 'Europe/Berlin', '2026-10-25T00:15:00.000Z'), '2026-10-25T00:15:00.000Z');
|
|
||||||
assert.throws(() => timeSpanInput('2026-10-25T02:15', 'Europe/Berlin'), /zweimal/);
|
|
||||||
assert.throws(() => timeSpanInput('2026-03-29T02:15', 'Europe/Berlin'), /existiert nicht/);
|
|
||||||
assert.equal(timeSpanInput('2026-10-25T01:15', 'UTC'), '2026-10-25T01:15:00.000Z');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('dashboard keeps a paused attendance active and freezes its net working duration', () => {
|
|
||||||
const s = work(undefined, null); s.id = randomUUID(); s.user_id = randomUUID();
|
const s = work(undefined, null); s.id = randomUUID(); s.user_id = randomUUID();
|
||||||
s.breaks = [{started_at:'2026-10-01T10:00:00Z',ended_at:null}];
|
s.breaks = [{ started_at: '2026-10-01T10:00:00Z', ended_at: null }];
|
||||||
const entries = staffTimeRows(legacySpans([s]),s.user_id,Date.parse('2026-10-01T11:00:00Z'));
|
const segments = legacySpans([s]);
|
||||||
assert.equal(entries.length,1); assert.equal(entries[0].type,'work');
|
assert.equal(segments.length, 2);
|
||||||
assert.equal((entries[0] as any).paused,true);assert.equal(entries[0].stopped_at,null);
|
assert.equal(segments[0].type, 'work');
|
||||||
assert.equal(workingMinutes(entries[0],Date.parse('2026-10-01T12:00:00Z')),240);
|
assert.equal(segments[0].endedAt, s.breaks[0].started_at);
|
||||||
|
assert.equal(segments[1].type, 'pause');
|
||||||
|
assert.equal(segments[1].endedAt, null);
|
||||||
|
for (const segment of segments) {
|
||||||
|
assert.equal(segment.model, 'spans-v2');
|
||||||
|
assert.equal(segment.id, s.id);
|
||||||
|
assert.deepEqual(segment.attendance, s);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ ARG FEDEO_VERSION=0.0.0-dev
|
|||||||
ARG FEDEO_REVISION=unknown
|
ARG FEDEO_REVISION=unknown
|
||||||
ENV FEDEO_VERSION=$FEDEO_VERSION
|
ENV FEDEO_VERSION=$FEDEO_VERSION
|
||||||
ENV FEDEO_REVISION=$FEDEO_REVISION
|
ENV FEDEO_REVISION=$FEDEO_REVISION
|
||||||
RUN npm run build
|
RUN node --test tests/time-v2.test.cjs && npm run build
|
||||||
|
|
||||||
# --- Stage 2: Runtime ---
|
# --- Stage 2: Runtime ---
|
||||||
FROM node:22-alpine AS runner
|
FROM node:22-alpine AS runner
|
||||||
|
|||||||
46
frontend/tests/time-v2.test.cjs
Normal file
46
frontend/tests/time-v2.test.cjs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
const assert = require('node:assert/strict')
|
||||||
|
const test = require('node:test')
|
||||||
|
const fs = require('node:fs')
|
||||||
|
const vm = require('node:vm')
|
||||||
|
const ts = require('typescript')
|
||||||
|
|
||||||
|
function loadUtility(name) {
|
||||||
|
const source = fs.readFileSync(`${__dirname}/../utils/${name}.ts`, 'utf8')
|
||||||
|
const { outputText } = ts.transpileModule(source, {
|
||||||
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2021 },
|
||||||
|
})
|
||||||
|
const exports = {}
|
||||||
|
vm.runInNewContext(outputText, { exports })
|
||||||
|
return exports
|
||||||
|
}
|
||||||
|
|
||||||
|
const { timeSpanInput } = loadUtility('timeSpanInput')
|
||||||
|
const { staffTimeRows, workingMinutes } = loadUtility('staffTimeSpans')
|
||||||
|
|
||||||
|
test('editing repeated local times preserves the original instant and rejects ambiguous new inputs', () => {
|
||||||
|
assert.equal(timeSpanInput('2026-10-25T02:15', 'Europe/Berlin', '2026-10-25T01:15:00.000Z'), '2026-10-25T01:15:00.000Z');
|
||||||
|
assert.equal(timeSpanInput('2026-10-25T02:15', 'Europe/Berlin', '2026-10-25T00:15:00.000Z'), '2026-10-25T00:15:00.000Z');
|
||||||
|
assert.throws(() => timeSpanInput('2026-10-25T02:15', 'Europe/Berlin'), /zweimal/);
|
||||||
|
assert.throws(() => timeSpanInput('2026-03-29T02:15', 'Europe/Berlin'), /existiert nicht/);
|
||||||
|
assert.equal(timeSpanInput('2026-10-25T01:15', 'UTC'), '2026-10-25T01:15:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dashboard keeps a paused attendance active and freezes its net working duration', () => {
|
||||||
|
const attendance = {
|
||||||
|
id: 'attendance-1', user_id: 'user-1', type: 'work', status: 'factual',
|
||||||
|
started_at: '2026-10-01T06:00:00Z', ended_at: null,
|
||||||
|
breaks: [{ started_at: '2026-10-01T10:00:00Z', ended_at: null }],
|
||||||
|
}
|
||||||
|
const base = { id: attendance.id, model: 'spans-v2', attendance }
|
||||||
|
const segments = [
|
||||||
|
{ ...base, type: 'work', startedAt: attendance.started_at, endedAt: attendance.breaks[0].started_at },
|
||||||
|
{ ...base, type: 'pause', startedAt: attendance.breaks[0].started_at, endedAt: null },
|
||||||
|
]
|
||||||
|
const entries = staffTimeRows(segments, attendance.user_id, Date.parse('2026-10-01T11:00:00Z'))
|
||||||
|
assert.equal(entries.length, 1)
|
||||||
|
assert.equal(entries[0].type, 'work')
|
||||||
|
assert.equal(entries[0].paused, true)
|
||||||
|
assert.equal(entries[0].stopped_at, null)
|
||||||
|
assert.equal(entries[0].duration_minutes, 240)
|
||||||
|
assert.equal(workingMinutes(entries[0], Date.parse('2026-10-01T12:00:00Z')), 240)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user