first attempt - savepoint commit

This commit is contained in:
Patryk Hegenberg 2025-03-31 15:22:34 +02:00
parent b083a8255c
commit 07c0d782af
8 changed files with 700 additions and 43 deletions

55
app.go
View file

@ -15,9 +15,10 @@ import (
type App struct {
cfg Config
flags Flags
tracker *SQLiteTimeTracker // Hinzufügen
}
func NewApp() (*App, error) {
func NewApp(tracker *SQLiteTimeTracker) (*App, error) {
cfg, err := loadConfig()
if err != nil {
return nil, fmt.Errorf("error loading config: %w", err)
@ -25,12 +26,14 @@ func NewApp() (*App, error) {
return &App{
cfg: cfg,
tracker: tracker, // Speichern
}, nil
}
func (a *App) connect() {
tw := NewTimeWarrior()
tw.StartWork()
// tw := NewTimeWarrior()
// tw.StartWork()
a.tracker.StartWork()
a.wakeWorkstation()
sshCon, err := a.newSSHConnection()
if err != nil {
@ -119,7 +122,7 @@ func (a *App) startRDPConnection() {
func (a *App) makeChoice() {
var choice string
tw := NewTimeWarrior()
// tw := NewTimeWarrior()
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
@ -148,27 +151,45 @@ func (a *App) makeChoice() {
switch choice {
case "start work":
a.connect()
a.connect() // connect nutzt jetzt a.tracker
case "stop work":
tw.StopWork()
a.tracker.StopWork()
case "start break":
tw.StartBreak()
a.tracker.StartBreak()
case "stop break":
tw.StopBreak()
a.tracker.StopBreak()
case "show week summary":
tw.ShowSummary(":week")
a.tracker.ShowSummary(":week")
case "show month summary":
tw.ShowSummary(":month")
case "connect to jump":
a.connectToJump()
case "connect to workstation":
a.connectToWorkstation()
case "start rdp connection":
a.startRDPConnection()
a.tracker.ShowSummary(":month")
case "export":
tw.ExportSummary(a.flags.ExportName)
if err := a.tracker.ExportSummary(a.flags.ExportName); err != nil {
log.Printf("Error exporting summary: %v", err)
}
}
// switch choice {
// case "start work":
// a.connect()
// case "stop work":
// tw.StopWork()
// case "start break":
// tw.StartBreak()
// case "stop break":
// tw.StopBreak()
// case "show week summary":
// tw.ShowSummary(":week")
// case "show month summary":
// tw.ShowSummary(":month")
// case "connect to jump":
// a.connectToJump()
// case "connect to workstation":
// a.connectToWorkstation()
// case "start rdp connection":
// a.startRDPConnection()
// case "export":
// tw.ExportSummary(a.flags.ExportName)
// }
}
func (a *App) getSSHAuth() ssh.AuthMethod {
keypath := os.ExpandEnv("$HOME/.ssh/hegenberg")

94
cmd.go
View file

@ -1,8 +1,8 @@
package main
import (
"fmt"
"log"
"os"
"github.com/spf13/cobra"
)
@ -31,41 +31,91 @@ func (a *App) startCommand() *cobra.Command {
}
}
// cmd.go (Auszüge)
func (a *App) stopCommand() *cobra.Command {
return &cobra.Command{
Use: "stop",
Short: "stop work",
Long: "command to stop the work day",
Run: func(cmd *cobra.Command, args []string) {
tw := NewTimeWarrior()
tw.StopWork()
if err := a.killForwardings(); err != nil {
log.Printf("error stoping port forwarding: %v", err)
// ... Use, Short, Long ...
RunE: func(cmd *cobra.Command, args []string) error { // RunE verwenden für Fehlerbehandlung
// tw := NewTimeWarrior() // Entfernen
if err := a.tracker.StopWork(); err != nil {
log.Printf("Error stopping work: %v", err)
// return err // Fehler zurückgeben, nicht os.Exit
}
os.Exit(0)
// Das Beenden der Forwardings ist hier richtig platziert
if err := a.killForwardings(); err != nil {
log.Printf("error stopping port forwarding: %v", err)
// Ggf. auch hier Fehler zurückgeben oder nur loggen
}
// Kein os.Exit(0) hier!
return nil // Erfolg signalisieren
},
}
}
func (a *App) showCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "show",
Short: "show timetracking",
Long: "show different timetracking",
Run: func(cmd *cobra.Command, args []string) {
tw := NewTimeWarrior()
// ... Use, Short, Long ...
RunE: func(cmd *cobra.Command, args []string) error { // RunE verwenden
// tw := NewTimeWarrior() // Entfernen
var err error
if a.flags.ShowExport {
tw.ExportSummary(a.flags.ExportName)
err = a.tracker.ExportSummary(a.flags.ExportName)
} else if a.flags.ShowWeek { // else if, falls nur eine Aktion gewünscht
err = a.tracker.ShowSummary(":week")
} else if a.flags.ShowMonth {
err = a.tracker.ShowSummary(":month")
} else {
// Standardaktion, wenn kein Flag gesetzt ist? Oder Fehler?
fmt.Println("Please specify a flag: --week, --month, or --export")
}
if a.flags.ShowWeek {
tw.ShowSummary(":week")
if err != nil {
log.Printf("Error in show command: %v", err)
return err // Fehler zurückgeben
}
if a.flags.ShowMonth {
tw.ShowSummary(":month")
}
os.Exit(0)
// Kein os.Exit(0)!
return nil
},
}
// Flags bleiben gleich...
// return cmd
// }
// func (a *App) stopCommand() *cobra.Command {
// return &cobra.Command{
// Use: "stop",
// Short: "stop work",
// Long: "command to stop the work day",
// Run: func(cmd *cobra.Command, args []string) {
// tw := NewTimeWarrior()
// tw.StopWork()
// if err := a.killForwardings(); err != nil {
// log.Printf("error stoping port forwarding: %v", err)
// }
// os.Exit(0)
// },
// }
// }
//
// func (a *App) showCommand() *cobra.Command {
// cmd := &cobra.Command{
// Use: "show",
// Short: "show timetracking",
// Long: "show different timetracking",
// Run: func(cmd *cobra.Command, args []string) {
// tw := NewTimeWarrior()
// if a.flags.ShowExport {
// tw.ExportSummary(a.flags.ExportName)
// }
// if a.flags.ShowWeek {
// tw.ShowSummary(":week")
// }
// if a.flags.ShowMonth {
// tw.ShowSummary(":month")
// }
// os.Exit(0)
// },
// }
cmd.Flags().BoolVarP(&a.flags.ShowWeek, "week", "w", false, "show timewarrior week summary")
cmd.Flags().BoolVarP(&a.flags.ShowMonth, "month", "m", false, "show timewarrior month summary")

View file

@ -53,3 +53,16 @@ func loadConfig() (Config, error) {
return cfg, nil
}
func getDBPath() (string, error) {
configPath, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("cannot get user config dir: %w", err)
}
workConfigPath := filepath.Join(configPath, "work")
// Sicherstellen, dass das Verzeichnis existiert
if err := os.MkdirAll(workConfigPath, 0750); err != nil {
return "", fmt.Errorf("cannot create config directory %s: %w", workConfigPath, err)
}
return filepath.Join(workConfigPath, "work_time.db"), nil
}

1
go.mod
View file

@ -4,6 +4,7 @@ go 1.23.0
require (
github.com/charmbracelet/huh v0.5.3
github.com/mattn/go-sqlite3 v1.14.24
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.19.0
github.com/xuri/excelize/v2 v2.9.0

2
go.sum
View file

@ -53,6 +53,8 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=

View file

@ -6,7 +6,12 @@ import (
)
func main() {
app, err := NewApp()
tracker, err := NewSQLiteTimeTracker()
if err != nil {
log.Fatalf("unable to setup database connection: %v", err)
}
defer tracker.Close() // Wichtig: DB-Verbindung schließen
app, err := NewApp(tracker)
if err != nil {
log.Fatalf("unable to setup application: %v", err)
}

565
tracker_sqlite.go Normal file
View file

@ -0,0 +1,565 @@
package main
import (
"database/sql"
"fmt"
"log"
"sort"
"strings"
"time"
_ "github.com/mattn/go-sqlite3" // SQLite driver
"github.com/xuri/excelize/v2"
)
// Stellt die Datenbankverbindung und Methoden zur Zeitverfolgung bereit
type SQLiteTimeTracker struct {
db *sql.DB
}
// Repräsentiert ein Zeitintervall aus der Datenbank
type TimeInterval struct {
ID int64
StartTime time.Time
EndTime sql.NullTime // Wichtig für offene Intervalle
Tag string
}
// Initialisiert die Datenbankverbindung und erstellt die Tabelle, falls nötig
func NewSQLiteTimeTracker() (*SQLiteTimeTracker, error) {
dbPath, err := getDBPath()
if err != nil {
return nil, err
}
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
return nil, fmt.Errorf("cannot open database %s: %w", dbPath, err)
}
// Tabelle erstellen, falls sie nicht existiert
createTableSQL := `
CREATE TABLE IF NOT EXISTS time_intervals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
start_time DATETIME NOT NULL,
end_time DATETIME DEFAULT NULL,
tag TEXT NOT NULL
);`
if _, err := db.Exec(createTableSQL); err != nil {
db.Close() // Aufräumen bei Fehler
return nil, fmt.Errorf("cannot create time_intervals table: %w", err)
}
return &SQLiteTimeTracker{db: db}, nil
}
// Schließt die Datenbankverbindung
func (t *SQLiteTimeTracker) Close() error {
if t.db != nil {
return t.db.Close()
}
return nil
}
// Findet das aktuell laufende (offene) Zeitintervall
func (t *SQLiteTimeTracker) findOpenInterval() (*TimeInterval, error) {
query := `SELECT id, start_time, end_time, tag FROM time_intervals WHERE end_time IS NULL ORDER BY start_time DESC LIMIT 1;`
row := t.db.QueryRow(query)
var interval TimeInterval
err := row.Scan(&interval.ID, &interval.StartTime, &interval.EndTime, &interval.Tag)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil // Kein offenes Intervall gefunden, kein Fehler
}
return nil, fmt.Errorf("error querying open interval: %w", err)
}
return &interval, nil
}
// Stoppt das aktuell offene Intervall (setzt end_time)
func (t *SQLiteTimeTracker) stopCurrentInterval(now time.Time) error {
openInterval, err := t.findOpenInterval()
if err != nil {
return err // Fehler beim Suchen
}
if openInterval == nil {
return nil // Nichts zu stoppen
}
updateSQL := `UPDATE time_intervals SET end_time = ? WHERE id = ?;`
_, err = t.db.Exec(updateSQL, now, openInterval.ID)
if err != nil {
return fmt.Errorf("error updating end_time for interval %d: %w", openInterval.ID, err)
}
fmt.Printf("Stopped interval: %s (ID: %d)\n", openInterval.Tag, openInterval.ID)
return nil
}
// Startet ein neues Intervall mit dem gegebenen Tag
func (t *SQLiteTimeTracker) startNewInterval(tag string, now time.Time) error {
// Zuerst das alte Intervall stoppen
if err := t.stopCurrentInterval(now); err != nil {
return fmt.Errorf("failed to stop previous interval before starting new one: %w", err)
}
insertSQL := `INSERT INTO time_intervals (start_time, tag) VALUES (?, ?);`
res, err := t.db.Exec(insertSQL, now, tag)
if err != nil {
return fmt.Errorf("error inserting new interval with tag %s: %w", tag, err)
}
newID, _ := res.LastInsertId()
fmt.Printf("Started interval: %s (ID: %d)\n", tag, newID)
return nil
}
// --- Implementierung der ursprünglichen TimeWarrior-Methoden ---
func (t *SQLiteTimeTracker) StartWork() error {
return t.startNewInterval("work", time.Now())
}
func (t *SQLiteTimeTracker) StopWork() error {
// Stoppt das *letzte* offene Intervall, egal welcher Tag
return t.stopCurrentInterval(time.Now())
}
func (t *SQLiteTimeTracker) StartBreak() error {
return t.startNewInterval("break", time.Now())
}
// Stoppt die Pause und startet 'work' wieder (entspricht altem `timew track work`)
func (t *SQLiteTimeTracker) StopBreak() error {
// `startNewInterval` stoppt implizit das vorherige ('break')
return t.startNewInterval("work", time.Now())
}
// Holt Intervalle für einen gegebenen Zeitraum
func (t *SQLiteTimeTracker) getIntervals(start, end time.Time) ([]TimeInterval, error) {
query := `SELECT id, start_time, end_time, tag FROM time_intervals WHERE start_time >= ? AND (end_time <= ? OR end_time IS NULL) ORDER BY start_time ASC;`
rows, err := t.db.Query(query, start, end)
if err != nil {
return nil, fmt.Errorf("error querying intervals between %s and %s: %w", start, end, err)
}
defer rows.Close()
var intervals []TimeInterval
for rows.Next() {
var interval TimeInterval
// Scanne auch end_time, auch wenn es NULL sein könnte
err := rows.Scan(&interval.ID, &interval.StartTime, &interval.EndTime, &interval.Tag)
if err != nil {
return nil, fmt.Errorf("error scanning interval row: %w", err)
}
intervals = append(intervals, interval)
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("error after iterating interval rows: %w", err)
}
return intervals, nil
}
// Berechnet Start- und Enddatum für relative Perioden wie ":week", ":month", ":year"
func calculateDateRange(period string) (time.Time, time.Time) {
now := time.Now()
year, month, day := now.Date()
loc := now.Location()
switch period {
case ":week":
// Gehe zum Anfang der Woche (Montag)
weekday := now.Weekday()
daysToSubtract := int(weekday) - int(time.Monday)
if daysToSubtract < 0 {
daysToSubtract += 7
}
startOfWeek := time.Date(year, month, day-daysToSubtract, 0, 0, 0, 0, loc)
endOfWeek := startOfWeek.AddDate(0, 0, 7).Add(-1 * time.Nanosecond) // Ende Sonntag
return startOfWeek, endOfWeek
case ":month":
startOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, loc)
endOfMonth := startOfMonth.AddDate(0, 1, 0).Add(-1 * time.Nanosecond) // Ende des Monats
return startOfMonth, endOfMonth
case ":year":
startOfYear := time.Date(year, 1, 1, 0, 0, 0, 0, loc)
endOfYear := startOfYear.AddDate(1, 0, 0).Add(-1 * time.Nanosecond) // Ende des Jahres
return startOfYear, endOfYear
default:
// Standard: heutiger Tag
startOfDay := time.Date(year, month, day, 0, 0, 0, 0, loc)
endOfDay := startOfDay.AddDate(0, 0, 1).Add(-1 * time.Nanosecond)
return startOfDay, endOfDay
}
}
func (t *SQLiteTimeTracker) ShowSummary(period string) error {
start, end := calculateDateRange(period)
fmt.Printf("Summary for %s (%s to %s)\n", period, start.Format("2006-01-02"), end.Format("2006-01-02"))
intervals, err := t.getIntervals(start, end)
if err != nil {
return err
}
if len(intervals) == 0 {
fmt.Println("No data for this period.")
return nil
}
// Aggregieren der Daten (vereinfachtes Beispiel)
var totalWork time.Duration
var totalBreak time.Duration
tags := make(map[string]time.Duration)
for _, interval := range intervals {
endTime := time.Now() // Nimm aktuelle Zeit, falls Intervall noch offen
if interval.EndTime.Valid {
endTime = interval.EndTime.Time
}
// Stelle sicher, dass das Ende nicht über den Abfragezeitraum hinausgeht
if endTime.After(end) {
endTime = end
}
// Stelle sicher, dass der Start nicht vor dem Abfragezeitraum liegt
startTime := interval.StartTime
if startTime.Before(start) {
startTime = start
}
duration := endTime.Sub(startTime)
if duration < 0 {
duration = 0
} // Sicherstellen, dass die Dauer nicht negativ ist
tags[interval.Tag] += duration
if interval.Tag == "work" {
totalWork += duration
} else if interval.Tag == "break" {
totalBreak += duration
}
}
fmt.Println("--- Totals ---")
fmt.Printf("Work: %s\n", formatDuration(totalWork))
fmt.Printf("Break: %s\n", formatDuration(totalBreak))
fmt.Println("--- Tags ---")
for tag, duration := range tags {
fmt.Printf("%-10s: %s\n", tag, formatDuration(duration))
}
return nil
}
// -- ExportSummary und Hilfsfunktionen (angepasst für SQLite Daten) --
// Behalte DailySummary und ExcelEntry Strukturen bei oder passe sie an
// type DailySummary struct { ... }
// type ExcelEntry struct { ... }
func (t *SQLiteTimeTracker) ExportSummary(name string) error {
fmt.Println("Export Timetable")
// Hole Daten für das ganze Jahr (oder mache Zeitraum konfigurierbar)
start, end := calculateDateRange(":year")
intervals, err := t.getIntervals(start, end)
if err != nil {
return fmt.Errorf("cannot get intervals for export: %w", err)
}
if len(intervals) == 0 {
fmt.Println("No data to export for the year.")
return nil
}
// Verarbeitung der Intervalle zu Tageszusammenfassungen
dailySummaries := make(map[string]*DailySummary) // Key: "YYYY-MM-DD"
for _, interval := range intervals {
currentDay := interval.StartTime.Format("2006-01-02")
daySummary, exists := dailySummaries[currentDay]
if !exists {
daySummary = &DailySummary{
Date: currentDay,
Day: interval.StartTime.Format("Mon"), // Wochentag
}
dailySummaries[currentDay] = daySummary
}
// Berechne Dauer nur für abgeschlossene Intervalle innerhalb des Tages
// Offene Intervalle werden für den Export ignoriert oder bis 'now' gerechnet?
// Hier ignoriert für Einfachheit, außer sie enden am selben Tag.
var intervalEnd time.Time
if interval.EndTime.Valid {
intervalEnd = interval.EndTime.Time
// Stelle sicher, dass das Intervall am selben Tag endet
if intervalEnd.Format("2006-01-02") != currentDay {
// Wenn es über Mitternacht geht, schneide es am Ende des Tages ab
dayEnd := interval.StartTime.Truncate(24*time.Hour).AddDate(0, 0, 1).Add(-time.Nanosecond)
if intervalEnd.After(dayEnd) {
intervalEnd = dayEnd
}
}
} else {
// Aktuell laufendes Intervall - nicht für Export berücksichtigen oder bis jetzt?
// Entscheide, wie du offene Intervalle behandeln willst. Hier ignoriert.
continue
}
duration := intervalEnd.Sub(interval.StartTime)
if duration < 0 {
duration = 0
}
switch strings.ToLower(interval.Tag) {
case "work":
// Finde frühesten Start und spätesten Endzeitpunkt für "work" an diesem Tag
if daySummary.WorkStart == "" || interval.StartTime.Format("15:04:05") < daySummary.WorkStart {
daySummary.WorkStart = interval.StartTime.Format("15:04:05")
}
// Nimm das späteste Ende *aller* Arbeitsintervalle des Tages
if intervalEnd.Format("15:04:05") > daySummary.WorkEnd {
daySummary.WorkEnd = intervalEnd.Format("15:04:05")
}
// Die tatsächliche Arbeitszeit wird später in Excel berechnet
// Alternativ: Addiere hier die Dauer zu einer `WorkDuration` hinzu
case "break":
daySummary.BreakDuration += duration
// Handle andere Tags wie "uni", "free", etc.
// Du könntest Ganztags-Tags speichern oder sie als spezielles Intervall behandeln
default:
if daySummary.Tag == "" { // Nur den ersten speziellen Tag nehmen?
daySummary.Tag = interval.Tag
}
}
}
// Konvertiere zu ExcelEntry
var excelEntries []ExcelEntry
for _, summary := range dailySummaries {
entry := ExcelEntry{
Date: summary.Date,
Day: summary.Day,
WorkStart: summary.WorkStart,
WorkEnd: summary.WorkEnd,
BreakDuration: formatDuration(summary.BreakDuration), // Formatieren für Excel
Tag: summary.Tag,
}
excelEntries = append(excelEntries, entry)
}
// Sortiere nach Datum vor dem Schreiben
sort.Slice(excelEntries, func(i, j int) bool {
dateI, _ := time.Parse("2006-01-02", excelEntries[i].Date)
dateJ, _ := time.Parse("2006-01-02", excelEntries[j].Date)
return dateI.Before(dateJ)
})
// Schreibe die Excel-Datei (reuse writeExcelSheet, ggf. anpassen)
return writeExcelSheet(excelEntries, name)
}
// --- Hilfsfunktionen (parseDuration wird nicht mehr benötigt, formatDuration schon) ---
// formatDuration bleibt gleich
func formatDuration(d time.Duration) string {
d = d.Round(time.Second) // Runde auf Sekunden für Konsistenz
h := d / time.Hour
d -= h * time.Hour
m := d / time.Minute
d -= m * time.Minute
s := d / time.Second
return fmt.Sprintf("%d:%02d:%02d", h, m, s)
}
// writeExcelSheet bleibt größtenteils gleich, muss aber ExcelEntry erhalten
// Stelle sicher, dass die Logik zur Berechnung von Brutto/Netto/Saldo in Excel korrekt ist
// oder passe die `ExcelEntry`-Struktur an, um vorkalkulierte Werte zu übergeben.
// Die Funktion muss `excelEntries []ExcelEntry` als Argument nehmen.
// func writeExcelSheet(entries []ExcelEntry, name string) error { ... }
// (Behalte die DailySummary und ExcelEntry structs wie vorher bei)
type DailySummary struct {
Date string
Day string
WorkStart string // Frühester Start
WorkEnd string // Spätestes Ende
BreakDuration time.Duration // Summe der Pausen
Tag string // Für Ganztags-Tags wie Urlaub, Feiertag
}
type ExcelEntry struct {
Date string
Day string
WorkStart string
WorkEnd string
BreakDuration string // Als "HH:MM:SS" String für Excel
Tag string
}
// (Füge hier die `writeExcelSheet`-Funktion aus deiner timewarrior.go ein,
//
// stelle sicher, dass sie `excelEntries []ExcelEntry` akzeptiert)
func writeExcelSheet(entries []ExcelEntry, name string) error {
// ... (Rest der Funktion wie in timewarrior.go, aber mit `entries`)
// Stelle sicher, dass Zeitwerte korrekt als Excel-Zeit/Datum formatiert werden.
// Die Berechnung von Brutto/Netto/Saldo sollte weiterhin über Excel-Formeln erfolgen.
sheetName := fmt.Sprint(time.Now().Year())
f := excelize.NewFile()
defer func() {
if err := f.Close(); err != nil {
fmt.Println(err)
}
}()
index, err := f.NewSheet(sheetName)
if err != nil {
return err
}
// Header setzen (wie zuvor)
f.SetCellValue(sheetName, "B1", "Arbeitszeiten "+sheetName)
// ... (alle anderen Header-Zellen)
f.SetCellValue(sheetName, "B3", "Datum")
f.SetCellValue(sheetName, "D3", "Arbeitszeit")
f.SetCellValue(sheetName, "G3", "Summe")
f.SetCellValue(sheetName, "I3", "Pause")
f.SetCellValue(sheetName, "J3", "Summe")
f.SetCellValue(sheetName, "K3", "Soll")
f.SetCellValue(sheetName, "L3", "Saldo")
f.SetCellValue(sheetName, "N3", "Saldo")
f.SetCellValue(sheetName, "D4", "von")
f.SetCellValue(sheetName, "E4", "bis")
f.SetCellValue(sheetName, "G4", "brutto")
f.SetCellValue(sheetName, "J4", "netto")
f.SetCellValue(sheetName, "L4", "Tag")
f.SetCellValue(sheetName, "N4", "total")
// Zeitformat für Excel
strStyle := "[h]:mm;@" // Format für Dauer/Zeit
timeStyle, err := f.NewStyle(&excelize.Style{
CustomNumFmt: &strStyle,
Alignment: &excelize.Alignment{Horizontal: "right"}, // Zeiten rechtsbündig
})
if err != nil {
return err
}
// Datumsformat für Excel
dateStyle, err := f.NewStyle(&excelize.Style{NumFmt: 14}) // Format "dd/mm/yyyy" oder ähnlich
if err != nil {
return err
}
for num, entry := range entries {
row := fmt.Sprint(num + 6) // Start in Zeile 6
// Datum setzen
dateValue, _ := time.Parse("2006-01-02", entry.Date)
f.SetCellValue(sheetName, "B"+row, dateValue)
f.SetCellStyle(sheetName, "B"+row, "B"+row, dateStyle)
if entry.Tag == "" && entry.WorkStart != "" && entry.WorkEnd != "" { // Nur wenn gearbeitet wurde
// Arbeitszeiten setzen (als Excel-Zeitwerte)
startTime, _ := time.Parse("15:04:05", entry.WorkStart)
endTime, _ := time.Parse("15:04:05", entry.WorkEnd)
// Excel speichert Zeiten als Bruchteil eines Tages
startExcelTime := float64(startTime.Hour())/24.0 + float64(startTime.Minute())/(24.0*60.0) + float64(startTime.Second())/(24.0*60.0*60.0)
endExcelTime := float64(endTime.Hour())/24.0 + float64(endTime.Minute())/(24.0*60.0) + float64(endTime.Second())/(24.0*60.0*60.0)
// Wenn Endzeit vor Startzeit liegt (über Mitternacht gearbeitet - unwahrscheinlich für Tageszusammenfassung)
if endExcelTime < startExcelTime {
endExcelTime += 1.0 // Füge einen Tag hinzu
}
f.SetCellValue(sheetName, "D"+row, startExcelTime)
f.SetCellStyle(sheetName, "D"+row, "D"+row, timeStyle)
f.SetCellValue(sheetName, "E"+row, endExcelTime)
f.SetCellStyle(sheetName, "E"+row, "E"+row, timeStyle)
// Formeln für Berechnungen
f.SetCellFormula(sheetName, "G"+row, fmt.Sprintf("IF(E%[1]d<D%[1]d,E%[1]d+1-D%[1]d,E%[1]d-D%[1]d)", num+6)) // Brutto = Ende - Anfang (Mitternacht berücksichtigen)
f.SetCellStyle(sheetName, "G"+row, "G"+row, timeStyle)
// Pause setzen (als Excel-Zeitwert)
breakDur, _ := time.ParseDuration(strings.ReplaceAll(entry.BreakDuration, ":", "h", 1) + "m" + strings.Split(entry.BreakDuration, ":")[2] + "s")
breakExcelTime := float64(breakDur) / float64(24*time.Hour)
f.SetCellValue(sheetName, "I"+row, breakExcelTime)
f.SetCellStyle(sheetName, "I"+row, "I"+row, timeStyle)
f.SetCellFormula(sheetName, "J"+row, fmt.Sprintf("G%d-I%d", num+6, num+6)) // Netto = Brutto - Pause
f.SetCellStyle(sheetName, "J"+row, "J"+row, timeStyle)
// Soll-Zeit setzen (Beispiel, anpassen nach Bedarf)
var sollExcelTime float64
switch entry.Day {
case "Mon", "Tue", "Thu", "Fri": // Annahme: Mo,Di,Do,Fr 8h
sollExcelTime = 8.0 / 24.0
case "Wed": // Annahme: Mi 4h
sollExcelTime = 4.0 / 24.0
default:
sollExcelTime = 0.0 // Wochenende
}
f.SetCellValue(sheetName, "K"+row, sollExcelTime)
f.SetCellStyle(sheetName, "K"+row, "K"+row, timeStyle)
f.SetCellFormula(sheetName, "L"+row, fmt.Sprintf("J%d-K%d", num+6, num+6)) // Saldo Tag = Netto - Soll
f.SetCellStyle(sheetName, "L"+row, "L"+row, timeStyle)
} else if entry.Tag != "" {
// Ganztags-Tags (Urlaub, Feiertag, etc.)
text := ""
switch strings.ToLower(entry.Tag) { // Kleinbuchstaben für Vergleich
case "uni":
text = "Hochschule"
case "urlaub":
text = "Urlaub"
case "feiertag":
text = "Feiertag"
case "krank":
text = "Krank"
case "free":
text = "Frei"
default:
text = entry.Tag // Unbekannte Tags anzeigen
}
f.SetCellValue(sheetName, "D"+row, text)
// Optional: Saldo für diese Tage auf 0 setzen oder spezielle Behandlung
f.SetCellValue(sheetName, "L"+row, 0.0)
f.SetCellStyle(sheetName, "L"+row, "L"+row, timeStyle)
f.SetCellValue(sheetName, "J"+row, 0.0) // Kein Netto
f.SetCellStyle(sheetName, "J"+row, "J"+row, timeStyle)
} else {
// Kein Tag und keine Arbeitszeit (z.B. Wochenende ohne Eintrag)
f.SetCellValue(sheetName, "L"+row, 0.0) // Saldo 0
f.SetCellStyle(sheetName, "L"+row, "L"+row, timeStyle)
f.SetCellValue(sheetName, "J"+row, 0.0) // Kein Netto
f.SetCellStyle(sheetName, "J"+row, "J"+row, timeStyle)
}
// Gesamtsaldo Formel
f.SetCellFormula(sheetName, "N"+row, fmt.Sprintf("N%d+L%d", num+5, num+6)) // Total = Total Vortag + Saldo Tag
f.SetCellStyle(sheetName, "N"+row, "N"+row, timeStyle)
}
// Spaltenbreiten anpassen (optional, aber nett)
f.SetColWidth(sheetName, "B", "B", 12) // Datum
f.SetColWidth(sheetName, "D", "E", 10) // Von/Bis
f.SetColWidth(sheetName, "G", "G", 10) // Brutto
f.SetColWidth(sheetName, "I", "I", 10) // Pause
f.SetColWidth(sheetName, "J", "J", 10) // Netto
f.SetColWidth(sheetName, "K", "K", 10) // Soll
f.SetColWidth(sheetName, "L", "L", 10) // Saldo Tag
f.SetColWidth(sheetName, "N", "N", 12) // Saldo Total
f.SetActiveSheet(index)
if err := f.SaveAs(name); err != nil {
// Verwende log statt fmt für Fehler
log.Printf("Failed to save excel file %s: %v", name, err)
return fmt.Errorf("failed to save excel file %s: %w", name, err)
}
log.Printf("Excel file saved as %s", name)
return nil
}