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