565 lines
19 KiB
Go
565 lines
19 KiB
Go
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
|
|
}
|