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 }