1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
|
package main
import (
"database/sql"
"fmt"
"log"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// InvoiceAction represents the action taken in the invoice preview
type InvoiceAction int
const (
InvoiceActionNone InvoiceAction = iota
InvoiceActionCancel
InvoiceActionConfirm
)
// InvoicePreviewModel manages the invoice preview dialog
type InvoicePreviewModel struct {
entries []TimeEntry
invoiceNumber string
totalHours float64
clientID int64
clientName string
db *sql.DB
windowWidth int
windowHeight int
}
// NewInvoicePreviewModel creates a new invoice preview model
func NewInvoicePreviewModel(
db *sql.DB,
clientID int64,
clientName string,
entries []TimeEntry,
invoiceNumber string,
totalHours float64,
windowWidth int,
windowHeight int,
) *InvoicePreviewModel {
return &InvoicePreviewModel{
entries: entries,
invoiceNumber: invoiceNumber,
totalHours: totalHours,
clientID: clientID,
clientName: clientName,
db: db,
windowWidth: windowWidth,
windowHeight: windowHeight,
}
}
// Update handles messages for the invoice preview
// Returns (updatedModel, cmd, action)
// action indicates what the parent should do (cancel, confirm, or nothing)
func (i *InvoicePreviewModel) Update(msg tea.Msg) (*InvoicePreviewModel, tea.Cmd, InvoiceAction) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
i.windowWidth = msg.Width
i.windowHeight = msg.Height
return i, nil, InvoiceActionNone
case tea.KeyMsg:
switch msg.Type {
case tea.KeyEscape, tea.KeyCtrlC:
// Cancel invoice preview
return i, nil, InvoiceActionCancel
case tea.KeyEnter:
// Confirm and create invoice
if len(i.entries) == 0 {
// No entries, just cancel
return i, nil, InvoiceActionCancel
}
// Collect entry IDs
var entryIDs []int64
for _, entry := range i.entries {
entryIDs = append(entryIDs, entry.ID)
}
// Create invoice
_, err := CreateInvoice(i.db, i.clientID, entryIDs, nil, "")
if err != nil {
log.Printf("Error creating invoice: %v", err)
// Stay in preview mode to let user try again or cancel
return i, nil, InvoiceActionNone
}
log.Printf("Created invoice %s for client %s", i.invoiceNumber, i.clientName)
return i, nil, InvoiceActionConfirm
}
}
return i, nil, InvoiceActionNone
}
// View renders the invoice preview dialog
func (i *InvoicePreviewModel) View() string {
// Calculate date range
var minDate, maxDate string
if len(i.entries) > 0 {
minDate = timestampToDate(i.entries[0].StartTime)
maxDate = timestampToDate(i.entries[len(i.entries)-1].StartTime)
}
// Build preview dialog
dialogStyle := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(ColorInvoiceBorder).
Padding(1, 2).
Background(ColorDialogBackground).
Foreground(ColorDialogForeground)
titleStyle := lipgloss.NewStyle().
Bold(true).
Foreground(ColorInvoiceBorder)
labelStyle := lipgloss.NewStyle().
Foreground(ColorTextDim)
valueStyle := lipgloss.NewStyle().
Bold(true).
Foreground(ColorAgeNewest)
instructionStyle := lipgloss.NewStyle().
Foreground(ColorRestartable). // Orange
Italic(true)
dialogContent := titleStyle.Render(fmt.Sprintf("Create Invoice for Client: %s", i.clientName)) + "\n\n"
dialogContent += labelStyle.Render("Date Range: ") + valueStyle.Render(fmt.Sprintf("%s to %s", minDate, maxDate)) + "\n"
dialogContent += labelStyle.Render("Entries: ") + valueStyle.Render(fmt.Sprintf("%d", len(i.entries))) + "\n"
dialogContent += labelStyle.Render("Total Hours: ") + valueStyle.Render(fmt.Sprintf("%.1f", i.totalHours)) + "\n\n"
dialogContent += labelStyle.Render("Invoice Number: ") + valueStyle.Render(i.invoiceNumber) + "\n\n"
dialogContent += instructionStyle.Render("[Enter] Create [Esc] Cancel")
dialog := dialogStyle.Render(dialogContent)
// Create overlay by positioning dialog in center
return lipgloss.Place(
i.windowWidth,
i.windowHeight,
lipgloss.Center,
lipgloss.Center,
dialog,
lipgloss.WithWhitespaceChars(" "),
lipgloss.WithWhitespaceForeground(ColorOverlayBackground),
)
}
|