Files
planer/scripts/zeit.sh
T
Florian WildeandClaude Opus 5 de1f923767 chore: Projektgerüst und Werkzeugkette einrichten
Next 16 mit App Router und standalone-Ausgabe, React 19, TypeScript strict,
Tailwind 4, Vitest und Playwright. Alle Versionen exakt gepinnt.

Abweichungen vom ursprünglich notierten Stand, jeweils begründet:
- Prisma 7.10.0 statt 6, weil 6 überholt ist und 8 nur als RC vorliegt.
- TypeScript 6.0.3 statt 7, weil typescript-eslint 8 nur "<6.1.0" trägt.
- ESLint 9.39.5 statt 10, weil das in eslint-config-next gebündelte
  eslint-plugin-react noch context.getFilename() benutzt.
- npm-overrides für SimpleWebAuthn und nodemailer, weil next-auth dort
  ältere optionale Peers deklariert. Die betroffenen next-auth-Provider
  werden nicht benutzt: Passkeys laufen direkt über SimpleWebAuthn 14.

"npm run pruefen" bündelt Prettier, ESLint, tsc, Vitest und ShellCheck.
ShellCheck läuft über das Docker-Abbild, wenn es lokal fehlt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 12:41:28 +02:00

176 lines
5.4 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Zeiterfassung für den Planer.
# Schreibt nach devlog/zeiten.csv: datum;start;ende;minuten;meilenstein;art;notiz
set -euo pipefail
WURZEL="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CSV="$WURZEL/devlog/zeiten.csv"
KOPF='datum;start;ende;minuten;meilenstein;art;notiz'
OFFEN="$WURZEL/devlog/.zeit-offen"
hilfe() {
cat <<'ENDE'
Verwendung: zeit.sh <befehl> [argumente]
start <Meilenstein> [Notiz] Eintrag öffnen (Art: claude)
stop [Notiz] Offenen Eintrag schließen
nachtrag <Meilenstein> <Minuten> <Art> <Notiz>
Eigene Zeit nachtragen
(Art: review, test, einrichtung)
summe [Meilenstein] Summen je Meilenstein und Art
status Zeigt, ob ein Eintrag offen ist
hilfe Diese Übersicht
ENDE
}
fehler() {
printf 'Fehler: %s\n' "$1" >&2
exit 1
}
# Semikolons und Zeilenumbrüche würden die CSV zerlegen.
saeubern() {
printf '%s' "$1" | tr '\n\r;' ' ' | sed 's/ */ /g; s/^ //; s/ $//'
}
csv_anlegen() {
mkdir -p "$(dirname "$CSV")"
if [ ! -f "$CSV" ]; then
printf '%s\n' "$KOPF" > "$CSV"
fi
}
# Minuten seit Mitternacht, damit auch ohne GNU date gerechnet werden kann.
zu_minuten() {
local uhr="$1"
local h="${uhr%%:*}"
local m="${uhr##*:}"
printf '%s' "$((10#$h * 60 + 10#$m))"
}
befehl_start() {
local meilenstein="${1:-}"
[ -n "$meilenstein" ] || fehler 'Meilenstein fehlt. Beispiel: zeit.sh start M1 "Gerüst"'
shift
local notiz
notiz="$(saeubern "${*:-}")"
if [ -f "$OFFEN" ]; then
local alt
alt="$(cat "$OFFEN")"
fehler "Es läuft bereits ein Eintrag ($alt). Erst 'zeit.sh stop'."
fi
csv_anlegen
mkdir -p "$(dirname "$OFFEN")"
printf '%s;%s;%s;%s\n' "$(date '+%Y-%m-%d')" "$(date '+%H:%M')" \
"$(saeubern "$meilenstein")" "$notiz" > "$OFFEN"
printf 'Gestartet: %s um %s Uhr%s\n' "$meilenstein" "$(date '+%H:%M')" \
"${notiz:+ – $notiz}"
}
befehl_stop() {
[ -f "$OFFEN" ] || fehler "Kein offener Eintrag. Erst 'zeit.sh start <Meilenstein>'."
local zeile datum start meilenstein notiz_start
zeile="$(cat "$OFFEN")"
IFS=';' read -r datum start meilenstein notiz_start <<< "$zeile"
local ende notiz
ende="$(date '+%H:%M')"
notiz="$(saeubern "${*:-}")"
[ -n "$notiz" ] || notiz="$notiz_start"
local von bis minuten
von="$(zu_minuten "$start")"
bis="$(zu_minuten "$ende")"
minuten=$((bis - von))
# Über Mitternacht hinweg.
[ "$minuten" -ge 0 ] || minuten=$((minuten + 1440))
csv_anlegen
printf '%s;%s;%s;%s;%s;claude;%s\n' \
"$datum" "$start" "$ende" "$minuten" "$meilenstein" "$notiz" >> "$CSV"
rm -f "$OFFEN"
printf 'Gestoppt: %s, %s Minuten (%s–%s)\n' "$meilenstein" "$minuten" "$start" "$ende"
}
befehl_nachtrag() {
local meilenstein="${1:-}" minuten="${2:-}" art="${3:-}"
[ -n "$meilenstein" ] || fehler 'Meilenstein fehlt.'
[ -n "$minuten" ] || fehler 'Minuten fehlen.'
[ -n "$art" ] || fehler 'Art fehlt (review, test, einrichtung).'
case "$minuten" in
''|*[!0-9]*) fehler "Minuten müssen eine ganze Zahl sein, nicht '$minuten'." ;;
esac
shift 3
local notiz
notiz="$(saeubern "${*:-}")"
[ -n "$notiz" ] || fehler 'Notiz fehlt.'
csv_anlegen
printf '%s;;;%s;%s;%s;%s\n' \
"$(date '+%Y-%m-%d')" "$minuten" "$(saeubern "$meilenstein")" \
"$(saeubern "$art")" "$notiz" >> "$CSV"
printf 'Nachgetragen: %s Minuten auf %s (%s)\n' "$minuten" "$meilenstein" "$art"
}
befehl_summe() {
local filter="${1:-}"
[ -f "$CSV" ] || fehler "Noch keine Zeiten erfasst ($CSV fehlt)."
awk -F';' -v filter="$filter" '
NR == 1 { next }
$5 == "" { next }
filter != "" && $5 != filter { next }
{
schluessel = $5 ";" $6
summe[schluessel] += $4
je_meilenstein[$5] += $4
gesamt += $4
if (!($5 in gesehen)) { gesehen[$5] = 1; reihenfolge[++anzahl] = $5 }
}
END {
if (gesamt == 0) { print "Keine Einträge gefunden."; exit }
printf "%-14s %-14s %10s %10s\n", "Meilenstein", "Art", "Minuten", "Stunden"
printf "%s\n", "------------------------------------------------------"
for (i = 1; i <= anzahl; i++) {
m = reihenfolge[i]
for (s in summe) {
split(s, teil, ";")
if (teil[1] == m) {
printf "%-14s %-14s %10d %10.2f\n", m, teil[2], summe[s], summe[s] / 60
}
}
printf "%-14s %-14s %10d %10.2f\n", m, "= Summe", je_meilenstein[m], je_meilenstein[m] / 60
printf "%s\n", "------------------------------------------------------"
}
printf "%-29s %10d %10.2f\n", "Gesamt", gesamt, gesamt / 60
}
' "$CSV"
}
befehl_status() {
if [ -f "$OFFEN" ]; then
local zeile datum start meilenstein notiz
zeile="$(cat "$OFFEN")"
IFS=';' read -r datum start meilenstein notiz <<< "$zeile"
printf 'Offen seit %s %s Uhr: %s%s\n' "$datum" "$start" "$meilenstein" "${notiz:+ – $notiz}"
else
printf 'Kein offener Eintrag.\n'
fi
}
befehl="${1:-hilfe}"
shift || true
case "$befehl" in
start) befehl_start "$@" ;;
stop) befehl_stop "$@" ;;
nachtrag) befehl_nachtrag "$@" ;;
summe) befehl_summe "$@" ;;
status) befehl_status ;;
hilfe|-h|--hilfe|--help) hilfe ;;
*) fehler "Unbekannter Befehl '$befehl'. 'zeit.sh hilfe' zeigt die Übersicht." ;;
esac