Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions cmd/clock/clock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package clock

import (
"github.com/seletz/odoo-work-cli/internal/app"
"github.com/spf13/cobra"
)

func CMD(deps *app.Deps) *cobra.Command {
cmd := &cobra.Command{
Use: "clock",
Short: "Clock in/out and attendance status",
}

cmd.AddCommand(inCMD(deps))
cmd.AddCommand(outCMD(deps))
cmd.AddCommand(statusCMD(deps))

return cmd
}
32 changes: 32 additions & 0 deletions cmd/clock/in.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package clock

import (
"fmt"
"time"

"github.com/seletz/odoo-work-cli/internal/app"
"github.com/spf13/cobra"
)

func inCMD(deps *app.Deps) *cobra.Command {
InCmd := &cobra.Command{
Use: "in",
Short: "Clock in (start attendance)",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := deps.RequireClient()
if err != nil {
return err
}

_, err = client.ClockIn()
if err != nil {
return err
}

fmt.Printf("Clocked in at %s\n", time.Now().Format("15:04"))
return nil
},
}

return InCmd
}
33 changes: 33 additions & 0 deletions cmd/clock/out.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package clock

import (
"fmt"
"time"

"github.com/seletz/odoo-work-cli/internal/app"
"github.com/seletz/odoo-work-cli/internal/tui"
"github.com/spf13/cobra"
)

func outCMD(deps *app.Deps) *cobra.Command {
cmd := &cobra.Command{
Use: "out",
Short: "Clock out (end attendance)",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := deps.RequireClient()
if err != nil {
return err
}

rec, err := client.ClockOut()
if err != nil {
return err
}

fmt.Printf("Clocked out at %s\n", time.Now().Format("15:04"))
fmt.Printf("Duration: %s (%.2fh)\n", tui.FormatHours(rec.WorkedHours), rec.WorkedHours)
return nil
},
}
return cmd
}
61 changes: 61 additions & 0 deletions cmd/clock/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package clock

import (
"fmt"
"time"

"github.com/seletz/odoo-work-cli/internal/app"
"github.com/seletz/odoo-work-cli/internal/tui"
"github.com/spf13/cobra"
)

func statusCMD(deps *app.Deps) *cobra.Command {
cmd := &cobra.Command{
Use: "status",
Short: "Show current attendance status",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := deps.RequireClient()
if err != nil {
return err
}

status, err := client.AttendanceStatus()
if err != nil {
return err
}

if status.ClockedIn && status.CheckIn != nil {
elapsed := time.Since(*status.CheckIn).Hours()
fmt.Printf("Status: Clocked in since %s (%s elapsed)\n",
status.CheckIn.Local().Format("15:04"),
tui.FormatHours(elapsed))
} else {
fmt.Println("Status: Not clocked in")
}

if len(status.Periods) > 0 {
fmt.Print("\nToday's attendance:\n\n")
fmt.Printf("%-3s %-10s %-10s %s\n", "#", "Check In", "Check Out", "Duration")
fmt.Printf("%-3s %-10s %-10s %s\n", "---", "----------", "----------", "--------")
for i, p := range status.Periods {
checkIn := p.CheckIn.Local().Format("15:04")
var checkOut, duration string
if p.CheckOut != nil {
checkOut = p.CheckOut.Local().Format("15:04")
duration = tui.FormatHours(p.WorkedHours)
} else {
checkOut = "--:--"
elapsed := time.Since(p.CheckIn).Hours()
duration = tui.FormatHours(elapsed) + " (running)"
}
fmt.Printf("%-3d %-10s %-10s %s\n", i+1, checkIn, checkOut, duration)
}
fmt.Printf("\nTotal: %s\n", tui.FormatHours(status.TotalHours))
} else {
fmt.Println("\nNo attendance records today.")
}
return nil
},
}
return cmd
}
52 changes: 52 additions & 0 deletions cmd/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package config

import (
"bytes"
"fmt"

"github.com/BurntSushi/toml"
"github.com/seletz/odoo-work-cli/internal/config"
"github.com/spf13/cobra"
)

var configMerged bool

