From 7ba68d333bc20b5795ccfd3870546a05eee60470 Mon Sep 17 00:00:00 2001 From: T Date: Mon, 29 Sep 2025 15:04:44 -0600 Subject: Support for archiving clients and projects. --- internal/tui/modal.go | 106 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 98 insertions(+), 8 deletions(-) (limited to 'internal/tui/modal.go') diff --git a/internal/tui/modal.go b/internal/tui/modal.go index 248654b..b614d04 100644 --- a/internal/tui/modal.go +++ b/internal/tui/modal.go @@ -3,8 +3,10 @@ package tui import ( "context" "database/sql" + "errors" "fmt" "strconv" + "strings" "time" "git.tjp.lol/punchcard/internal/actions" @@ -26,6 +28,7 @@ const ( ModalTypeHistoryFilter ModalTypeGenerateReport ModalTypeContractor + ModalTypeArchivedWarning ) func (mt ModalType) newForm() Form { @@ -56,6 +59,17 @@ type ModalBoxModel struct { form Form editedID int64 + + // For archived warning modal - store punch-in parameters + archivedPunchInParams *ArchivedPunchInParams +} + +type ArchivedPunchInParams struct { + ClientID string + ProjectID string + Description string + Rate *float64 + EntityType string // "client" or "project" } func (m *ModalBoxModel) HandleKeyPress(msg tea.KeyMsg) tea.Cmd { @@ -94,6 +108,8 @@ func (m ModalBoxModel) Render() string { return m.RenderFormModal("⏰ Time Entry") case ModalTypeDeleteConfirmation: return m.RenderDeleteConfirmation() + case ModalTypeArchivedWarning: + return m.RenderArchivedWarning() case ModalTypeClient: return m.RenderFormModal("👤 Client") case ModalTypeProjectCreate, ModalTypeProjectEdit: @@ -128,6 +144,21 @@ func (m ModalBoxModel) RenderDeleteConfirmation() string { ) } +func (m ModalBoxModel) RenderArchivedWarning() string { + entityType := "client" + if m.archivedPunchInParams != nil { + entityType = m.archivedPunchInParams.EntityType + } + + return fmt.Sprintf( + "%s\n\nThis %s is archived.\n\nContinuing will unarchive it and start tracking time.\n\n%s Continue %s Cancel", + modalTitleStyle.Render("⚠️ Archived "+entityType), + entityType, + boldStyle.Render("[Enter]"), + boldStyle.Render("[Esc]"), + ) +} + func (m *ModalBoxModel) activateCreateProjectModal(am AppModel) { m.activate(ModalTypeProjectCreate, 0, am) if am.selectedBox == ProjectsBox && len(am.projectsBox.clients) > 0 { @@ -184,7 +215,7 @@ func (m *ModalBoxModel) SubmitForm(am AppModel) tea.Cmd { if err != nil { return reOpenModal() } - return tea.Sequence(am.refreshCmd, func() tea.Msg { return recheckBounds{} }) + return tea.Sequence(closeModal(), am.refreshCmd, func() tea.Msg { return recheckBounds{} }) case ModalTypeEntry: if err := m.form.Error(); err != nil { @@ -202,7 +233,7 @@ func (m *ModalBoxModel) SubmitForm(am AppModel) tea.Cmd { return reOpenModal() } - return am.refreshCmd + return tea.Sequence(closeModal(), am.refreshCmd) case ModalTypeClient: if err := m.form.Error(); err != nil { @@ -238,7 +269,7 @@ func (m *ModalBoxModel) SubmitForm(am AppModel) tea.Cmd { } } - return am.refreshCmd + return tea.Sequence(closeModal(), am.refreshCmd) case ModalTypeProjectCreate: if err := m.form.Error(); err != nil { @@ -261,7 +292,7 @@ func (m *ModalBoxModel) SubmitForm(am AppModel) tea.Cmd { return reOpenModal() } - return am.refreshCmd + return tea.Sequence(closeModal(), am.refreshCmd) case ModalTypeProjectEdit: if err := m.form.Error(); err != nil { @@ -284,7 +315,7 @@ func (m *ModalBoxModel) SubmitForm(am AppModel) tea.Cmd { return reOpenModal() } - return am.refreshCmd + return tea.Sequence(closeModal(), am.refreshCmd) case ModalTypeHistoryFilter: if err := m.form.Error(); err != nil { @@ -335,14 +366,55 @@ func (m *ModalBoxModel) SubmitForm(am AppModel) tea.Cmd { } // Return filter update message - return func() tea.Msg { return updateHistoryFilter(newFilter) } + return tea.Sequence(closeModal(), func() tea.Msg { return updateHistoryFilter(newFilter) }) case ModalTypeGenerateReport: if err := m.form.Error(); err != nil { return reOpenModal() } - return generateReport(m, am) + // Validate report type + var genFunc func(context.Context, *queries.Queries, reports.ReportParams) (*reports.ReportResult, error) + switch strings.ToLower(m.form.fields[0].Value()) { + case "invoice": + genFunc = reports.GenerateInvoice + case "timesheet": + genFunc = reports.GenerateTimesheet + case "unified": + genFunc = reports.GenerateUnifiedReport + default: + m.form.fields[0].Err = errors.New("pick one of invoice, timesheet, or unified") + return reOpenModal() + } + + // Parse date range + dateRange, err := reports.ParseDateRange(m.form.fields[1].Value()) + if err != nil { + m.form.fields[1].Err = fmt.Errorf("invalid date range: %v", err) + return reOpenModal() + } + + // Parse timezone + var tz *time.Location + tzstr := m.form.fields[5].Value() + if tzstr == "" { + tz = time.Local + } else { + zone, err := time.LoadLocation(tzstr) + if err != nil { + m.form.fields[5].Err = err + return reOpenModal() + } + tz = zone + } + + return generateReport(am, genFunc, reports.ReportParams{ + ClientName: m.form.fields[2].Value(), + ProjectName: m.form.fields[3].Value(), + DateRange: dateRange, + OutputPath: m.form.fields[4].Value(), + Timezone: tz, + }) case ModalTypeContractor: if err := m.form.Error(); err != nil { @@ -358,7 +430,25 @@ func (m *ModalBoxModel) SubmitForm(am AppModel) tea.Cmd { return reOpenModal() } - return am.refreshCmd + return tea.Sequence(closeModal(), am.refreshCmd) + + case ModalTypeArchivedWarning: + // User confirmed unarchiving - punch in with autoUnarchive=true + if m.archivedPunchInParams == nil { + return nil + } + + a := actions.New(am.queries) + _, _ = a.PunchIn( + context.Background(), + m.archivedPunchInParams.ClientID, + m.archivedPunchInParams.ProjectID, + m.archivedPunchInParams.Description, + m.archivedPunchInParams.Rate, + true, // autoUnarchive + ) + + return tea.Sequence(closeModal(), am.refreshCmd) } return nil -- cgit v1.2.3