diff options
Diffstat (limited to 'internal/tui/projects_box.go')
-rw-r--r-- | internal/tui/projects_box.go | 194 |
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 +} |