6 Commits

Author SHA1 Message Date
spok 1b904c597b Add explicit ask mode 2026-06-29 18:20:53 +02:00
spok e8cd6e2a91 Show help when no arguments are provided 2026-06-29 18:08:52 +02:00
spok 5fdf2e7cb0 Respect signed balance in interactive mode 2026-06-29 17:51:14 +02:00
spok 0de4425bff Limit counted work time to 06-20 2026-06-29 17:41:06 +02:00
spok bb58bdfaee Use signed daily balance for net calculations 2026-06-29 17:14:48 +02:00
spok 94a0b86620 Add gross time and daily balance calculation 2026-06-29 17:04:03 +02:00
4 changed files with 503 additions and 103 deletions
+64 -9
View File
@@ -14,30 +14,82 @@ Ausgabe:
```text
Netto-Zeit 06:01:14
+ Minuszeit 01:51:46
Tagesaldo -01:51:46
= Sollzeit 07:53:00
```
Ohne Argumente fragt das Programm die Zeiten interaktiv im Terminal ab. Dabei
wird kein Webserver gestartet.
Ohne Argumente zeigt das Programm nur die Hilfe an. Berechnet wird erst, wenn
passende Werte uebergeben werden. Beim Tages-Saldo bitte `+` oder `-` angeben;
eine Eingabe ohne Vorzeichen wird aus Kompatibilitaet als Minuszeit behandelt.
Kurze Zeiten wie `6:01` oder `01:52` sind ebenfalls erlaubt.
## Werte abfragen
```sh
goTimecalc -a
```
Oder ausgeschrieben:
```sh
goTimecalc --ask
```
## Netto-Zeit und Tages-Saldo
Der Tages-Saldo wird mit Vorzeichen angegeben: Minuszeit mit `-`, Pluszeit mit
`+`.
```sh
goTimecalc --netto 08:56:12 --saldo +01:03:12
```
Ausgabe:
```text
Netto-Zeit 08:56:12
Tagesaldo +01:03:12
= Sollzeit 07:53:00
```
## Brutto, Pause, Netto und Tages-Saldo
```sh
goTimecalc --brutto 09:26:12 --pause 00:30:00 --soll 07:53:00
```
Ausgabe:
```text
Brutto-Zeit 09:26:12
- Pause 00:30:00
= Netto-Zeit 08:56:12
- Sollzeit 07:53:00
= Tagesaldo +01:03:12
```
Der Tages-Saldo kann positiv oder negativ sein.
## Start, Ende und Pause
```sh
goTimecalc --start 08:00 --ende 16:30 --pause 00:30 --soll 07:53
```
Bei Start-/Endzeit zaehlt nur Arbeitszeit zwischen `06:00` und `20:00`.
Zeit davor oder danach wird aus der Brutto-Zeit herausgerechnet.
Beispielausgabe:
```text
Start 08:00:00
Ende 16:30:00
- Pause 00:30:00
= Netto 08:00:00
Soll 07:53:00
Ueberzeit +00:07:00
Start 08:00:00
Ende 16:30:00
Brutto-Zeit 08:30:00
- Pause 00:30:00
= Netto-Zeit 08:00:00
- Sollzeit 07:53:00
= Tagesaldo +00:07:00
```
Wenn die Endzeit kleiner als die Startzeit ist, wird eine Nachtschicht angenommen:
@@ -46,6 +98,9 @@ Wenn die Endzeit kleiner als die Startzeit ist, wird eine Nachtschicht angenomme
goTimecalc --start 22:00 --ende 06:00 --pause 00:30
```
Da diese Zeit komplett ausserhalb `06:00` bis `20:00` liegt, wird sie nicht als
Arbeitszeit angerechnet.
## Webinterface
Der Webserver startet nur, wenn `--web` angegeben wird:
+205 -60
View File
@@ -14,6 +14,12 @@ import (
const appName = "goTimecalc"
const (
workdayStart = 6 * 3600
workdayEnd = 20 * 3600
daySeconds = 24 * 3600
)
type dayList []dayEntry
type dayEntry struct {
@@ -42,7 +48,7 @@ func run(args []string, out io.Writer) error {
return fmt.Errorf("Minuszeit: %w", err)
}
printSollzeit(out, netto, minuszeit)
printSollzeitFromSaldo(out, netto, -minuszeit)
return nil
}
@@ -50,10 +56,14 @@ func run(args []string, out io.Writer) error {
flags.SetOutput(out)
var days dayList
askInput := flags.Bool("a", false, "Werte interaktiv abfragen")
flags.BoolVar(askInput, "ask", false, "Werte interaktiv abfragen")
webInput := flags.Bool("web", false, "Optionales Webinterface lokal starten")
addrInput := flags.String("addr", "127.0.0.1:8080", "Adresse fuer das Webinterface")
nettoInput := flags.String("netto", "", "Netto-Zeit, z.B. 06:01 oder 06:01:14")
minusInput := flags.String("minus", "", "Minuszeit, z.B. 01:52")
saldoInput := flags.String("saldo", "", "Tagesaldo mit Vorzeichen, z.B. -01:52 oder +01:03")
bruttoInput := flags.String("brutto", "", "Brutto-Zeit, z.B. 09:26:12")
startInput := flags.String("start", "", "Startzeit, z.B. 08:00")
endInput := flags.String("ende", "", "Endzeit, z.B. 16:30")
pauseInput := flags.String("pause", "00:00", "Pause, z.B. 00:30")
@@ -64,6 +74,9 @@ func run(args []string, out io.Writer) error {
flags.Usage = func() {
fmt.Fprintln(out, "Nutzung:")
fmt.Fprintln(out, " goTimecalc 06:01:14 01:51:46")
fmt.Fprintln(out, " goTimecalc -a")
fmt.Fprintln(out, " goTimecalc --netto 08:56:12 --saldo +01:03:12")
fmt.Fprintln(out, " goTimecalc --brutto 09:26:12 --pause 00:30 --soll 07:53")
fmt.Fprintln(out, " goTimecalc --start 08:00 --ende 16:30 --pause 00:30 --soll 07:53")
fmt.Fprintln(out, " goTimecalc --day 08:00-16:30,00:30 --day 09:00-17:00 --soll 15:46")
fmt.Fprintln(out, " goTimecalc --set-soll 07:53")
@@ -72,6 +85,11 @@ func run(args []string, out io.Writer) error {
flags.PrintDefaults()
}
if len(args) == 0 {
flags.Usage()
return nil
}
if err := flags.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return nil
@@ -83,6 +101,10 @@ func run(args []string, out io.Writer) error {
return errors.New("unbekannte Argumente. Hilfe mit: goTimecalc --help")
}
if *askInput {
return runAsk(os.Stdin, out)
}
if *webInput {
return startWebServer(*addrInput, out)
}
@@ -103,20 +125,48 @@ func run(args []string, out io.Writer) error {
return runDays(out, days, *sollInput)
}
if *bruttoInput != "" {
return runBruttoPause(out, *bruttoInput, *pauseInput, *sollInput)
}
if *startInput != "" || *endInput != "" {
return runStartEnd(out, *startInput, *endInput, *pauseInput, *sollInput)
}
if *nettoInput != "" || *minusInput != "" {
return runNettoMinus(out, *nettoInput, *minusInput)
if *nettoInput != "" || *minusInput != "" || *saldoInput != "" {
return runNettoSaldo(out, *nettoInput, *saldoInput, *minusInput)
}
return runInteractive(out)
flags.Usage()
return nil
}
func runNettoMinus(out io.Writer, nettoInput string, minusInput string) error {
if nettoInput == "" || minusInput == "" {
return errors.New("--netto und --minus muessen zusammen angegeben werden")
func runAsk(input io.Reader, out io.Writer) error {
reader := bufio.NewReader(input)
netto, err := promptDuration(reader, out, "Netto-Zeit: ")
if err != nil {
return err
}
saldo, err := promptSaldo(reader, out, "Tagesaldo (+/-): ")
if err != nil {
return err
}
printSollzeitFromSaldo(out, netto, saldo)
return nil
}
func runNettoSaldo(out io.Writer, nettoInput string, saldoInput string, minusInput string) error {
if nettoInput == "" {
return errors.New("--netto muss angegeben werden")
}
if saldoInput == "" && minusInput == "" {
return errors.New("--saldo mit Vorzeichen angeben, zum Beispiel: --saldo -01:52 oder --saldo +01:03")
}
if saldoInput != "" && minusInput != "" {
return errors.New("bitte entweder --saldo oder --minus angeben, nicht beides")
}
netto, err := parseDuration(nettoInput)
@@ -124,15 +174,38 @@ func runNettoMinus(out io.Writer, nettoInput string, minusInput string) error {
return fmt.Errorf("Netto-Zeit: %w", err)
}
minuszeit, err := parseDuration(minusInput)
if err != nil {
return fmt.Errorf("Minuszeit: %w", err)
saldo := 0
if saldoInput != "" {
saldo, err = parseSignedDuration(saldoInput)
if err != nil {
return fmt.Errorf("Tagesaldo: %w", err)
}
} else {
minuszeit, err := parseDuration(minusInput)
if err != nil {
return fmt.Errorf("Minuszeit: %w", err)
}
saldo = -minuszeit
}
printSollzeit(out, netto, minuszeit)
printSollzeitFromSaldo(out, netto, saldo)
return nil
}
func runBruttoPause(out io.Writer, bruttoInput string, pauseInput string, sollInput string) error {
brutto, err := parseDuration(bruttoInput)
if err != nil {
return fmt.Errorf("Brutto-Zeit: %w", err)
}
pause, err := parseDuration(pauseInput)
if err != nil {
return fmt.Errorf("Pause: %w", err)
}
return printBruttoNetto(out, brutto, pause, sollInput)
}
func runStartEnd(out io.Writer, startInput string, endInput string, pauseInput string, sollInput string) error {
if startInput == "" || endInput == "" {
return errors.New("--start und --ende muessen zusammen angegeben werden")
@@ -153,24 +226,18 @@ func runStartEnd(out io.Writer, startInput string, endInput string, pauseInput s
return fmt.Errorf("Pause: %w", err)
}
netto := workDuration(start, end, pause)
if netto < 0 {
return errors.New("Pause ist laenger als die Arbeitszeit")
}
fmt.Fprintf(out, "Start %8s\n", formatDuration(start))
fmt.Fprintf(out, "Ende %8s\n", formatDuration(end))
fmt.Fprintf(out, "- Pause %8s\n", formatDuration(pause))
fmt.Fprintf(out, "= Netto %8s\n", formatDuration(netto))
return printBalance(out, netto, sollInput)
brutto := grossDuration(start, end)
return printBruttoNetto(out, brutto, pause, sollInput)
}
func runDays(out io.Writer, days dayList, sollInput string) error {
total := 0
for i, day := range days {
netto := workDuration(day.start, day.end, day.pause)
netto := grossDuration(day.start, day.end) - day.pause
if netto < 0 {
return fmt.Errorf("Tag %d: Pause ist laenger als die Arbeitszeit", i+1)
}
@@ -183,27 +250,55 @@ func runDays(out io.Writer, days dayList, sollInput string) error {
return printBalance(out, total, sollInput)
}
func runInteractive(out io.Writer) error {
reader := bufio.NewReader(os.Stdin)
netto, err := promptDuration(reader, "Netto-Zeit: ")
if err != nil {
return err
}
minuszeit, err := promptDuration(reader, "Minuszeit: ")
if err != nil {
return err
}
printSollzeit(out, netto, minuszeit)
return nil
func printSollzeitFromSaldo(out io.Writer, netto int, saldo int) {
fmt.Fprintf(out, "Netto-Zeit %8s\n", formatDuration(netto))
fmt.Fprintf(out, "Tagesaldo %s\n", formatSignedDuration(saldo))
fmt.Fprintf(out, "= Sollzeit %8s\n", formatDuration(netto-saldo))
}
func printSollzeit(out io.Writer, netto int, minuszeit int) {
fmt.Fprintf(out, "Netto-Zeit %8s\n", formatDuration(netto))
fmt.Fprintf(out, "+ Minuszeit %8s\n", formatDuration(minuszeit))
fmt.Fprintf(out, "= Sollzeit %8s\n", formatDuration(netto+minuszeit))
func promptDuration(reader *bufio.Reader, out io.Writer, label string) (int, error) {
fmt.Fprint(out, label)
input, err := reader.ReadString('\n')
if err != nil {
return 0, err
}
value, err := parseDuration(strings.TrimSpace(input))
if err != nil {
return 0, fmt.Errorf("%s%w", strings.TrimSuffix(label, ": "), err)
}
return value, nil
}
func promptSaldo(reader *bufio.Reader, out io.Writer, label string) (int, error) {
fmt.Fprint(out, label)
input, err := reader.ReadString('\n')
if err != nil {
return 0, err
}
value, err := parseSaldoInput(strings.TrimSpace(input))
if err != nil {
return 0, fmt.Errorf("%s%w", strings.TrimSuffix(label, ": "), err)
}
return value, nil
}
func printBruttoNetto(out io.Writer, brutto int, pause int, sollInput string) error {
netto := brutto - pause
if netto < 0 {
return errors.New("Pause ist laenger als die Brutto-Zeit")
}
fmt.Fprintf(out, "Brutto-Zeit %8s\n", formatDuration(brutto))
fmt.Fprintf(out, "- Pause %8s\n", formatDuration(pause))
fmt.Fprintf(out, "= Netto-Zeit %8s\n", formatDuration(netto))
return printBalance(out, netto, sollInput)
}
func printBalance(out io.Writer, netto int, sollInput string) error {
@@ -216,13 +311,13 @@ func printBalance(out io.Writer, netto int, sollInput string) error {
}
diff := netto - soll
fmt.Fprintf(out, "Soll %8s\n", formatDuration(soll))
fmt.Fprintf(out, "- Sollzeit %8s\n", formatDuration(soll))
if diff >= 0 {
fmt.Fprintf(out, "Ueberzeit +%s\n", formatDuration(diff))
fmt.Fprintf(out, "= Tagesaldo +%s\n", formatDuration(diff))
return nil
}
fmt.Fprintf(out, "Minuszeit -%s\n", formatDuration(-diff))
fmt.Fprintf(out, "= Tagesaldo -%s\n", formatDuration(-diff))
return nil
}
@@ -246,22 +341,6 @@ func wantedSoll(input string) (int, bool, error) {
return value, true, nil
}
func promptDuration(reader *bufio.Reader, label string) (int, error) {
fmt.Print(label)
input, err := reader.ReadString('\n')
if err != nil {
return 0, err
}
value, err := parseDuration(strings.TrimSpace(input))
if err != nil {
return 0, fmt.Errorf("%s%w", strings.TrimSuffix(label, ": "), err)
}
return value, nil
}
func parseDuration(input string) (int, error) {
parts := strings.Split(strings.TrimSpace(input), ":")
if len(parts) < 2 || len(parts) > 3 {
@@ -293,6 +372,44 @@ func parseDuration(input string) (int, error) {
return hours*3600 + minutes*60 + seconds, nil
}
func parseSignedDuration(input string) (int, error) {
input = strings.TrimSpace(input)
if input == "" {
return 0, errors.New("Zeit fehlt")
}
sign := input[0]
if sign != '+' && sign != '-' {
return 0, errors.New("Tagesaldo braucht ein Vorzeichen, zum Beispiel -01:52 oder +01:03")
}
value, err := parseDuration(input[1:])
if err != nil {
return 0, err
}
if sign == '-' {
return -value, nil
}
return value, nil
}
func parseSaldoInput(input string) (int, error) {
input = strings.TrimSpace(input)
if input == "" {
return 0, errors.New("Zeit fehlt")
}
if strings.HasPrefix(input, "+") || strings.HasPrefix(input, "-") {
return parseSignedDuration(input)
}
value, err := parseDuration(input)
if err != nil {
return 0, err
}
return -value, nil
}
func parseClock(input string) (int, error) {
value, err := parseDuration(input)
if err != nil {
@@ -318,10 +435,31 @@ func parsePart(input string, label string) (int, error) {
}
func workDuration(start int, end int, pause int) int {
return grossDuration(start, end) - pause
}
func grossDuration(start int, end int) int {
if end < start {
end += 24 * 3600
end += daySeconds
}
return end - start - pause
total := 0
for dayStart := 0; dayStart <= end; dayStart += daySeconds {
windowStart := dayStart + workdayStart
windowEnd := dayStart + workdayEnd
total += overlapSeconds(start, end, windowStart, windowEnd)
}
return total
}
func overlapSeconds(start int, end int, windowStart int, windowEnd int) int {
from := max(start, windowStart)
to := min(end, windowEnd)
if to <= from {
return 0
}
return to - from
}
func formatDuration(totalSeconds int) string {
@@ -332,6 +470,13 @@ func formatDuration(totalSeconds int) string {
return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
}
func formatSignedDuration(totalSeconds int) string {
if totalSeconds < 0 {
return "-" + formatDuration(-totalSeconds)
}
return "+" + formatDuration(totalSeconds)
}
func (d *dayList) String() string {
return fmt.Sprint([]dayEntry(*d))
}
+150 -13
View File
@@ -11,7 +11,7 @@ func TestOldNettoMinusUsage(t *testing.T) {
want := strings.Join([]string{
"Netto-Zeit 06:01:14",
"+ Minuszeit 01:51:46",
"Tagesaldo -01:51:46",
"= Sollzeit 07:53:00",
"",
}, "\n")
@@ -21,12 +21,50 @@ func TestOldNettoMinusUsage(t *testing.T) {
}
}
func TestNettoSaldoUsage(t *testing.T) {
output := runCommand(t, "--netto", "08:56:12", "--saldo", "+01:03:12")
assertContains(t, output, "Netto-Zeit 08:56:12")
assertContains(t, output, "Tagesaldo +01:03:12")
assertContains(t, output, "= Sollzeit 07:53:00")
}
func TestNoArgumentsShowsHelp(t *testing.T) {
output := runCommand(t)
assertContains(t, output, "Nutzung:")
assertContains(t, output, "goTimecalc -a")
assertContains(t, output, "goTimecalc --netto 08:56:12 --saldo +01:03:12")
}
func TestAskMode(t *testing.T) {
var out bytes.Buffer
if err := runAsk(strings.NewReader("08:05:25\n+00:12:25\n"), &out); err != nil {
t.Fatal(err)
}
assertContains(t, out.String(), "Netto-Zeit: Tagesaldo (+/-):")
assertContains(t, out.String(), "Tagesaldo +00:12:25")
assertContains(t, out.String(), "= Sollzeit 07:53:00")
}
func TestStartEndWithPauseAndSoll(t *testing.T) {
output := runCommand(t, "--start", "08:00", "--ende", "16:30", "--pause", "00:30", "--soll", "07:53")
assertContains(t, output, "= Netto 08:00:00")
assertContains(t, output, "Soll 07:53:00")
assertContains(t, output, "Ueberzeit +00:07:00")
assertContains(t, output, "Brutto-Zeit 08:30:00")
assertContains(t, output, "= Netto-Zeit 08:00:00")
assertContains(t, output, "- Sollzeit 07:53:00")
assertContains(t, output, "= Tagesaldo +00:07:00")
}
func TestBruttoPauseWithPositiveTagesaldo(t *testing.T) {
output := runCommand(t, "--brutto", "09:26:12", "--pause", "00:30:00", "--soll", "07:53:00")
assertContains(t, output, "Brutto-Zeit 09:26:12")
assertContains(t, output, "- Pause 00:30:00")
assertContains(t, output, "= Netto-Zeit 08:56:12")
assertContains(t, output, "- Sollzeit 07:53:00")
assertContains(t, output, "= Tagesaldo +01:03:12")
}
func TestMultipleDays(t *testing.T) {
@@ -39,7 +77,7 @@ func TestMultipleDays(t *testing.T) {
assertContains(t, output, "Tag 1 08:00:00")
assertContains(t, output, "Tag 2 07:30:00")
assertContains(t, output, "= Summe 15:30:00")
assertContains(t, output, "Minuszeit -00:16:00")
assertContains(t, output, "= Tagesaldo -00:16:00")
}
func TestShortDurationInput(t *testing.T) {
@@ -53,12 +91,49 @@ func TestShortDurationInput(t *testing.T) {
}
}
func TestOvernightWorkDuration(t *testing.T) {
start, err := parseClock("22:00")
func TestSignedDurationRequiresSign(t *testing.T) {
if _, err := parseSignedDuration("01:03"); err == nil {
t.Fatal("expected missing sign error")
}
got, err := parseSignedDuration("-01:03")
if err != nil {
t.Fatal(err)
}
end, err := parseClock("06:00")
if got != -3780 {
t.Fatalf("want -3780 seconds, got %d", got)
}
}
func TestSaldoInputAcceptsSignsAndLegacyMinus(t *testing.T) {
tests := []struct {
input string
want int
}{
{input: "+00:12:25", want: 745},
{input: "-00:12:25", want: -745},
{input: "00:12:25", want: -745},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got, err := parseSaldoInput(tt.input)
if err != nil {
t.Fatal(err)
}
if got != tt.want {
t.Fatalf("want %d seconds, got %d", tt.want, got)
}
})
}
}
func TestOvernightWorkDuration(t *testing.T) {
start, err := parseClock("19:00")
if err != nil {
t.Fatal(err)
}
end, err := parseClock("07:00")
if err != nil {
t.Fatal(err)
}
@@ -67,8 +142,39 @@ func TestOvernightWorkDuration(t *testing.T) {
t.Fatal(err)
}
if got := workDuration(start, end, pause); got != 27000 {
t.Fatalf("want 27000 seconds, got %d", got)
if got := workDuration(start, end, pause); got != 5400 {
t.Fatalf("want 5400 seconds, got %d", got)
}
}
func TestGrossDurationOnlyCountsWorkdayWindow(t *testing.T) {
tests := []struct {
name string
start string
end string
want int
}{
{name: "full span is capped", start: "05:00", end: "21:00", want: 14 * 3600},
{name: "after workday is removed", start: "19:00", end: "22:00", want: 1 * 3600},
{name: "overnight counts both workday windows", start: "19:00", end: "07:00", want: 2 * 3600},
{name: "outside workday only", start: "22:00", end: "05:00", want: 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
start, err := parseClock(tt.start)
if err != nil {
t.Fatal(err)
}
end, err := parseClock(tt.end)
if err != nil {
t.Fatal(err)
}
if got := grossDuration(start, end); got != tt.want {
t.Fatalf("want %d seconds, got %d", tt.want, got)
}
})
}
}
@@ -87,8 +193,39 @@ func TestWebRangeCalculation(t *testing.T) {
if len(resp.Lines) == 0 {
t.Fatal("expected result lines")
}
assertLine(t, resp.Lines, "= Netto", "08:00:00")
assertLine(t, resp.Lines, "Ueberzeit", "+00:07:00")
assertLine(t, resp.Lines, "Brutto-Zeit", "08:30:00")
assertLine(t, resp.Lines, "= Netto-Zeit", "08:00:00")
assertLine(t, resp.Lines, "= Tagesaldo", "+00:07:00")
}
func TestWebNettoSaldoCalculation(t *testing.T) {
resp, err := calculateWeb(webCalcRequest{
Mode: "netto",
Netto: "08:56:12",
Saldo: "+01:03:12",
})
if err != nil {
t.Fatal(err)
}
assertLine(t, resp.Lines, "Tagesaldo", "+01:03:12")
assertLine(t, resp.Lines, "= Sollzeit", "07:53:00")
}
func TestWebBruttoCalculation(t *testing.T) {
resp, err := calculateWeb(webCalcRequest{
Mode: "brutto",
Brutto: "09:26:12",
Pause: "00:30:00",
Soll: "07:53:00",
})
if err != nil {
t.Fatal(err)
}
assertLine(t, resp.Lines, "Brutto-Zeit", "09:26:12")
assertLine(t, resp.Lines, "= Netto-Zeit", "08:56:12")
assertLine(t, resp.Lines, "= Tagesaldo", "+01:03:12")
}
func TestWebDayCalculation(t *testing.T) {
@@ -105,7 +242,7 @@ func TestWebDayCalculation(t *testing.T) {
}
assertLine(t, resp.Lines, "= Summe", "15:30:00")
assertLine(t, resp.Lines, "Minuszeit", "-00:16:00")
assertLine(t, resp.Lines, "= Tagesaldo", "-00:16:00")
}
func runCommand(t *testing.T, args ...string) string {
+84 -21
View File
@@ -12,6 +12,8 @@ type webCalcRequest struct {
Mode string `json:"mode"`
Netto string `json:"netto"`
Minus string `json:"minus"`
Saldo string `json:"saldo"`
Brutto string `json:"brutto"`
Start string `json:"start"`
End string `json:"end"`
Pause string `json:"pause"`
@@ -93,6 +95,8 @@ func calculateWeb(req webCalcRequest) (webCalcResponse, error) {
switch mode {
case "netto":
return calculateWebNetto(req)
case "brutto":
return calculateWebBrutto(req)
case "range":
return calculateWebRange(req)
case "days":
@@ -107,20 +111,56 @@ func calculateWebNetto(req webCalcRequest) (webCalcResponse, error) {
if err != nil {
return webCalcResponse{}, fmt.Errorf("Netto-Zeit: %w", err)
}
minus, err := parseDuration(req.Minus)
if err != nil {
return webCalcResponse{}, fmt.Errorf("Minuszeit: %w", err)
saldoInput := req.Saldo
if strings.TrimSpace(saldoInput) == "" {
saldoInput = req.Minus
}
var saldo int
if strings.TrimSpace(saldoInput) == "" {
return webCalcResponse{}, fmt.Errorf("Tagesaldo fehlt")
}
if strings.HasPrefix(strings.TrimSpace(saldoInput), "+") || strings.HasPrefix(strings.TrimSpace(saldoInput), "-") {
saldo, err = parseSignedDuration(saldoInput)
if err != nil {
return webCalcResponse{}, fmt.Errorf("Tagesaldo: %w", err)
}
} else {
minus, err := parseDuration(saldoInput)
if err != nil {
return webCalcResponse{}, fmt.Errorf("Minuszeit: %w", err)
}
saldo = -minus
}
lines := []resultLine{
{Label: "Netto-Zeit", Value: formatDuration(netto)},
{Label: "+ Minuszeit", Value: formatDuration(minus)},
{Label: "= Sollzeit", Value: formatDuration(netto + minus), Kind: "total"},
{Label: "Tagesaldo", Value: formatSignedDuration(saldo)},
{Label: "= Sollzeit", Value: formatDuration(netto - saldo), Kind: "total"},
}
return webCalcResponse{Lines: lines}, nil
}
func calculateWebBrutto(req webCalcRequest) (webCalcResponse, error) {
brutto, err := parseDuration(req.Brutto)
if err != nil {
return webCalcResponse{}, fmt.Errorf("Brutto-Zeit: %w", err)
}
pauseInput := req.Pause
if strings.TrimSpace(pauseInput) == "" {
pauseInput = "00:00"
}
pause, err := parseDuration(pauseInput)
if err != nil {
return webCalcResponse{}, fmt.Errorf("Pause: %w", err)
}
return webBruttoNettoResponse(brutto, pause, req)
}
func calculateWebRange(req webCalcRequest) (webCalcResponse, error) {
start, err := parseClock(req.Start)
if err != nil {
@@ -140,23 +180,37 @@ func calculateWebRange(req webCalcRequest) (webCalcResponse, error) {
return webCalcResponse{}, fmt.Errorf("Pause: %w", err)
}
netto := workDuration(start, end, pause)
if netto < 0 {
return webCalcResponse{}, fmt.Errorf("Pause ist laenger als die Arbeitszeit")
}
lines := []resultLine{
{Label: "Start", Value: formatDuration(start)},
{Label: "Ende", Value: formatDuration(end)},
{Label: "- Pause", Value: formatDuration(pause)},
{Label: "= Netto", Value: formatDuration(netto), Kind: "total"},
}
brutto := grossDuration(start, end)
resp, err := webBruttoNettoResponse(brutto, pause, req)
if err != nil {
return webCalcResponse{}, err
}
resp.Lines = append(lines, resp.Lines...)
return resp, nil
}
func webBruttoNettoResponse(brutto int, pause int, req webCalcRequest) (webCalcResponse, error) {
netto := brutto - pause
if netto < 0 {
return webCalcResponse{}, fmt.Errorf("Pause ist laenger als die Brutto-Zeit")
}
lines := []resultLine{
{Label: "Brutto-Zeit", Value: formatDuration(brutto)},
{Label: "- Pause", Value: formatDuration(pause)},
{Label: "= Netto-Zeit", Value: formatDuration(netto), Kind: "total"},
}
var err error
lines, err = appendSollBalance(lines, netto, req.Soll)
if err != nil {
return webCalcResponse{}, err
}
if req.SetDefault && strings.TrimSpace(req.Soll) != "" {
soll, err := parseDuration(req.Soll)
if err != nil {
@@ -241,12 +295,12 @@ func appendSollBalance(lines []resultLine, netto int, sollInput string) ([]resul
return lines, nil
}
lines = append(lines, resultLine{Label: "Soll", Value: formatDuration(soll)})
lines = append(lines, resultLine{Label: "- Sollzeit", Value: formatDuration(soll)})
diff := netto - soll
if diff >= 0 {
return append(lines, resultLine{Label: "Ueberzeit", Value: "+" + formatDuration(diff), Kind: "positive"}), nil
return append(lines, resultLine{Label: "= Tagesaldo", Value: "+" + formatDuration(diff), Kind: "positive"}), nil
}
return append(lines, resultLine{Label: "Minuszeit", Value: "-" + formatDuration(-diff), Kind: "negative"}), nil
return append(lines, resultLine{Label: "= Tagesaldo", Value: "-" + formatDuration(-diff), Kind: "negative"}), nil
}
const webPageHTML = `<!doctype html>
@@ -310,7 +364,7 @@ h1 {
}
.tabs {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(4, 1fr);
gap: 8px;
margin-bottom: 18px;
}
@@ -449,7 +503,8 @@ input:focus {
<section class="panel">
<nav class="tabs" aria-label="Rechenmodus">
<button class="tab active" data-mode="range" type="button">Start / Ende</button>
<button class="tab" data-mode="netto" type="button">Netto + Minus</button>
<button class="tab" data-mode="brutto" type="button">Brutto / Pause</button>
<button class="tab" data-mode="netto" type="button">Netto / Saldo</button>
<button class="tab" data-mode="days" type="button">Mehrere Tage</button>
</nav>
@@ -462,10 +517,17 @@ input:focus {
</div>
</div>
<div id="mode-brutto" class="mode">
<div class="grid">
<label>Brutto-Zeit <input id="brutto" value="09:26:12" inputmode="numeric"></label>
<label>Pause <input id="brutto-pause" value="00:30:00" inputmode="numeric"></label>
</div>
</div>
<div id="mode-netto" class="mode">
<div class="grid">
<label>Netto-Zeit <input id="netto" value="06:01:14" inputmode="numeric"></label>
<label>Minuszeit <input id="minus" value="01:51:46" inputmode="numeric"></label>
<label>Tagesaldo <input id="saldo" value="-01:51:46" inputmode="numeric"></label>
</div>
</div>
@@ -536,10 +598,11 @@ function payload() {
return {
mode: activeMode,
netto: document.querySelector("#netto").value,
minus: document.querySelector("#minus").value,
saldo: document.querySelector("#saldo").value,
brutto: document.querySelector("#brutto").value,
start: document.querySelector("#start").value,
end: document.querySelector("#end").value,
pause: document.querySelector("#pause").value,
pause: activeMode === "brutto" ? document.querySelector("#brutto-pause").value : document.querySelector("#pause").value,
soll: document.querySelector("#soll").value,
setDefault: document.querySelector("#set-default").checked,
days: dayPayload()