summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/tui/app.go47
-rw-r--r--internal/tui/keys.go17
-rw-r--r--internal/tui/shared.go46
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