feat: worklog source field, suggestion logic fix, modal form, activity navigation

- Add source column to worklog_entries (migration 014): manual/suggestion/unknown
- GetSuggestions now excludes only events linked in worklog_entry_events,
  not entire nodes — repeated activity same day now produces suggestions
- Manual entry form replaced with '+' button + modal dialog
- Source display shows correct origin (manual/suggestion/unknown/no-events)
- Include-children checkbox hidden when no node selected
- Activity events navigate to specific notes/files instead of just case
- Expandable row reactivity fixed (journalRows/worklog reassignment)
This commit is contained in:
2026-06-03 12:27:50 +08:00
parent 1472bb3e6f
commit fd99dd4f5c
18 changed files with 445 additions and 160 deletions
+1
View File
@@ -176,6 +176,7 @@ type WorklogDTO struct {
Details string `json:"details,omitempty"`
Approximate bool `json:"approximate"`
Billable bool `json:"billable"`
Source string `json:"source"`
CreatedAt string `json:"createdAt"`
}
+20 -6
View File
@@ -7,16 +7,31 @@ import (
"time"
"verstak/internal/core/activity"
"verstak/internal/core/worklog"
syncsvc "verstak/internal/core/sync"
)
// GetSuggestions analyzes today's activity and returns conservative suggestions.
// Only events not already linked in worklog_entry_events are considered.
func (a *App) GetSuggestions() ([]activity.Suggestion, error) {
events, err := a.activity.ListTodayEvents()
if err != nil || len(events) == 0 {
return nil, err
}
// Determine which event IDs are already accounted for in any worklog entry.
accounted := make(map[string]bool)
rows, err := a.db.Query(`SELECT DISTINCT event_id FROM worklog_entry_events`)
if err == nil {
defer rows.Close()
for rows.Next() {
var eid string
if rows.Scan(&eid) == nil {
accounted[eid] = true
}
}
}
type acc struct {
title string
kind string
@@ -24,6 +39,9 @@ func (a *App) GetSuggestions() ([]activity.Suggestion, error) {
}
grouped := make(map[string]*acc)
for _, e := range events {
if accounted[e.ID] {
continue
}
grp, ok := grouped[e.NodeID]
if !ok {
n, err := a.nodes.GetActive(e.NodeID)
@@ -41,11 +59,7 @@ func (a *App) GetSuggestions() ([]activity.Suggestion, error) {
var suggestions []activity.Suggestion
for nodeID, grp := range grouped {
if grp.title == "" {
continue
}
hasEntries, err := a.worklog.HasTodayEntries(nodeID)
if err != nil || hasEntries {
if grp.title == "" || len(grp.events) == 0 {
continue
}
@@ -108,7 +122,7 @@ func (a *App) AcceptSuggestionWith(s activity.Suggestion, minutes int, date stri
if d == "" {
d = time.Now().Format("2006-01-02")
}
entry, err := a.worklog.AddWithDate(s.NodeID, s.Summary, "", d, minutes, true, false)
entry, err := a.worklog.AddWithSource(s.NodeID, s.Summary, "", d, minutes, true, false, worklog.SourceSuggestion)
if err != nil {
return nil, err
}
+1
View File
@@ -207,6 +207,7 @@ func entryToDTO(e *worklog.Entry) *WorklogDTO {
Details: e.Details,
Approximate: e.Approximate,
Billable: e.Billable,
Source: e.Source,
CreatedAt: e.CreatedAt.Format("2006-01-02T15:04:05Z"),
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -16,8 +16,8 @@
background: #13131f;
}
</style>
<script type="module" crossorigin src="/assets/main-BbnSy6IG.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-Bh5tqssv.css">
<script type="module" crossorigin src="/assets/main-cq32hqy7.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-BafVhx43.css">
</head>
<body>
<div id="app"></div>