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:
@@ -8,7 +8,6 @@ import (
|
||||
"time"
|
||||
|
||||
"verstak/internal/core/storage"
|
||||
"verstak/internal/core/util"
|
||||
)
|
||||
|
||||
// ReportFilter specifies which worklog entries to include.
|
||||
@@ -33,6 +32,7 @@ type ReportRow struct {
|
||||
Minutes int `json:"minutes"`
|
||||
Approximate bool `json:"approximate"`
|
||||
Billable bool `json:"billable"`
|
||||
Source string `json:"source"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
@@ -145,7 +145,7 @@ func (s *Service) buildReportQuery(f ReportFilter) (string, []interface{}) {
|
||||
|
||||
q := `SELECT w.id, w.node_id, COALESCE(n.title,''), w.date, w.summary,
|
||||
COALESCE(w.details,''), COALESCE(w.minutes,0), w.approximate, w.billable,
|
||||
w.created_at, w.updated_at
|
||||
COALESCE(w.source,'unknown'), w.created_at, w.updated_at
|
||||
FROM worklog_entries w
|
||||
LEFT JOIN nodes n ON n.id = w.node_id` +
|
||||
whereClause +
|
||||
@@ -170,7 +170,7 @@ func (s *Service) ListReport(f ReportFilter) ([]ReportRow, error) {
|
||||
var approxInt, billInt int
|
||||
err := rows.Scan(&r.ID, &r.NodeID, &r.NodeTitle, &r.Date,
|
||||
&r.Summary, &r.Details, &r.Minutes, &approxInt, &billInt,
|
||||
&createdStr, &updatedStr)
|
||||
&r.Source, &createdStr, &updatedStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -349,7 +349,7 @@ func (s *Service) ExportMarkdown(f ReportFilter) (string, error) {
|
||||
func (s *Service) GetByIDWithNode(id string) (*ReportRow, error) {
|
||||
q := `SELECT w.id, w.node_id, COALESCE(n.title,''), w.date, w.summary,
|
||||
COALESCE(w.details,''), COALESCE(w.minutes,0), w.approximate, w.billable,
|
||||
w.created_at, w.updated_at
|
||||
COALESCE(w.source,'unknown'), w.created_at, w.updated_at
|
||||
FROM worklog_entries w
|
||||
LEFT JOIN nodes n ON n.id = w.node_id
|
||||
WHERE w.id = ?`
|
||||
@@ -358,7 +358,7 @@ func (s *Service) GetByIDWithNode(id string) (*ReportRow, error) {
|
||||
var approxInt, billInt int
|
||||
err := s.db.QueryRow(q, id).Scan(&r.ID, &r.NodeID, &r.NodeTitle, &r.Date,
|
||||
&r.Summary, &r.Details, &r.Minutes, &approxInt, &billInt,
|
||||
&createdStr, &updatedStr)
|
||||
&r.Source, &createdStr, &updatedStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -371,41 +371,7 @@ func (s *Service) GetByIDWithNode(id string) (*ReportRow, error) {
|
||||
|
||||
// AddWithDate inserts a worklog entry with a specific date.
|
||||
func (s *Service) AddWithDate(nodeID, summary, details, date string, minutes int, approximate, billable bool) (*Entry, error) {
|
||||
if nodeID == "" {
|
||||
return nil, fmt.Errorf("node_id required")
|
||||
}
|
||||
if summary == "" {
|
||||
return nil, fmt.Errorf("summary required")
|
||||
}
|
||||
if date == "" {
|
||||
date = time.Now().Format("2006-01-02")
|
||||
}
|
||||
|
||||
e := &Entry{
|
||||
ID: util.UUID7(),
|
||||
NodeID: nodeID,
|
||||
Summary: summary,
|
||||
Details: details,
|
||||
Date: date,
|
||||
Minutes: &minutes,
|
||||
Approximate: approximate,
|
||||
Billable: billable,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
UpdatedAt: time.Now().UTC(),
|
||||
}
|
||||
|
||||
_, err := s.db.Exec(
|
||||
`INSERT INTO worklog_entries (id,node_id,date,minutes,approximate,billable,
|
||||
summary,details,created_at,updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?)`,
|
||||
e.ID, e.NodeID, e.Date, e.Minutes, boolInt(e.Approximate),
|
||||
boolInt(e.Billable), e.Summary, e.Details,
|
||||
e.CreatedAt.Format(time.RFC3339), e.UpdatedAt.Format(time.RFC3339),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e, nil
|
||||
return s.AddWithSource(nodeID, summary, details, date, minutes, approximate, billable, SourceManual)
|
||||
}
|
||||
|
||||
// UpdateDate updates the date of an entry.
|
||||
|
||||
@@ -22,10 +22,18 @@ type Entry struct {
|
||||
Billable bool `json:"billable"`
|
||||
Summary string `json:"summary"`
|
||||
Details string `json:"details,omitempty"`
|
||||
Source string `json:"source"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
const (
|
||||
SourceManual = "manual"
|
||||
SourceSuggestion = "suggestion"
|
||||
SourceImported = "imported"
|
||||
SourceUnknown = "unknown"
|
||||
)
|
||||
|
||||
// Service manages worklog entries.
|
||||
type Service struct {
|
||||
db *storage.DB
|
||||
@@ -36,34 +44,44 @@ func NewService(db *storage.DB) *Service {
|
||||
return &Service{db: db}
|
||||
}
|
||||
|
||||
// Add inserts a new worklog entry.
|
||||
// Add inserts a new worklog entry with manual source.
|
||||
func (s *Service) Add(nodeID, summary, details string, minutes int, approximate, billable bool) (*Entry, error) {
|
||||
date := time.Now().Format("2006-01-02")
|
||||
return s.AddWithSource(nodeID, summary, details, date, minutes, approximate, billable, SourceManual)
|
||||
}
|
||||
|
||||
// Add inserts a new worklog entry.
|
||||
func (s *Service) AddWithSource(nodeID, summary, details, date string, minutes int, approximate, billable bool, source string) (*Entry, error) {
|
||||
if nodeID == "" {
|
||||
return nil, fmt.Errorf("node_id required")
|
||||
}
|
||||
if summary == "" {
|
||||
return nil, fmt.Errorf("summary required")
|
||||
}
|
||||
if date == "" {
|
||||
date = time.Now().Format("2006-01-02")
|
||||
}
|
||||
|
||||
e := &Entry{
|
||||
ID: util.UUID7(),
|
||||
NodeID: nodeID,
|
||||
Summary: summary,
|
||||
Details: details,
|
||||
Date: time.Now().Format("2006-01-02"),
|
||||
Date: date,
|
||||
Minutes: &minutes,
|
||||
Approximate: approximate,
|
||||
Billable: billable,
|
||||
Source: source,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
UpdatedAt: time.Now().UTC(),
|
||||
}
|
||||
|
||||
_, err := s.db.Exec(
|
||||
`INSERT INTO worklog_entries (id,node_id,date,minutes,approximate,billable,
|
||||
summary,details,created_at,updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?)`,
|
||||
summary,details,source,created_at,updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
e.ID, e.NodeID, e.Date, e.Minutes, boolInt(e.Approximate),
|
||||
boolInt(e.Billable), e.Summary, e.Details,
|
||||
boolInt(e.Billable), e.Summary, e.Details, e.Source,
|
||||
e.CreatedAt.Format(time.RFC3339), e.UpdatedAt.Format(time.RFC3339),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -94,7 +112,7 @@ func (s *Service) Update(id, summary, details string, minutes int, approximate,
|
||||
func (s *Service) Get(id string) (*Entry, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT id,node_id,started_at,ended_at,date,minutes,approximate,
|
||||
billable,summary,details,created_at,updated_at
|
||||
billable,summary,details,COALESCE(source,'unknown'),created_at,updated_at
|
||||
FROM worklog_entries WHERE id=?`, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -110,7 +128,7 @@ func (s *Service) Get(id string) (*Entry, error) {
|
||||
func (s *Service) ListByNode(nodeID string) ([]Entry, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT id,node_id,started_at,ended_at,date,minutes,approximate,
|
||||
billable,summary,details,created_at,updated_at
|
||||
billable,summary,details,COALESCE(source,'unknown'),created_at,updated_at
|
||||
FROM worklog_entries WHERE node_id=? ORDER BY date DESC, created_at DESC`, nodeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -210,14 +228,14 @@ type rowScanner interface {
|
||||
|
||||
func scanEntry(s rowScanner) (*Entry, error) {
|
||||
var e Entry
|
||||
var startedAt, endedAt, details sql.NullString
|
||||
var startedAt, endedAt, details, source sql.NullString
|
||||
var minutes sql.NullInt64
|
||||
var createdStr, updatedStr string
|
||||
var approxInt, billInt int
|
||||
|
||||
err := s.Scan(
|
||||
&e.ID, &e.NodeID, &startedAt, &endedAt, &e.Date, &minutes,
|
||||
&approxInt, &billInt, &e.Summary, &details, &createdStr, &updatedStr,
|
||||
&approxInt, &billInt, &e.Summary, &details, &source, &createdStr, &updatedStr,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -239,6 +257,10 @@ func scanEntry(s rowScanner) (*Entry, error) {
|
||||
if details.Valid {
|
||||
e.Details = details.String
|
||||
}
|
||||
e.Source = SourceUnknown
|
||||
if source.Valid {
|
||||
e.Source = source.String
|
||||
}
|
||||
e.CreatedAt, _ = time.Parse(time.RFC3339, createdStr)
|
||||
e.UpdatedAt, _ = time.Parse(time.RFC3339, updatedStr)
|
||||
return &e, nil
|
||||
|
||||
Reference in New Issue
Block a user