diff options
Diffstat (limited to 'internal/tui')
| -rw-r--r-- | internal/tui/app.go | 47 | ||||
| -rw-r--r-- | internal/tui/keys.go | 17 | ||||
| -rw-r--r-- | internal/tui/shared.go | 46 |
3 files changed, 90 insertions, 20 deletions
diff --git a/internal/tui/app.go b/internal/tui/app.go index 9850595..05ed358 100644 --- a/internal/tui/app.go +++ b/internal/tui/app.go @@ -16,13 +16,16 @@ import ( type BoxType int const ( - TimerBox BoxType = iota + ContractorBox BoxType = iota + TimerBox ProjectsBox HistoryBox ) func (b BoxType) String() string { switch b { + case ContractorBox: + return "Contractor" case TimerBox: return "Timer" case ProjectsBox: @@ -36,20 +39,24 @@ func (b BoxType) String() string { func (b BoxType) Next() BoxType { switch b { + case ContractorBox: + return TimerBox case TimerBox: return ProjectsBox case ProjectsBox: return HistoryBox case HistoryBox: - return TimerBox + return ContractorBox } return 0 } func (b BoxType) Prev() BoxType { switch b { - case TimerBox: + case ContractorBox: return HistoryBox + case TimerBox: + return ContractorBox case HistoryBox: return ProjectsBox case ProjectsBox: @@ -272,14 +279,12 @@ func (m AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m *AppModel) filterHistoryByProjectBox() { - selectedClient := m.projectsBox.clients[m.projectsBox.selectedClient] - m.historyBox.filter.ClientID = &selectedClient.ID - if m.projectsBox.selectedProject != nil { - project := m.projectsBox.projects[selectedClient.ID][*m.projectsBox.selectedProject] - m.historyBox.filter.ProjectID = &project.ID - } else { - m.historyBox.filter.ProjectID = nil + clientID, projectID := m.projectsBox.getSelectedIDs() + if clientID == 0 { + return } + m.historyBox.filter.ClientID = &clientID + m.historyBox.filter.ProjectID = projectID m.historyBox.resetSelection() } @@ -365,12 +370,20 @@ func (m *AppModel) openContractorEditor() { } func (m *AppModel) openClientOrProjectEditor() { - client := m.projectsBox.clients[m.projectsBox.selectedClient] + visibleClients := m.projectsBox.visibleClients() + if m.projectsBox.selectedClient >= len(visibleClients) { + return + } + client := visibleClients[m.projectsBox.selectedClient] if m.projectsBox.selectedProject == nil { m.modalBox.activate(ModalTypeClient, client.ID, *m) m.modalBox.populateClientFields(client) } else { - project := m.projectsBox.projects[client.ID][*m.projectsBox.selectedProject] + visibleProjects := m.projectsBox.visibleProjects(client.ID) + if *m.projectsBox.selectedProject >= len(visibleProjects) { + return + } + project := visibleProjects[*m.projectsBox.selectedProject] m.modalBox.activate(ModalTypeProjectEdit, project.ID, *m) m.modalBox.populateProjectFields(project) } @@ -467,7 +480,13 @@ func (m AppModel) View() string { topBarHeight := 1 bottomBarHeight := 2 - contentHeight := m.height - topBarHeight - bottomBarHeight + + // Render the contractor info panel (full width, matching the main content row). + contractorPanel := RenderContractorPanel(m.contractor, contentWidth, m.selectedBox == ContractorBox) + contractorPanelHeight := lipgloss.Height(contractorPanel) + + // +1 for the newline separator between contractor panel and main content + contentHeight := m.height - topBarHeight - bottomBarHeight - contractorPanelHeight - 1 // Sidebar: fixed comfortable width, capped to avoid dominating sidebarWidth := 38 @@ -506,7 +525,7 @@ func (m AppModel) View() string { keyBindings := activeBindings(m.selectedBox, m.historyBox.viewLevel, m.modalBox) bottomBar := RenderBottomBar(m, keyBindings, m.err, contentWidth) - fullView := topBar + "\n" + mainContent + "\n" + bottomBar + fullView := topBar + "\n" + contractorPanel + "\n" + mainContent + "\n" + bottomBar // Apply modal overlay (uses contentWidth for centering within the content area) fullView = m.modalBox.RenderCenteredOver(fullView, m, contentWidth) diff --git a/internal/tui/keys.go b/internal/tui/keys.go index c0c5605..775cccc 100644 --- a/internal/tui/keys.go +++ b/internal/tui/keys.go @@ -10,6 +10,7 @@ type KeyBindingScope int const ( ScopeGlobal KeyBindingScope = iota + ScopeContractorBox ScopeTimerBox ScopeProjectsBox ScopeHistoryBoxSummaries @@ -57,11 +58,6 @@ var Bindings map[KeyBindingScope]map[string]KeyBinding = map[KeyBindingScope]map Description: func(am AppModel) string { return "Refresh" }, Result: func(am *AppModel) tea.Cmd { return am.refreshCmd }, }, - "c": KeyBinding{ - Key: "c", - Description: func(am AppModel) string { return "Edit Contractor" }, - Result: func(am *AppModel) tea.Cmd { return editContractor() }, - }, "q": KeyBinding{ Key: "q", Description: func(am AppModel) string { return "Quit" }, @@ -80,6 +76,13 @@ var Bindings map[KeyBindingScope]map[string]KeyBinding = map[KeyBindingScope]map Hide: true, }, }, + ScopeContractorBox: { + "e": KeyBinding{ + Key: "e", + Description: func(AppModel) string { return "Edit Contractor" }, + Result: func(am *AppModel) tea.Cmd { return editContractor() }, + }, + }, ScopeTimerBox: { "enter": KeyBinding{ Key: "Enter", @@ -384,6 +387,8 @@ func HandleKeyPress(msg tea.KeyMsg, data *AppModel) tea.Cmd { var local map[string]KeyBinding switch data.selectedBox { + case ContractorBox: + local = Bindings[ScopeContractorBox] case TimerBox: local = Bindings[ScopeTimerBox] case ProjectsBox: @@ -416,6 +421,8 @@ func activeBindings(box BoxType, level HistoryViewLevel, modal ModalBoxModel) [] var scope KeyBindingScope switch box { + case ContractorBox: + scope = ScopeContractorBox case TimerBox: scope = ScopeTimerBox case ProjectsBox: diff --git a/internal/tui/shared.go b/internal/tui/shared.go index 9025fb8..75126c9 100644 --- a/internal/tui/shared.go +++ b/internal/tui/shared.go @@ -157,7 +157,7 @@ func getMostRecentTimerInfo(ctx context.Context, q *queries.Queries) (TimerInfo, // RenderTopBar renders the top bar with view name and time stats func RenderTopBar(m AppModel, contentWidth int) string { - leftText := fmt.Sprintf("Punchcard / %s", m.selectedBox.String()) + leftText := fmt.Sprintf("Punchcard 👊 💳 / %s", m.selectedBox.String()) today := m.timeStats.TodayTotal week := m.timeStats.WeekTotal @@ -344,6 +344,50 @@ func getAppData( return } +// RenderContractorPanel renders the full-width contractor info panel. +func RenderContractorPanel(c ContractorInfo, contentWidth int, isSelected bool) string { + style := unselectedBoxStyle + if isSelected { + style = selectedBoxStyle + } + + // Build content: name, label, email on one line separated by dimmed dividers + dimStyle := lipgloss.NewStyle().Foreground(colorDimmed) + nameStyle := lipgloss.NewStyle().Bold(true).Foreground(colorFg) + + var parts []string + if c.name != "" { + parts = append(parts, nameStyle.Render(c.name)) + } + if c.label != "" { + parts = append(parts, dimStyle.Render(c.label)) + } + if c.email != "" { + parts = append(parts, dimStyle.Render(c.email)) + } + + var content string + if len(parts) == 0 { + content = dimStyle.Render("No contractor info set. Press 'e' to edit.") + } else { + sep := dimStyle.Render(" | ") + content = "" + for i, part := range parts { + if i > 0 { + content += sep + } + content += part + } + } + + // Use padding 0 vertically to keep it compact, override the box style padding + panelStyle := style. + Width(contentWidth). + Padding(0, 2) + + return panelStyle.Render(content) +} + func mostRecentMonday(from time.Time) time.Time { d := dateOnly(from.Local()) dayOffset := time.Duration(d.Weekday()-1) % 7 |
