feat: edit and delete worklog entries
This commit is contained in:
@@ -6,8 +6,8 @@ import (
|
||||
|
||||
wailsruntime "github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
|
||||
"verstak/internal/core/worklog"
|
||||
syncsvc "verstak/internal/core/sync"
|
||||
"verstak/internal/core/worklog"
|
||||
)
|
||||
|
||||
func (a *App) ListWorklog(nodeID string) ([]WorklogDTO, error) {
|
||||
@@ -45,6 +45,32 @@ func (a *App) CreateWorklogFull(nodeID, summary, details, date string, minutes i
|
||||
return entryToDTO(entry), nil
|
||||
}
|
||||
|
||||
func (a *App) UpdateWorklogEntry(id, summary, details, date string, minutes int, approximate, billable bool) (*WorklogDTO, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := a.worklog.UpdateWithDate(id, summary, details, date, minutes, approximate, billable); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry, err := a.worklog.Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = a.sync.RecordOp(syncsvc.EntityWorklog, entry.ID, syncsvc.OpUpdate, worklogPayload(entry))
|
||||
return entryToDTO(entry), nil
|
||||
}
|
||||
|
||||
func (a *App) DeleteWorklogEntry(id string) error {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.worklog.Delete(id); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = a.sync.RecordOp(syncsvc.EntityWorklog, id, syncsvc.OpDelete, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- report bindings ---
|
||||
|
||||
func (a *App) ListWorklogReport(dateFrom, dateTo, nodeID string, includeChildren bool, billableFilter, approxFilter string) ([]worklog.ReportRow, error) {
|
||||
|
||||
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
@@ -16,8 +16,8 @@
|
||||
background: #13131f;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/main-HFhkxYxJ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-D1RMkKjM.css">
|
||||
<script type="module" crossorigin src="/assets/main-Cc1HprFt.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-DAyIHTpH.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"verstak/internal/core/activity"
|
||||
syncsvc "verstak/internal/core/sync"
|
||||
"verstak/internal/core/util"
|
||||
"verstak/internal/core/worklog"
|
||||
)
|
||||
@@ -282,3 +283,112 @@ func TestManualWorklogEntry(t *testing.T) {
|
||||
t.Errorf("dto.Source = %q, want %q", dto.Source, worklog.SourceManual)
|
||||
}
|
||||
}
|
||||
|
||||
func hasWorklogSyncOp(t *testing.T, app *App, entryID, opType string) bool {
|
||||
t.Helper()
|
||||
ops, err := app.sync.GetUnpushedOps()
|
||||
if err != nil {
|
||||
t.Fatalf("GetUnpushedOps: %v", err)
|
||||
}
|
||||
for _, op := range ops {
|
||||
if op.EntityType == syncsvc.EntityWorklog && op.EntityID == entryID && op.OpType == opType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestUpdateAndDeleteWorklogEntryBinding(t *testing.T) {
|
||||
app, _ := setupTestApp(t)
|
||||
|
||||
n, err := app.CreateNodeFromTemplate("", "Editable Worklog Node", "folder.default")
|
||||
if err != nil {
|
||||
t.Fatalf("create node: %v", err)
|
||||
}
|
||||
dto, err := app.CreateWorklogFull(n.ID, "Old summary", "Old details", "2026-01-01", 30, false, false)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateWorklogFull: %v", err)
|
||||
}
|
||||
eventID := insertTestEvent(t, app, n.ID, activity.TypeNoteUpdated, "note", "note-1", "Связанное событие")
|
||||
if _, err := app.db.Exec(`INSERT INTO worklog_entry_events(entry_id,event_id) VALUES(?,?)`, dto.ID, eventID); err != nil {
|
||||
t.Fatalf("insert worklog event link: %v", err)
|
||||
}
|
||||
|
||||
updated, err := app.UpdateWorklogEntry(dto.ID, "New summary", "New details", "2026-01-02", 45, true, true)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateWorklogEntry: %v", err)
|
||||
}
|
||||
if updated.Summary != "New summary" || updated.Details != "New details" || updated.Date != "2026-01-02" {
|
||||
t.Fatalf("updated DTO = %#v", updated)
|
||||
}
|
||||
if updated.Minutes != 45 || !updated.Approximate || !updated.Billable {
|
||||
t.Fatalf("updated flags/minutes = %#v", updated)
|
||||
}
|
||||
if !hasWorklogSyncOp(t, app, dto.ID, syncsvc.OpUpdate) {
|
||||
t.Fatal("missing worklog update sync op")
|
||||
}
|
||||
if n := countLinked(t, app, dto.ID); n != 1 {
|
||||
t.Fatalf("event links after update = %d, want 1", n)
|
||||
}
|
||||
|
||||
if err := app.DeleteWorklogEntry(dto.ID); err != nil {
|
||||
t.Fatalf("DeleteWorklogEntry: %v", err)
|
||||
}
|
||||
if _, err := app.worklog.Get(dto.ID); err == nil {
|
||||
t.Fatal("expected deleted worklog entry to be gone")
|
||||
}
|
||||
if n := countLinked(t, app, dto.ID); n != 0 {
|
||||
t.Fatalf("event links after delete = %d, want 0", n)
|
||||
}
|
||||
if !hasWorklogSyncOp(t, app, dto.ID, syncsvc.OpDelete) {
|
||||
t.Fatal("missing worklog delete sync op")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyRemoteWorklogUpdate(t *testing.T) {
|
||||
app, _ := setupTestApp(t)
|
||||
|
||||
n, err := app.CreateNodeFromTemplate("", "Remote Worklog Node", "folder.default")
|
||||
if err != nil {
|
||||
t.Fatalf("create node: %v", err)
|
||||
}
|
||||
dto, err := app.CreateWorklogFull(n.ID, "Before remote", "", "2026-01-03", 15, false, false)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateWorklogFull: %v", err)
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(map[string]interface{}{
|
||||
"id": dto.ID,
|
||||
"node_id": n.ID,
|
||||
"summary": "After remote",
|
||||
"details": "Remote details",
|
||||
"minutes": 75,
|
||||
"date": "2026-01-04",
|
||||
"approximate": true,
|
||||
"billable": true,
|
||||
"updated_at": nowISO(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal payload: %v", err)
|
||||
}
|
||||
|
||||
if err := app.applyRemoteWorklogOp(syncsvc.Op{
|
||||
EntityType: syncsvc.EntityWorklog,
|
||||
EntityID: dto.ID,
|
||||
OpType: syncsvc.OpUpdate,
|
||||
PayloadJSON: string(payload),
|
||||
}); err != nil {
|
||||
t.Fatalf("applyRemoteWorklogOp update: %v", err)
|
||||
}
|
||||
|
||||
got, err := app.worklog.Get(dto.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get updated entry: %v", err)
|
||||
}
|
||||
if got.Summary != "After remote" || got.Details != "Remote details" || got.Date != "2026-01-04" {
|
||||
t.Fatalf("remote updated entry = %#v", got)
|
||||
}
|
||||
if got.Minutes == nil || *got.Minutes != 75 || !got.Approximate || !got.Billable {
|
||||
t.Fatalf("remote updated minutes/flags = %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -885,7 +885,12 @@ func (a *App) applyRemoteWorklogOp(op syncsvc.Op) error {
|
||||
switch op.OpType {
|
||||
case syncsvc.OpCreate:
|
||||
return a.applyRemoteWorklogCreate(op)
|
||||
case syncsvc.OpUpdate:
|
||||
return a.applyRemoteWorklogUpdate(op)
|
||||
case syncsvc.OpDelete:
|
||||
if _, err := a.db.Exec(`DELETE FROM worklog_entry_events WHERE entry_id=?`, op.EntityID); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := a.db.Exec(`DELETE FROM worklog_entries WHERE id=?`, op.EntityID)
|
||||
return err
|
||||
}
|
||||
@@ -921,3 +926,36 @@ func (a *App) applyRemoteWorklogCreate(op syncsvc.Op) error {
|
||||
payload.Summary, payload.Details, payload.CreatedAt, payload.UpdatedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *App) applyRemoteWorklogUpdate(op syncsvc.Op) error {
|
||||
var payload struct {
|
||||
ID string `json:"id"`
|
||||
Summary string `json:"summary"`
|
||||
Details string `json:"details"`
|
||||
Minutes int `json:"minutes"`
|
||||
Date string `json:"date"`
|
||||
Approximate bool `json:"approximate"`
|
||||
Billable bool `json:"billable"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(op.PayloadJSON), &payload); err != nil {
|
||||
return fmt.Errorf("unmarshal worklog update: %w", err)
|
||||
}
|
||||
id := payload.ID
|
||||
if id == "" {
|
||||
id = op.EntityID
|
||||
}
|
||||
if id == "" {
|
||||
return nil
|
||||
}
|
||||
updatedAt := payload.UpdatedAt
|
||||
if updatedAt == "" {
|
||||
updatedAt = time.Now().UTC().Format(time.RFC3339)
|
||||
}
|
||||
_, err := a.db.Exec(
|
||||
`UPDATE worklog_entries SET date=?, minutes=?, approximate=?, billable=?,
|
||||
summary=?, details=?, updated_at=? WHERE id=?`,
|
||||
payload.Date, payload.Minutes, boolToInt(payload.Approximate), boolToInt(payload.Billable),
|
||||
payload.Summary, payload.Details, updatedAt, id)
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user