diff --git a/server/routes/pushTime.js b/server/routes/pushTime.js index 070c74f..5266e2e 100644 --- a/server/routes/pushTime.js +++ b/server/routes/pushTime.js @@ -3,7 +3,7 @@ import { config } from '../config.js' import { getDocument } from '../services/appwriteAdmin.js' import { escapeHtml } from '../services/mailTemplates.js' import { verifyPushTimeToken } from '../services/pushTimeToken.js' -import { applyPushTime, formatMinutes, needsTimeEntry, parseMinutes, MAX_MINUTES } from '../services/pushTimeEntry.js' +import { applyPushTime, formatMinutes, needsTimeEntry, parseMinutes, MAX_MINUTES, TIME_STEP } from '../services/pushTimeEntry.js' /** * Bestaetigungsseite der Push-Zeitmail: /zeit/[?min=60]. @@ -84,13 +84,13 @@ dt{color:#6b6964;margin-top:8px} dd{margin:0} ul{margin:4px 0 0;padding-left:18px;font-family:Menlo,Consolas,monospace;font-size:13px} label{display:block;font-weight:bold;margin:22px 0 6px} -input[type=number]{width:100%;font-size:20px;padding:12px;border:1px solid #d9d6ce;border-radius:10px} +select{width:100%;font-size:20px;padding:12px;border:1px solid #d9d6ce;border-radius:10px;background:#fff;color:inherit} .err{background:#fdecec;color:#8a1c1c;border-radius:10px;padding:10px 12px;margin-top:16px;font-size:14px} .ok{background:#e6f4ea;color:#1d5b2c;border-radius:10px;padding:10px 12px;margin-top:4px} button,.btn{display:inline-block;margin-top:16px;padding:13px 22px;border-radius:10px;font-size:16px;font-weight:bold;text-decoration:none;cursor:pointer} button{background:#111;color:#fff;border:0;width:100%} .btn{background:#fff;color:#111;border:1px solid #d9d6ce} -@media (prefers-color-scheme:dark){body{background:#161615;color:#f2f1ee}.card{background:#1f1f1e}input[type=number]{background:#2a2a28;color:#f2f1ee;border-color:#3a3a37}button{background:#f2f1ee;color:#161615}.btn{background:#2a2a28;color:#f2f1ee;border-color:#3a3a37}.mut,dt{color:#a8a6a0}} +@media (prefers-color-scheme:dark){body{background:#161615;color:#f2f1ee}.card{background:#1f1f1e}select{background:#2a2a28;color:#f2f1ee;border-color:#3a3a37}button{background:#f2f1ee;color:#161615}.btn{background:#2a2a28;color:#f2f1ee;border-color:#3a3a37}.mut,dt{color:#a8a6a0}}
@@ -133,6 +133,16 @@ async function loadTicket(ws) { } } +// 15 Min bis 12 Std in 15er-Schritten; ohne gueltige Vorgabe ist 1 Std gewaehlt +function minuteOptions(selected) { + const chosen = parseMinutes(selected) ?? 60 + const options = [] + for (let m = TIME_STEP; m <= MAX_MINUTES; m += TIME_STEP) { + options.push(``) + } + return options.join('') +} + function formPage(res, status, { ws, ticket, token, minutes, error }) { const { repo, commits } = pushInfo(ws.details) const woid = ticket?.woid || ws.woid @@ -152,7 +162,7 @@ ${commits.length ? `
Commits
    ${commits.map((c) => `
  • ${e(c)}${e(error)}` : ''}
    - +
    ${openTicket(woid)}` @@ -222,7 +232,7 @@ router.post('/:token', async (req, res) => { ...found, token: req.params.token, minutes: '', - error: `Bitte ganze Minuten zwischen 1 und ${MAX_MINUTES} eingeben.`, + error: 'Bitte eine Zeit in 15-Minuten-Schritten wählen.', }) } diff --git a/server/services/pushTimeEntry.js b/server/services/pushTimeEntry.js index 05f9a0b..20f4c8b 100644 --- a/server/services/pushTimeEntry.js +++ b/server/services/pushTimeEntry.js @@ -37,13 +37,16 @@ export function needsTimeEntry(ws) { return ws?.serviceType === 'GIT' && !(ws.startTime && Number(ws.totalTime) > 0) } -/** Ganze Minuten 1-720 aus Formular/Query, sonst null */ +/** Zeiten nur in 15-Minuten-Schritten (wie WOMS lib/timeSteps.js) */ +export const TIME_STEP = 15 + +/** 15-720 in 15er-Schritten 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 + return value >= TIME_STEP && value <= MAX_MINUTES && value % TIME_STEP === 0 ? value : null } /** 15 -> '15 Min', 60 -> '1 Std', 90 -> '1,5 Std', 130 -> '2 Std 10 Min' */ diff --git a/test/pushTimeEntry.test.js b/test/pushTimeEntry.test.js index 9a4fd94..d13b061 100644 --- a/test/pushTimeEntry.test.js +++ b/test/pushTimeEntry.test.js @@ -44,10 +44,11 @@ test('needsTimeEntry', () => { assert.equal(needsTimeEntry({ serviceType: 'Remote', startTime: '', totalTime: 0 }), false) }) -test('parseMinutes: ganze Minuten 1–720', () => { +test('parseMinutes: nur 15-Minuten-Schritte 15–720', () => { assert.equal(parseMinutes('45'), 45) + assert.equal(parseMinutes('15'), 15) assert.equal(parseMinutes(720), 720) - for (const bad of ['1.5', '0', '721', '', 'abc', undefined, null, '-5', '45abc']) { + for (const bad of ['1.5', '0', '721', '735', '70', '1', '', 'abc', undefined, null, '-5', '-15', '45abc']) { assert.equal(parseMinutes(bad), null, String(bad)) } }) diff --git a/test/pushTimeRoute.test.js b/test/pushTimeRoute.test.js index 035c8ea..6be32d8 100644 --- a/test/pushTimeRoute.test.js +++ b/test/pushTimeRoute.test.js @@ -90,10 +90,15 @@ test('GET speichert nie', async () => { assert.equal(ws.startTime, '') }) -test('GET mit min=60 füllt das Feld vor, Unsinn nicht', async () => { +test('GET: Auswahl in 15-Minuten-Schritten, min=90 vorausgewählt, sonst 1 Std', async () => { const t = token('open1') - assert.match((await get(`/zeit/${t}?min=60`)).html, /name="minutes"[^>]*value="60"/) - assert.match((await get(`/zeit/${t}?min=abc`)).html, /name="minutes"[^>]*value=""/) + const html = (await get(`/zeit/${t}?min=90`)).html + assert.match(html, /