summaryrefslogtreecommitdiff
path: root/internal/tui/projects_box.go
diff options
context:
space:
mode:
authorT <t@tjp.lol>2025-08-05 12:36:30 -0600
committerT <t@tjp.lol>2025-08-06 12:13:11 -0600
commit65e2ed65775d64afbc6065a3b4ac1069020093ca (patch)
treef94fabfed5be2d2622429ebc7c8af1bf51085824 /internal/tui/projects_box.go
parent665bd389a0a1c8adadcaa1122e846cc81f5ead31 (diff)
most features in TUI working, remaining unimplemented keybinds need a modal view
Diffstat (limited to 'internal/tui/projects_box.go')
-rw-r--r--internal/tui/projects_box.go194
1 files changed, 194 insertions, 0 deletions
diff --git a/internal/tui/projects_box.go b/internal/tui/projects_box.go
new file mode 100644
index 0000000..f90ac03
--- /dev/null
+++ b/internal/tui/projects_box.go
@@ -0,0 +1,194 @@
+package tui
+
+import (
+ "fmt"
+ "strconv"
+
+ "punchcard/internal/queries"
+
+ "github.com/charmbracelet/lipgloss/v2"
+)
+
+type ClientsProjectsModel struct {
+ clients []queries.Client
+ projects map[int64][]queries.Project
+ selectedClient int
+ selectedProject *int
+}
+
+// NewClientsProjectsModel creates a new clients/projects model
+func NewClientsProjectsModel() ClientsProjectsModel {
+ return ClientsProjectsModel{}
+}
+
+// View renders the clients/projects box
+func (m ClientsProjectsModel) View(width, height int, isSelected bool) string {
+ var content string
+
+ if len(m.clients) == 0 {
+ content = "No clients found\n\nUse 'punch add client' to\nadd your first client."
+ } else {
+ content = m.renderClientsAndProjects()
+ }
+
+ // Apply box styling
+ style := unselectedBoxStyle
+ if isSelected {
+ style = selectedBoxStyle
+ }
+
+ title := "👥 Clients & Projects"
+
+ return style.Width(width).Height(height).Render(
+ fmt.Sprintf("%s\n\n%s", title, content),
+ )
+}
+
+// renderClientsAndProjects renders the clients and their projects
+func (m ClientsProjectsModel) renderClientsAndProjects() string {
+ var content string
+ absoluteRowIndex := 0
+
+ for i, client := range m.clients {
+ if i > 0 {
+ content += "\n"
+ }
+
+ clientLine := fmt.Sprintf("• %s", client.Name)
+ if client.BillableRate.Valid {
+ rateInDollars := float64(client.BillableRate.Int64) / 100.0
+ clientLine += fmt.Sprintf(" ($%.2f/hr)", rateInDollars)
+ }
+
+ // Highlight if this client is selected
+ clientStyle := lipgloss.NewStyle().Bold(true)
+ if m.selectedClient == i && m.selectedProject == nil {
+ clientStyle = clientStyle.Background(lipgloss.Color("62")).Foreground(lipgloss.Color("230"))
+ }
+ content += clientStyle.Render(clientLine) + "\n"
+ absoluteRowIndex++
+
+ clientProjects := m.projects[client.ID]
+ if len(clientProjects) == 0 {
+ content += " └── (no projects)\n"
+ } else {
+ for j, project := range clientProjects {
+ prefix := "├──"
+ if j == len(clientProjects)-1 {
+ prefix = "└──"
+ }
+
+ projectLine := fmt.Sprintf(" %s %s", prefix, project.Name)
+ if project.BillableRate.Valid {
+ rateInDollars := float64(project.BillableRate.Int64) / 100.0
+ projectLine += fmt.Sprintf(" ($%.2f/hr)", rateInDollars)
+ }
+
+ projectStyle := lipgloss.NewStyle()
+ if m.selectedClient == i && m.selectedProject != nil && *m.selectedProject == j {
+ projectStyle = projectStyle.Background(lipgloss.Color("62")).Foreground(lipgloss.Color("230"))
+ }
+ content += projectStyle.Render(projectLine) + "\n"
+ }
+ }
+ }
+
+ return content
+}
+
+func (m *ClientsProjectsModel) changeSelection(forward bool) {
+ if forward {
+ m.changeSelectionForward()
+ } else {
+ m.changeSelectionBackward()
+ }
+}
+
+func (m *ClientsProjectsModel) changeSelectionForward() {
+ selectedClient := m.clients[m.selectedClient]
+ projects := m.projects[selectedClient.ID]
+
+ if m.selectedProject == nil {
+ // starting with a client selected
+ if len(projects) > 0 {
+ // can jump into the first project
+ var zero int = 0
+ m.selectedProject = &zero
+ return
+ }
+
+ // there is no next client - at the bottom, no-op
+ if m.selectedClient == len(m.clients)-1 {
+ return
+ }
+
+ // jump to next client
+ m.selectedClient++
+ return
+ }
+
+ if *m.selectedProject == len(projects)-1 {
+ // at last project
+
+ if m.selectedClient == len(m.clients)-1 {
+ // also at last client - at the bottom, no-op
+ return
+ }
+
+ // jump to next client, no project
+ m.selectedClient++
+ m.selectedProject = nil
+ return
+ }
+
+ // jump to next project
+ *m.selectedProject++
+}
+
+func (m *ClientsProjectsModel) changeSelectionBackward() {
+ selectedClient := m.clients[m.selectedClient]
+ projects := m.projects[selectedClient.ID]
+
+ if m.selectedProject == nil {
+ // starting with a client selected
+ if m.selectedClient == 0 {
+ // at first client - at the start, no-op
+ return
+ }
+
+ m.selectedClient--
+ selectedClient = m.clients[m.selectedClient]
+ projects = m.projects[selectedClient.ID]
+
+ if len(projects) > 0 {
+ // previous client has projects, jump to last one
+ i := len(projects) - 1
+ m.selectedProject = &i
+ }
+
+ // otherwise selectedProject is already nil
+ return
+ }
+
+ if *m.selectedProject == 0 {
+ // selected first project - jump up to client
+ m.selectedProject = nil
+ return
+ }
+
+ // otherwise, jump to previous project in same client
+ *m.selectedProject--
+}
+
+func (m ClientsProjectsModel) selection() (string, string, string, *float64) {
+ client := m.clients[m.selectedClient]
+ clientID := strconv.FormatInt(client.ID, 10)
+
+ projectID := ""
+ if m.selectedProject != nil {
+ project := m.projects[client.ID][*m.selectedProject]
+ projectID = strconv.FormatInt(project.ID, 10)
+ }
+
+ return clientID, projectID, "", nil
+}