8 Commits

5 changed files with 511 additions and 108 deletions
+69 -11
View File
@@ -1,8 +1,8 @@
# goTimecalc
Ein kleines Terminalprogramm mit lokalem Webinterface fuer Arbeitszeiten. Es
berechnet Sollzeiten, Nettozeiten, Pausen, Tages- und Wochensummen sowie
Ueberstunden oder Minuszeit.
Ein kleines Terminalprogramm fuer Arbeitszeiten. Es berechnet Sollzeiten,
Nettozeiten, Pausen, Tages- und Wochensummen sowie Ueberstunden oder Minuszeit.
Ein lokales Webinterface ist optional dabei und startet nur mit `--web`.
## Nutzung
@@ -14,29 +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 ab.
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:
@@ -45,8 +98,13 @@ 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:
```sh
goTimecalc --web
```
+1 -1
View File
@@ -1,3 +1,3 @@
module git.fhi.mpg.de/spok/timecalc
module git.fhi.mpg.de/spok/goTimecalc
go 1.26
+207 -62
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
webInput := flags.Bool("web", false, "Webinterface lokal starten")
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,14 +74,22 @@ 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")
fmt.Fprintln(out, " goTimecalc --web")
fmt.Fprintln(out, " goTimecalc --web # startet optional den lokalen Browsermodus")
fmt.Fprintln(out)
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()