Push-Zeitmail: Zeit nur in 15-Minuten-Schritten

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-30 15:09:12 +00:00
1 parent 7da4452bbd
commit 88854aa2b4
4 files changed
+33 -14

No files matched your search

+15 -5
View File
@@ -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/<token>[?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}}
</style>
</head>
<body><main>
@@ -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(`<option value="${m}"${m === chosen ? ' selected' : ''}>${e(formatMinutes(m))}</option>`)
}
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 ? `<dt>Commits</dt><dd><ul>${commits.map((c) => `<li>${e(c)}</l
${error ? `<div class="err">${e(error)}</div>` : ''}
<form method="post" action="/zeit/${e(token)}">
<label for="minutes">Minuten</label>
<input id="minutes" type="number" name="minutes" min="1" max="${MAX_MINUTES}" step="1" inputmode="numeric" value="${e(minutes ?? '')}" required autofocus>
<select id="minutes" name="minutes" required autofocus>${minuteOptions(minutes)}</select>
<button type="submit">Speichern</button>
</form>
${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.',
})
}
+5 -2
View File
@@ -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' */