feat: journal UX overhaul — picker, export dialog, events, readability
- Sidebar i18n: added missing nav.journal to backend ru.json - Export: SaveWorklogReport binding with native SaveFileDialog + os.WriteFile - Filter: better IncludeChildren label with disabled tooltip - Filter: renamed billable→К оплате, approximate→Тип времени with hints - worklog_entry_events table (migration 013) linking entries to activity events - Suggestion: EventIDs + Events details, expandable cards with timestamps - Journal rows: expandable with details, source, linked events - Contrast: improved readability for dates, timestamps, hover states - i18n: added worklog.*, journal.*, suggest.* keys to ru.js/en.js
This commit is contained in:
@@ -60,6 +60,21 @@ func (a *App) GetSuggestions() ([]activity.Suggestion, error) {
|
||||
min := estimateMinutes(bursts, spread, len(grp.events))
|
||||
conf, reason := confidence(bursts, spread, len(grp.events))
|
||||
|
||||
eventIDs := make([]string, 0, len(grp.events))
|
||||
evDetails := make([]activity.SuggestionDetail, 0, len(grp.events))
|
||||
for _, e := range grp.events {
|
||||
eventIDs = append(eventIDs, e.ID)
|
||||
evDetails = append(evDetails, activity.SuggestionDetail{
|
||||
ID: e.ID,
|
||||
EventType: e.EventType,
|
||||
TargetType: e.TargetType,
|
||||
TargetID: e.TargetID,
|
||||
Title: e.Title,
|
||||
CreatedAt: e.CreatedAt,
|
||||
NodeID: e.NodeID,
|
||||
})
|
||||
}
|
||||
|
||||
suggestions = append(suggestions, activity.Suggestion{
|
||||
NodeID: nodeID,
|
||||
NodeTitle: grp.title,
|
||||
@@ -70,6 +85,8 @@ func (a *App) GetSuggestions() ([]activity.Suggestion, error) {
|
||||
Confidence: conf,
|
||||
ConfidenceReason: reason,
|
||||
TimeSpreadMin: spread,
|
||||
EventIDs: eventIDs,
|
||||
Events: evDetails,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -95,6 +112,12 @@ func (a *App) AcceptSuggestionWith(s activity.Suggestion, minutes int, date stri
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Link activity events to this worklog entry.
|
||||
for _, eid := range s.EventIDs {
|
||||
_, _ = a.db.Exec(
|
||||
`INSERT OR IGNORE INTO worklog_entry_events (entry_id, event_id) VALUES (?,?)`,
|
||||
entry.ID, eid)
|
||||
}
|
||||
_ = a.sync.RecordOp(syncsvc.EntityWorklog, entry.ID, syncsvc.OpCreate, worklogPayload(entry))
|
||||
mins := 0
|
||||
if entry.Minutes != nil {
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
syncsvc "verstak/internal/core/sync"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
wailsruntime "github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
|
||||
"verstak/internal/core/worklog"
|
||||
syncsvc "verstak/internal/core/sync"
|
||||
)
|
||||
|
||||
func (a *App) ListWorklog(nodeID string) ([]WorklogDTO, error) {
|
||||
@@ -90,6 +95,94 @@ func buildWorklogFilter(dateFrom, dateTo, nodeID string, includeChildren bool, b
|
||||
}
|
||||
}
|
||||
|
||||
// SaveWorklogReport generates a worklog report and opens a SaveFileDialog.
|
||||
func (a *App) SaveWorklogReport(format, dateFrom, dateTo, nodeID string, includeChildren bool, billableFilter, approxFilter string) (string, error) {
|
||||
f := buildWorklogFilter(dateFrom, dateTo, nodeID, includeChildren, billableFilter, approxFilter)
|
||||
|
||||
var data []byte
|
||||
var ext string
|
||||
switch format {
|
||||
case "csv":
|
||||
s, err := a.worklog.ExportCSV(f)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data = []byte(s)
|
||||
ext = ".csv"
|
||||
case "markdown":
|
||||
s, err := a.worklog.ExportMarkdown(f)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data = []byte(s)
|
||||
ext = ".md"
|
||||
case "pdf":
|
||||
var err error
|
||||
data, err = a.worklog.ExportPDF(f)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ext = ".pdf"
|
||||
default:
|
||||
return "", fmt.Errorf("unknown format: %s", format)
|
||||
}
|
||||
|
||||
from := dateFrom
|
||||
if from == "" {
|
||||
from = "all"
|
||||
}
|
||||
to := dateTo
|
||||
if to == "" {
|
||||
to = "all"
|
||||
}
|
||||
defaultName := fmt.Sprintf("verstak-worklog-%s--%s%s", from, to, ext)
|
||||
|
||||
path, err := wailsruntime.SaveFileDialog(a.ctx, wailsruntime.SaveDialogOptions{
|
||||
DefaultFilename: defaultName,
|
||||
Filters: []wailsruntime.FileFilter{
|
||||
{DisplayName: format, Pattern: "*" + ext},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if path == "" {
|
||||
return "", fmt.Errorf("отменено пользователем")
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||
return "", fmt.Errorf("не удалось сохранить файл: %w", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Отчёт сохранён: %s", path), nil
|
||||
}
|
||||
|
||||
// GetWorklogEntryEvents returns activity events linked to a worklog entry.
|
||||
func (a *App) GetWorklogEntryEvents(entryID string) ([]EventDTO, error) {
|
||||
rows, err := a.db.Query(
|
||||
`SELECT e.id, e.node_id, e.event_type, e.target_type, e.target_id, e.target_path,
|
||||
e.title, e.details_json, e.created_at
|
||||
FROM activity_events e
|
||||
JOIN worklog_entry_events wle ON wle.event_id = e.id
|
||||
WHERE wle.entry_id = ?
|
||||
ORDER BY e.created_at ASC`, entryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []EventDTO
|
||||
for rows.Next() {
|
||||
var d EventDTO
|
||||
if err := rows.Scan(&d.ID, &d.NodeID, &d.EventType, &d.TargetType,
|
||||
&d.TargetID, &d.TargetPath, &d.Title, &d.DetailsJSON, &d.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, d)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
func toWorklogDTOs(list []worklog.Entry) []WorklogDTO {
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -16,8 +16,8 @@
|
||||
background: #13131f;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/main-BYxU8Qbt.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-C7UPiZtQ.css">
|
||||
<script type="module" crossorigin src="/assets/main-BbnSy6IG.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-Bh5tqssv.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
Reference in New Issue
Block a user