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),
	)
}