feat: edit and delete worklog entries
This commit is contained in:
parent
272a7f870b
commit
eb6a861310
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@
|
|||
let wlModalDetails = ''
|
||||
let wlModalBillable = false
|
||||
let wlModalApprox = false
|
||||
let editingWorklogEntry = null
|
||||
let suggestions = []
|
||||
let suggestionCount = 0
|
||||
let showCreateNode = false
|
||||
|
|
@ -965,28 +966,67 @@
|
|||
}
|
||||
|
||||
// ===== Worklog =====
|
||||
function openWorklogModal() {
|
||||
wlModalSummary = ''
|
||||
wlModalMinutes = ''
|
||||
wlModalDate = ''
|
||||
wlModalDetails = ''
|
||||
wlModalBillable = false
|
||||
wlModalApprox = false
|
||||
function openWorklogModal(entry = null) {
|
||||
editingWorklogEntry = entry
|
||||
wlModalSummary = entry ? entry.summary : ''
|
||||
wlModalMinutes = entry ? String(entry.minutes || '') : ''
|
||||
wlModalDate = entry ? (entry.date || '') : ''
|
||||
wlModalDetails = entry ? (entry.details || '') : ''
|
||||
wlModalBillable = entry ? !!entry.billable : false
|
||||
wlModalApprox = entry ? !!entry.approximate : false
|
||||
showWorklogModal = true
|
||||
}
|
||||
|
||||
function closeWorklogModal() {
|
||||
showWorklogModal = false
|
||||
editingWorklogEntry = null
|
||||
}
|
||||
|
||||
async function refreshWorklogViews(nodeID = '') {
|
||||
const targetNodeID = nodeID || (selectedNode ? selectedNode.id : '')
|
||||
if (selectedNode && selectedNode.id === targetNodeID) {
|
||||
worklog = initWorklogEntries(await wailsCall('ListWorklog', selectedNode.id)) || worklog
|
||||
}
|
||||
if (selectedSection === 'journal') {
|
||||
await loadJournal()
|
||||
}
|
||||
}
|
||||
|
||||
async function submitWorklogModal() {
|
||||
const mins = parseInt(wlModalMinutes, 10)
|
||||
if (!wlModalSummary.trim() || isNaN(mins) || mins <= 0 || !selectedNode) return
|
||||
if (!wlModalSummary.trim() || isNaN(mins) || mins <= 0) return
|
||||
if (!editingWorklogEntry && !selectedNode) return
|
||||
try {
|
||||
if (editingWorklogEntry) {
|
||||
await wailsCall('UpdateWorklogEntry', editingWorklogEntry.id, wlModalSummary.trim(), wlModalDetails, wlModalDate, mins, wlModalApprox, wlModalBillable)
|
||||
await refreshWorklogViews(editingWorklogEntry.nodeId)
|
||||
} else {
|
||||
await wailsCall('CreateWorklogFull', selectedNode.id, wlModalSummary.trim(), wlModalDetails, wlModalDate, mins, wlModalApprox, wlModalBillable)
|
||||
} catch (e) { /* fallback */ }
|
||||
worklog = initWorklogEntries(await wailsCall('ListWorklog', selectedNode.id)) || worklog
|
||||
await refreshWorklogViews(selectedNode.id)
|
||||
}
|
||||
} catch (e) {
|
||||
error = String(e)
|
||||
return
|
||||
}
|
||||
showWorklogModal = false
|
||||
editingWorklogEntry = null
|
||||
}
|
||||
|
||||
function deleteWorklogEntry(entry) {
|
||||
openConfirm({
|
||||
title: t('worklog.deleteEntry'),
|
||||
message: t('worklog.deleteConfirm'),
|
||||
confirmText: t('common.delete'),
|
||||
danger: true,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await wailsCall('DeleteWorklogEntry', entry.id)
|
||||
await refreshWorklogViews(entry.nodeId)
|
||||
} catch (e) {
|
||||
error = String(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshAfterSuggestion() {
|
||||
|
|
@ -1803,7 +1843,7 @@
|
|||
{:else if activeTab === 'worklog'}
|
||||
<div class="worklog-tab">
|
||||
<div class="worklog-toolbar">
|
||||
<button class="btn btn-primary btn-sm" on:click={openWorklogModal}>+ {t('worklog.addEntry')}</button>
|
||||
<button class="btn btn-primary btn-sm" on:click={() => openWorklogModal()}>+ {t('worklog.addEntry')}</button>
|
||||
</div>
|
||||
{#if selectedNode && suggestions.filter(s => s.nodeId === selectedNode.id).length > 0}
|
||||
<div class="worklog-tab-suggestions">
|
||||
|
|
@ -1852,10 +1892,14 @@
|
|||
<span class="worklog-entry-mins">{e.minutes} {t('worklog.min')}</span>
|
||||
{#if e.billable}<span class="wl-tag-billable">{t('journal.billableYes')}</span>{/if}
|
||||
{#if e.approximate}<span class="wl-tag-approx">{t('journal.approxEstimated')}</span>{/if}
|
||||
<span class="worklog-entry-date">{formatDate(e.createdAt)}</span>
|
||||
<span class="worklog-entry-date">{e.date}</span>
|
||||
</div>
|
||||
{#if e._expanded}
|
||||
<div class="worklog-entry-detail">
|
||||
<div class="worklog-entry-actions">
|
||||
<button class="btn btn-sm" on:click|stopPropagation={() => openWorklogModal(e)}>{t('worklog.editEntry')}</button>
|
||||
<button class="btn btn-sm btn-danger" on:click|stopPropagation={() => deleteWorklogEntry(e)}>{t('worklog.deleteEntry')}</button>
|
||||
</div>
|
||||
{#if e.details}
|
||||
<div class="wl-detail-section">
|
||||
<span class="wl-detail-label">{t('worklog.details')}</span>
|
||||
|
|
@ -2088,6 +2132,10 @@
|
|||
<tr class="journal-row-detail">
|
||||
<td colspan="8">
|
||||
<div class="journal-detail-body">
|
||||
<div class="journal-detail-actions">
|
||||
<button class="btn btn-sm" on:click|stopPropagation={() => openWorklogModal(r)}>{t('worklog.editEntry')}</button>
|
||||
<button class="btn btn-sm btn-danger" on:click|stopPropagation={() => deleteWorklogEntry(r)}>{t('worklog.deleteEntry')}</button>
|
||||
</div>
|
||||
{#if r.details}
|
||||
<div class="journal-detail-section">
|
||||
<span class="journal-detail-label">{t('worklog.details')}</span>
|
||||
|
|
@ -2352,7 +2400,7 @@
|
|||
{#if showWorklogModal}
|
||||
<div class="modal-overlay" role="button" tabindex="0" on:click|self={closeWorklogModal} on:keydown={onKeyActivate(closeWorklogModal)}>
|
||||
<div class="modal modal-worklog">
|
||||
<h3>{t('worklog.addEntry')}</h3>
|
||||
<h3>{editingWorklogEntry ? t('worklog.editEntry') : t('worklog.addEntry')}</h3>
|
||||
<div class="form-group">
|
||||
<label><span class="label-text">{t('worklog.date')}</span>
|
||||
<input type="date" bind:value={wlModalDate} />
|
||||
|
|
@ -2647,6 +2695,7 @@
|
|||
.journal-summary-cell { max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.journal-row-detail td { padding: 0 12px 12px; background: #16162a; }
|
||||
.journal-detail-body { display: flex; flex-direction: column; gap: 12px; padding: 8px 0; }
|
||||
.journal-detail-actions { display: flex; gap: 8px; justify-content: flex-end; }
|
||||
.journal-detail-section { font-size: 13px; }
|
||||
.journal-detail-label { font-size: 11px; font-weight: 600; color: #a5b4fc; text-transform: uppercase; letter-spacing: 0.3px; display: block; margin-bottom: 4px; }
|
||||
.journal-detail-section p { margin: 0; color: #c0c0d0; }
|
||||
|
|
@ -2883,6 +2932,7 @@
|
|||
.worklog-entry-mins { color: #b0b0c8; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||
.worklog-entry-date { color: #b0b0c0; font-size: 12px; white-space: nowrap; }
|
||||
.worklog-entry-detail { margin-top: 8px; padding-top: 8px; border-top: 1px solid #2a2a3c; display: flex; flex-direction: column; gap: 8px; }
|
||||
.worklog-entry-actions { display: flex; gap: 8px; justify-content: flex-end; }
|
||||
.wl-detail-section { font-size: 13px; }
|
||||
.wl-detail-label { font-size: 11px; font-weight: 600; color: #a5b4fc; text-transform: uppercase; letter-spacing: 0.3px; display: block; margin-bottom: 2px; }
|
||||
.wl-detail-section p { margin: 0; color: #c0c0d0; }
|
||||
|
|
|
|||
|
|
@ -149,6 +149,9 @@ export default {
|
|||
'worklog.min': 'min',
|
||||
'worklog.log': 'Log',
|
||||
'worklog.addEntry': 'Add entry',
|
||||
'worklog.editEntry': 'Edit entry',
|
||||
'worklog.deleteEntry': 'Delete entry',
|
||||
'worklog.deleteConfirm': 'Delete this work entry? Related events stay in activity, but their link to this entry will be removed.',
|
||||
'worklog.date': 'Date',
|
||||
'worklog.empty': 'No work logged yet',
|
||||
'worklog.details': 'Details',
|
||||
|
|
|
|||
|
|
@ -158,6 +158,9 @@ export default {
|
|||
'worklog.min': 'мин',
|
||||
'worklog.log': 'Записать',
|
||||
'worklog.addEntry': 'Добавить запись',
|
||||
'worklog.editEntry': 'Редактировать запись',
|
||||
'worklog.deleteEntry': 'Удалить запись',
|
||||
'worklog.deleteConfirm': 'Удалить эту запись работы? Связанные события останутся в активности, но связь с записью будет удалена.',
|
||||
'worklog.date': 'Дата',
|
||||
'worklog.empty': 'Записей работы пока нет',
|
||||
'worklog.details': 'Детали',
|
||||
|
|
|
|||
|
|
@ -82,6 +82,18 @@ export function CreateWorklog(arg1, arg2, arg3) {
|
|||
return window['go']['main']['App']['CreateWorklog'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function CreateWorklogFull(arg1, arg2, arg3, arg4, arg5, arg6, arg7) {
|
||||
return window['go']['main']['App']['CreateWorklogFull'](arg1, arg2, arg3, arg4, arg5, arg6, arg7);
|
||||
}
|
||||
|
||||
export function UpdateWorklogEntry(arg1, arg2, arg3, arg4, arg5, arg6, arg7) {
|
||||
return window['go']['main']['App']['UpdateWorklogEntry'](arg1, arg2, arg3, arg4, arg5, arg6, arg7);
|
||||
}
|
||||
|
||||
export function DeleteWorklogEntry(arg1) {
|
||||
return window['go']['main']['App']['DeleteWorklogEntry'](arg1);
|
||||
}
|
||||
|
||||
export function Search(arg1) {
|
||||
return window['go']['main']['App']['Search'](arg1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,12 +124,30 @@ func buildEntry(nodeID, summary, details, date string, minutes int, approximate,
|
|||
|
||||
// Update modifies an existing entry.
|
||||
func (s *Service) Update(id, summary, details string, minutes int, approximate, billable bool) error {
|
||||
return s.UpdateWithDate(id, summary, details, "", minutes, approximate, billable)
|
||||
}
|
||||
|
||||
// UpdateWithDate modifies an existing entry, including its work date when provided.
|
||||
func (s *Service) UpdateWithDate(id, summary, details, date string, minutes int, approximate, billable bool) error {
|
||||
if summary == "" {
|
||||
return fmt.Errorf("summary required")
|
||||
}
|
||||
t := time.Now().UTC().Format(time.RFC3339)
|
||||
res, err := s.db.Exec(
|
||||
var res sql.Result
|
||||
var err error
|
||||
if date == "" {
|
||||
res, err = s.db.Exec(
|
||||
`UPDATE worklog_entries SET summary=?, details=?, minutes=?,
|
||||
approximate=?, billable=?, updated_at=? WHERE id=?`,
|
||||
summary, details, &minutes, boolInt(approximate), boolInt(billable), t, id,
|
||||
)
|
||||
} else {
|
||||
res, err = s.db.Exec(
|
||||
`UPDATE worklog_entries SET date=?, summary=?, details=?, minutes=?,
|
||||
approximate=?, billable=?, updated_at=? WHERE id=?`,
|
||||
date, summary, details, &minutes, boolInt(approximate), boolInt(billable), t, id,
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -179,7 +197,16 @@ func (s *Service) ListByNode(nodeID string) ([]Entry, error) {
|
|||
|
||||
// Delete removes an entry.
|
||||
func (s *Service) Delete(id string) error {
|
||||
res, err := s.db.Exec("DELETE FROM worklog_entries WHERE id=?", id)
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if _, err := tx.Exec("DELETE FROM worklog_entry_events WHERE entry_id=?", id); err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := tx.Exec("DELETE FROM worklog_entries WHERE id=?", id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -187,7 +214,7 @@ func (s *Service) Delete(id string) error {
|
|||
if n == 0 {
|
||||
return fmt.Errorf("entry not found")
|
||||
}
|
||||
return nil
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// HasTodayEntries checks if any worklog entries exist for today.
|
||||
|
|
|
|||
|
|
@ -76,12 +76,15 @@ func TestUpdate(t *testing.T) {
|
|||
svc := NewService(db)
|
||||
|
||||
e, _ := svc.Add("node-1", "Old text", "Old details", 60, false, false)
|
||||
err := svc.Update(e.ID, "New text", "New details", 90, true, true)
|
||||
err := svc.UpdateWithDate(e.ID, "New text", "New details", "2026-01-02", 90, true, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, _ := svc.Get(e.ID)
|
||||
if got.Date != "2026-01-02" {
|
||||
t.Errorf("date = %q", got.Date)
|
||||
}
|
||||
if got.Summary != "New text" {
|
||||
t.Errorf("summary = %q", got.Summary)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -169,6 +169,18 @@ async function runReadyScenario(cdp, url) {
|
|||
await clickText(cdp, '.modal-worklog .btn', 'Сохранить')
|
||||
await waitForGone(cdp, '.modal-worklog')
|
||||
await assertText(cdp, 'GUI smoke worklog', 'worklog: created entry appears')
|
||||
await clickText(cdp, '.worklog-entry', 'GUI smoke worklog')
|
||||
await clickText(cdp, '.worklog-entry-actions .btn', 'Редактировать запись')
|
||||
await waitForSelector(cdp, '.modal-worklog')
|
||||
await setInputValue(cdp, '.modal-worklog input[type="text"]', 'GUI smoke worklog edited')
|
||||
await setInputValue(cdp, '.modal-worklog input[type="number"]', '25')
|
||||
await clickText(cdp, '.modal-worklog .btn', 'Сохранить')
|
||||
await waitForGone(cdp, '.modal-worklog')
|
||||
await assertText(cdp, 'GUI smoke worklog edited', 'worklog: edited entry appears')
|
||||
await clickText(cdp, '.worklog-entry', 'GUI smoke worklog edited')
|
||||
await clickText(cdp, '.worklog-entry-actions .btn', 'Удалить запись')
|
||||
await clickText(cdp, '.overlay .btn', 'Удалить')
|
||||
await assertEval(cdp, `!document.body.innerText.includes('GUI smoke worklog edited')`, 'worklog: deleted entry disappears')
|
||||
|
||||
await clickText(cdp, '.tab', 'Активность')
|
||||
await assertText(cdp, 'Smoke activity', 'activity: per-node activity visible')
|
||||
|
|
@ -729,6 +741,23 @@ function wailsMockSource() {
|
|||
state.worklog[nodeId] = [...(state.worklog[nodeId] || []), entry];
|
||||
return clone(entry);
|
||||
},
|
||||
UpdateWorklogEntry: async (id, summary, details, date, minutes, approximate, billable) => {
|
||||
for (const nodeId of Object.keys(state.worklog)) {
|
||||
const idx = state.worklog[nodeId].findIndex((entry) => entry.id === id);
|
||||
if (idx >= 0) {
|
||||
const updated = { ...state.worklog[nodeId][idx], summary, details, date, minutes, approximate, billable, updatedAt: now };
|
||||
state.worklog[nodeId][idx] = updated;
|
||||
return clone(updated);
|
||||
}
|
||||
}
|
||||
throw new Error('worklog entry not found');
|
||||
},
|
||||
DeleteWorklogEntry: async (id) => {
|
||||
for (const nodeId of Object.keys(state.worklog)) {
|
||||
state.worklog[nodeId] = state.worklog[nodeId].filter((entry) => entry.id !== id);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
GetSuggestions: async () => [{
|
||||
nodeId: 'node-project',
|
||||
nodeTitle: 'Smoke Project',
|
||||
|
|
|
|||
Loading…
Reference in New Issue