Push-Zeitmail: Zeit für Git-Arbeitsblatt eintragen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5b7365b99e
commit
ec3f22d98e
2 files changed
+149
No files matched your search
@@ -0,0 +1,75 @@
|
|||||||
|
import { config } from '../config.js'
|
||||||
|
import { withLocks } from './keyedLock.js'
|
||||||
|
import { getDocument, updateDocument } from './appwriteAdmin.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeit fuer ein Git-Push-Arbeitsblatt eintragen (Push-Zeitmail). Ergebnis wie "Zeit nachtragen" in
|
||||||
|
* WOMS: Startzeit = Push-Zeit (Endzeit) minus Dauer, totalTime = Minuten, isComment = false.
|
||||||
|
* Eine schon eingetragene Zeit wird nie ueberschrieben - Aendern geht nur in WOMS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const MAX_MINUTES = 720
|
||||||
|
|
||||||
|
const pad = (n) => String(n).padStart(2, '0')
|
||||||
|
|
||||||
|
/** Wand-Uhrzeit rueckwaerts rechnen ('tt.mm.jjjj', 'hhmm'); reine Kalenderrechnung, ohne Zeitzone. */
|
||||||
|
export function startFromEnd(endDate, endTime, minutes) {
|
||||||
|
const [day, month, year] = String(endDate).split('.').map(Number)
|
||||||
|
const hours = Number(String(endTime).slice(0, 2))
|
||||||
|
const mins = Number(String(endTime).slice(2, 4))
|
||||||
|
const start = new Date(Date.UTC(year, month - 1, day, hours, mins) - minutes * 60 * 1000)
|
||||||
|
return {
|
||||||
|
startDate: `${pad(start.getUTCDate())}.${pad(start.getUTCMonth() + 1)}.${start.getUTCFullYear()}`,
|
||||||
|
startTime: `${pad(start.getUTCHours())}${pad(start.getUTCMinutes())}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Git-Push ohne eingetragene Zeit (wie needsTimeEntry in WOMS) */
|
||||||
|
export function needsTimeEntry(ws) {
|
||||||
|
return ws?.serviceType === 'GIT' && !(ws.startTime && Number(ws.totalTime) > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ganze Minuten 1-720 aus Formular/Query, sonst null */
|
||||||
|
export function parseMinutes(raw) {
|
||||||
|
if (raw === null || raw === undefined) return null
|
||||||
|
const text = String(raw).trim()
|
||||||
|
if (!/^\d+$/.test(text)) return null
|
||||||
|
const value = Number(text)
|
||||||
|
return value >= 1 && value <= MAX_MINUTES ? value : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 15 -> '15 Min', 60 -> '1 Std', 90 -> '1,5 Std', 130 -> '2 Std 10 Min' */
|
||||||
|
export function formatMinutes(min) {
|
||||||
|
const total = Number(min) || 0
|
||||||
|
if (total < 60) return `${total} Min`
|
||||||
|
const hours = Math.floor(total / 60)
|
||||||
|
const rest = total % 60
|
||||||
|
if (rest === 0) return `${hours} Std`
|
||||||
|
if (rest === 30) return `${hours},5 Std`
|
||||||
|
return `${hours} Std ${rest} Min`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<{ status: 'saved' | 'already' | 'missing' | 'not_git', worksheet?: object }>}
|
||||||
|
*/
|
||||||
|
export async function applyPushTime(worksheetId, minutes) {
|
||||||
|
return withLocks([`ws-time:${worksheetId}`], async () => {
|
||||||
|
let ws
|
||||||
|
try {
|
||||||
|
ws = await getDocument(config.collections.worksheets, worksheetId)
|
||||||
|
} catch {
|
||||||
|
return { status: 'missing' }
|
||||||
|
}
|
||||||
|
if (ws.serviceType !== 'GIT') return { status: 'not_git', worksheet: ws }
|
||||||
|
if (!needsTimeEntry(ws)) return { status: 'already', worksheet: ws }
|
||||||
|
|
||||||
|
const { startDate, startTime } = startFromEnd(ws.endDate, ws.endTime, minutes)
|
||||||
|
const worksheet = await updateDocument(config.collections.worksheets, worksheetId, {
|
||||||
|
startDate,
|
||||||
|
startTime,
|
||||||
|
totalTime: minutes,
|
||||||
|
isComment: false,
|
||||||
|
})
|
||||||
|
return { status: 'saved', worksheet }
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { test, before, after } from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { startFakeAppwrite } from './helpers/fakeAppwrite.js'
|
||||||
|
|
||||||
|
let fake
|
||||||
|
let startFromEnd, needsTimeEntry, parseMinutes, formatMinutes, applyPushTime
|
||||||
|
|
||||||
|
const openGit = (id) => ({
|
||||||
|
$id: id, wsid: '100200', woid: '100118', serviceType: 'GIT',
|
||||||
|
endDate: '30.09.2026', endTime: '1646', startDate: '30.09.2026', startTime: '', totalTime: 0, isComment: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
fake = await startFakeAppwrite({
|
||||||
|
seed: {
|
||||||
|
worksheets: [
|
||||||
|
openGit('ws1'),
|
||||||
|
openGit('ws-race'),
|
||||||
|
{ $id: 'remote1', serviceType: 'Remote', startTime: '', totalTime: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
Object.assign(process.env, { APPWRITE_ENDPOINT: fake.endpoint, APPWRITE_API_KEY: 'test-key' })
|
||||||
|
;({ startFromEnd, needsTimeEntry, parseMinutes, formatMinutes, applyPushTime } = await import(
|
||||||
|
'../server/services/pushTimeEntry.js'
|
||||||
|
))
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await fake.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('startFromEnd: normal, über Mitternacht, Jahres- und Schalttag-Grenze', () => {
|
||||||
|
assert.deepEqual(startFromEnd('30.09.2026', '1646', 60), { startDate: '30.09.2026', startTime: '1546' })
|
||||||
|
assert.deepEqual(startFromEnd('01.10.2026', '0020', 45), { startDate: '30.09.2026', startTime: '2335' })
|
||||||
|
assert.deepEqual(startFromEnd('01.01.2027', '0010', 30), { startDate: '31.12.2026', startTime: '2340' })
|
||||||
|
assert.deepEqual(startFromEnd('01.03.2028', '0000', 1), { startDate: '29.02.2028', startTime: '2359' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('needsTimeEntry', () => {
|
||||||
|
assert.equal(needsTimeEntry({ serviceType: 'GIT', startTime: '', totalTime: 0 }), true)
|
||||||
|
assert.equal(needsTimeEntry({ serviceType: 'GIT', startTime: '1500', totalTime: 45 }), false)
|
||||||
|
assert.equal(needsTimeEntry({ serviceType: 'Remote', startTime: '', totalTime: 0 }), false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parseMinutes: ganze Minuten 1–720', () => {
|
||||||
|
assert.equal(parseMinutes('45'), 45)
|
||||||
|
assert.equal(parseMinutes(720), 720)
|
||||||
|
for (const bad of ['1.5', '0', '721', '', 'abc', undefined, null, '-5', '45abc']) {
|
||||||
|
assert.equal(parseMinutes(bad), null, String(bad))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('formatMinutes', () => {
|
||||||
|
assert.equal(formatMinutes(15), '15 Min')
|
||||||
|
assert.equal(formatMinutes(60), '1 Std')
|
||||||
|
assert.equal(formatMinutes(90), '1,5 Std')
|
||||||
|
assert.equal(formatMinutes(130), '2 Std 10 Min')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('applyPushTime: speichert einmal, überschreibt nie', async () => {
|
||||||
|
assert.equal((await applyPushTime('ws1', 60)).status, 'saved')
|
||||||
|
const ws = fake.get('worksheets', 'ws1')
|
||||||
|
assert.deepEqual([ws.startDate, ws.startTime, ws.totalTime, ws.isComment], ['30.09.2026', '1546', 60, false])
|
||||||
|
assert.equal((await applyPushTime('ws1', 30)).status, 'already')
|
||||||
|
assert.equal(fake.get('worksheets', 'ws1').totalTime, 60)
|
||||||
|
assert.equal((await applyPushTime('gibtsnicht', 30)).status, 'missing')
|
||||||
|
assert.equal((await applyPushTime('remote1', 30)).status, 'not_git')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('gleichzeitig: nur einer speichert', async () => {
|
||||||
|
const results = await Promise.all([applyPushTime('ws-race', 30), applyPushTime('ws-race', 90)])
|
||||||
|
assert.deepEqual(results.map((r) => r.status).sort(), ['already', 'saved'])
|
||||||
|
})
|
||||||
Reference in new issue
Block a user