func CMD(cfgFile *string) *cobra.Command {

cmd := &cobra.Command{
Use: "config",
Short: "Show discovered config files and merged configuration",
RunE: func(cmd *cobra.Command, args []string) error {
result, err := config.Discover(*cfgFile)
if err != nil {
return err
}

if !configMerged {
if len(result.Files) == 0 {
fmt.Println("No config files discovered.")
} else {
fmt.Println("Discovered config files (merge order):")
for _, f := range result.Files {
fmt.Printf(" %s\n", f)
}
}
return nil
}

// Password is tagged toml:"-" on Config, so the encoder
// omits it automatically — output is a valid config file.
var buf bytes.Buffer
if err := toml.NewEncoder(&buf).Encode(result.Config); err != nil {
return fmt.Errorf("encoding config: %w", err)
}
fmt.Print(buf.String())
return nil
},
}

cmd.Flags().BoolVar(&configMerged, "merged", false, "print merged TOML config (password redacted)")

cmd.AddCommand(installcmd())
return cmd
}
52 changes: 52 additions & 0 deletions cmd/config/install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package config

import (
"fmt"
"strings"

"github.com/seletz/odoo-work-cli/internal/config"
"github.com/spf13/cobra"
)

func installcmd() *cobra.Command {

cmd := &cobra.Command{
Use: "install",
Short: "Create a default config file in the platform config directory",
RunE: func(cmd *cobra.Command, args []string) error {
path, err := config.DefaultConfigPath()
if err != nil {
return err
}

fmt.Printf("This will create a default config file at:\n %s\n\n", path)

if !confirmPrompt("Continue?") {
fmt.Println("Aborted.")
return nil
}

if err := config.InstallConfig(path); err != nil {
return err
}

fmt.Printf("Config file created at: %s\n", path)
fmt.Println("Edit it to set your Odoo URL, database, and username.")
fmt.Println("Set ODOO_PASSWORD via environment variable (see .env.1p).")
return nil
},
}

return cmd
}

// confirmPrompt prints msg and reads y/N from stdin.
func confirmPrompt(msg string) bool {
fmt.Printf("%s [y/N] ", msg)
var response string
if _, err := fmt.Scanln(&response); err != nil {
return false
}
response = strings.TrimSpace(strings.ToLower(response))
return response == "y" || response == "yes"
}
49 changes: 49 additions & 0 deletions cmd/entries/add.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package entries

import (
"fmt"

"github.com/seletz/odoo-work-cli/internal/app"
"github.com/spf13/cobra"
)

func addCmd(deps *app.Deps) *cobra.Command {
ops := &subOps{}
cmd := &cobra.Command{
Use: "add",
Short: "Create a new timesheet entry",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := deps.RequireClient()
if err != nil {
return err
}

params, err := buildTimesheetWriteParams(ops.projectID,
ops.taskID,
ops.date,
ops.description,
ops.hours)
if err != nil {
return err
}

id, err := client.CreateTimesheet(params)
if err != nil {
return err
}

fmt.Printf("Created entry %d\n", id)
return nil
},
}

cmd.Flags().Int64Var(&ops.projectID, "project-id", 0, "Odoo project ID (required)")
cmd.Flags().Int64Var(&ops.taskID, "task-id", 0, "Odoo task ID (optional)")
cmd.Flags().StringVar(&ops.date, "date", "", "entry date YYYY-MM-DD (defaults to today)")
cmd.Flags().StringVar(&ops.hours, "hours", "", "hours worked (e.g. 2.5 or 2:30)")
cmd.Flags().StringVar(&ops.description, "description", "", "work description (required)")
_ = cmd.MarkFlagRequired("hours")
_ = cmd.MarkFlagRequired("description")

return cmd
}
35 changes: 35 additions & 0 deletions cmd/entries/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package entries

import (
"fmt"

"github.com/seletz/odoo-work-cli/internal/app"
"github.com/spf13/cobra"
)

func deleteCmd(deps *app.Deps) *cobra.Command {
cmd := &cobra.Command{
Use: "delete ID",
Short: "Delete a timesheet entry",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := deps.RequireClient()
if err != nil {
return err
}

id, err := parseEntryID(args[0])
if err != nil {
return err
}

if err := client.DeleteTimesheet(id); err != nil {
return err
}

fmt.Printf("Deleted entry %d\n", id)
return nil
},
}
return cmd
}
Loading