summaryrefslogtreecommitdiff
path: root/internal/tui/projects_box.go
diff options
context:
space:
mode:
authorT <t@tjp.lol>2025-09-29 15:04:44 -0600
committerT <t@tjp.lol>2025-09-30 11:40:45 -0600
commit7ba68d333bc20b5795ccfd3870546a05eee60470 (patch)
tree12dc4b017803b7d01844fd42b9e3be281cbbd986 /internal/tui/projects_box.go
parentbce8dbb58165e443902d9dae3909225ef42630c4 (diff)
Support for archiving clients and projects.HEADmain
Diffstat (limited to 'internal/tui/projects_box.go')
-rw-r--r--internal/tui/projects_box.go210
1 files changed, 178 insertions, 32 deletions
diff --git a/internal/tui/projects_box.go b/internal/tui/projects_box.go
index 3bf44b5..cd50d5e 100644
--- a/internal/tui/projects_box.go
+++ b/internal/tui/projects_box.go
@@ -14,6 +14,7 @@ type ClientsProjectsModel struct {
projects map[int64][]queries.Project
selectedClient int
selectedProject *int
+ showArchived bool
}
// NewClientsProjectsModel creates a new clients/projects model
@@ -21,12 +22,48 @@ func NewClientsProjectsModel() ClientsProjectsModel {
return ClientsProjectsModel{}
}
+// visibleClients returns the list of clients that should be displayed
+func (m ClientsProjectsModel) visibleClients() []queries.Client {
+ if m.showArchived {
+ return m.clients
+ }
+
+ visible := make([]queries.Client, 0, len(m.clients))
+ for _, client := range m.clients {
+ if client.Archived == 0 {
+ visible = append(visible, client)
+ }
+ }
+ return visible
+}
+
+// visibleProjects returns the list of projects for a client that should be displayed
+func (m ClientsProjectsModel) visibleProjects(clientID int64) []queries.Project {
+ allProjects := m.projects[clientID]
+ if m.showArchived {
+ return allProjects
+ }
+
+ visible := make([]queries.Project, 0, len(allProjects))
+ for _, project := range allProjects {
+ if project.Archived == 0 {
+ visible = append(visible, project)
+ }
+ }
+ return visible
+}
+
// 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 = inactiveTimerStyle.Render("No clients found\n\nUse 'punch add client' to\nadd your first client.")
+ visibleClients := m.visibleClients()
+ if len(visibleClients) == 0 {
+ if len(m.clients) == 0 {
+ content = inactiveTimerStyle.Render("No clients found\n\nUse 'punch add client' to\nadd your first client.")
+ } else {
+ content = inactiveTimerStyle.Render("All clients archived\n\nPress '.' to show archived")
+ }
} else {
content = m.renderClientsAndProjects()
}
@@ -47,48 +84,58 @@ func (m ClientsProjectsModel) View(width, height int, isSelected bool) string {
// renderClientsAndProjects renders the clients and their projects
func (m ClientsProjectsModel) renderClientsAndProjects() string {
var content string
- absoluteRowIndex := 0
+ visibleClients := m.visibleClients()
- for i, client := range m.clients {
+ for i, client := range visibleClients {
if i > 0 {
content += "\n"
}
- clientLine := fmt.Sprintf("• %s", client.Name)
+ // Build client name and rate
+ clientText := client.Name
if client.BillableRate.Valid {
rateInDollars := float64(client.BillableRate.Int64) / 100.0
- clientLine += fmt.Sprintf(" ($%.2f/hr)", rateInDollars)
+ clientText += fmt.Sprintf(" ($%.2f/hr)", rateInDollars)
}
- // Highlight if this client is selected
+ // Style for client text
clientStyle := lipgloss.NewStyle().Bold(true)
if m.selectedClient == i && m.selectedProject == nil {
clientStyle = clientStyle.Background(lipgloss.Color("62")).Foreground(lipgloss.Color("230"))
+ } else if client.Archived != 0 {
+ // Gray out archived clients
+ clientStyle = clientStyle.Foreground(lipgloss.Color("246"))
}
- content += clientStyle.Render(clientLine) + "\n"
- absoluteRowIndex++
- clientProjects := m.projects[client.ID]
- if len(clientProjects) == 0 {
+ content += "• " + clientStyle.Render(clientText) + "\n"
+
+ visibleProjects := m.visibleProjects(client.ID)
+ if len(visibleProjects) == 0 {
content += " └── (no projects)\n"
} else {
- for j, project := range clientProjects {
+ for j, project := range visibleProjects {
prefix := "├──"
- if j == len(clientProjects)-1 {
+ if j == len(visibleProjects)-1 {
prefix = "└──"
}
- projectLine := fmt.Sprintf(" %s %s", prefix, project.Name)
+ // Build project name and rate
+ projectText := project.Name
if project.BillableRate.Valid {
rateInDollars := float64(project.BillableRate.Int64) / 100.0
- projectLine += fmt.Sprintf(" ($%.2f/hr)", rateInDollars)
+ projectText += fmt.Sprintf(" ($%.2f/hr)", rateInDollars)
}
+ // Style for project text
projectStyle := lipgloss.NewStyle()
if m.selectedClient == i && m.selectedProject != nil && *m.selectedProject == j {
projectStyle = projectStyle.Background(lipgloss.Color("62")).Foreground(lipgloss.Color("230"))
+ } else if project.Archived != 0 {
+ // Gray out archived projects
+ projectStyle = projectStyle.Foreground(lipgloss.Color("246"))
}
- content += projectStyle.Render(projectLine) + "\n"
+
+ content += fmt.Sprintf(" %s ", prefix) + projectStyle.Render(projectText) + "\n"
}
}
}
@@ -105,16 +152,17 @@ func (m *ClientsProjectsModel) changeSelection(forward bool) {
}
func (m *ClientsProjectsModel) changeSelectionForward() {
- if len(m.clients) == 0 {
+ visibleClients := m.visibleClients()
+ if len(visibleClients) == 0 {
return
}
- selectedClient := m.clients[m.selectedClient]
- projects := m.projects[selectedClient.ID]
+ selectedClient := visibleClients[m.selectedClient]
+ visibleProjects := m.visibleProjects(selectedClient.ID)
if m.selectedProject == nil {
// starting with a client selected
- if len(projects) > 0 {
+ if len(visibleProjects) > 0 {
// can jump into the first project
zero := 0
m.selectedProject = &zero
@@ -122,7 +170,7 @@ func (m *ClientsProjectsModel) changeSelectionForward() {
}
// there is no next client - at the bottom, no-op
- if m.selectedClient == len(m.clients)-1 {
+ if m.selectedClient == len(visibleClients)-1 {
return
}
@@ -131,10 +179,10 @@ func (m *ClientsProjectsModel) changeSelectionForward() {
return
}
- if *m.selectedProject == len(projects)-1 {
+ if *m.selectedProject == len(visibleProjects)-1 {
// at last project
- if m.selectedClient == len(m.clients)-1 {
+ if m.selectedClient == len(visibleClients)-1 {
// also at last client - at the bottom, no-op
return
}
@@ -150,11 +198,12 @@ func (m *ClientsProjectsModel) changeSelectionForward() {
}
func (m *ClientsProjectsModel) changeSelectionBackward() {
- if len(m.clients) == 0 {
+ visibleClients := m.visibleClients()
+ if len(visibleClients) == 0 {
return
}
- selectedClient := m.clients[m.selectedClient]
+ selectedClient := visibleClients[m.selectedClient]
if m.selectedProject == nil {
// starting with a client selected
@@ -164,12 +213,12 @@ func (m *ClientsProjectsModel) changeSelectionBackward() {
}
m.selectedClient--
- selectedClient = m.clients[m.selectedClient]
- projects := m.projects[selectedClient.ID]
+ selectedClient = visibleClients[m.selectedClient]
+ visibleProjects := m.visibleProjects(selectedClient.ID)
- if len(projects) > 0 {
+ if len(visibleProjects) > 0 {
// previous client has projects, jump to last one
- i := len(projects) - 1
+ i := len(visibleProjects) - 1
m.selectedProject = &i
}
@@ -188,18 +237,115 @@ func (m *ClientsProjectsModel) changeSelectionBackward() {
}
func (m ClientsProjectsModel) selection() (string, string, string, *float64) {
- if len(m.clients) == 0 {
+ visibleClients := m.visibleClients()
+ if len(visibleClients) == 0 {
return "", "", "", nil
}
- client := m.clients[m.selectedClient]
+ client := visibleClients[m.selectedClient]
clientID := strconv.FormatInt(client.ID, 10)
projectID := ""
if m.selectedProject != nil {
- project := m.projects[client.ID][*m.selectedProject]
+ visibleProjects := m.visibleProjects(client.ID)
+ project := visibleProjects[*m.selectedProject]
projectID = strconv.FormatInt(project.ID, 10)
}
return clientID, projectID, "", nil
}
+
+// resetSelection clamps the selection to valid bounds after visibility changes
+func (m *ClientsProjectsModel) resetSelection() {
+ visibleClients := m.visibleClients()
+ if len(visibleClients) == 0 {
+ m.selectedClient = 0
+ m.selectedProject = nil
+ return
+ }
+
+ // Clamp client selection
+ if m.selectedClient >= len(visibleClients) {
+ m.selectedClient = len(visibleClients) - 1
+ }
+
+ // Clamp project selection
+ if m.selectedProject != nil {
+ client := visibleClients[m.selectedClient]
+ visibleProjects := m.visibleProjects(client.ID)
+ if len(visibleProjects) == 0 {
+ m.selectedProject = nil
+ } else if *m.selectedProject >= len(visibleProjects) {
+ i := len(visibleProjects) - 1
+ m.selectedProject = &i
+ }
+ }
+}
+
+// getSelectedIDs returns the currently selected client ID and optional project ID
+func (m ClientsProjectsModel) getSelectedIDs() (clientID int64, projectID *int64) {
+ visibleClients := m.visibleClients()
+ if len(visibleClients) == 0 {
+ return 0, nil
+ }
+ if m.selectedClient >= len(visibleClients) {
+ return 0, nil
+ }
+
+ client := visibleClients[m.selectedClient]
+ clientID = client.ID
+
+ if m.selectedProject != nil {
+ visibleProjects := m.visibleProjects(client.ID)
+ if *m.selectedProject >= len(visibleProjects) {
+ return clientID, nil
+ }
+ project := visibleProjects[*m.selectedProject]
+ projectID = &project.ID
+ }
+
+ return clientID, projectID
+}
+
+// restoreSelection tries to restore selection to the given IDs, or resets to top if not found
+func (m *ClientsProjectsModel) restoreSelection(clientID int64, projectID *int64) {
+ visibleClients := m.visibleClients()
+ if len(visibleClients) == 0 {
+ m.selectedClient = 0
+ m.selectedProject = nil
+ return
+ }
+
+ // Try to find the client
+ clientFound := false
+ for i, client := range visibleClients {
+ if client.ID == clientID {
+ m.selectedClient = i
+ clientFound = true
+
+ // Try to find the project if one was selected
+ if projectID != nil {
+ visibleProjects := m.visibleProjects(client.ID)
+ for j, project := range visibleProjects {
+ if project.ID == *projectID {
+ m.selectedProject = &j
+ return
+ }
+ }
+ // Project not found, select client only
+ m.selectedProject = nil
+ return
+ }
+
+ // No project was selected, just client
+ m.selectedProject = nil
+ return
+ }
+ }
+
+ // Client not found, reset to top
+ if !clientFound {
+ m.selectedClient = 0
+ m.selectedProject = nil
+ }
+}