Compare commits
1 commit
main
...
dev/rebuil
| Author | SHA1 | Date | |
|---|---|---|---|
| 07c0d782af |
8 changed files with 700 additions and 43 deletions
61
app.go
61
app.go
|
|
@ -13,24 +13,27 @@ import (
|
|||
)
|
||||
|
||||
type App struct {
|
||||
cfg Config
|
||||
flags Flags
|
||||
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)
|
||||
}
|
||||
|
||||
return &App{
|
||||
cfg: cfg,
|
||||
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,26 +151,44 @@ 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 {
|
||||
|
|
|
|||
94
cmd.go
94
cmd.go
|
|
@ -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")
|
||||
|
|
|
|||
13
config.go
13
config.go
|
|
@ -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
1
go.mod
|
|
@ -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
2
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
7
main.go
7
main.go
|
|
@ -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
565
tracker_sqlite.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue