fix(step16.1): review fixes — acceptance, filters, sorting, export
- Remove dead acceptSuggestion, unify into refreshAfterSuggestion() - Journal: nodeID picker, includeChildren only with selected node - Journal: billable/approximate filters (all/yes/no selects) - Summary: ByDay sorted by date desc, ByNode by minutes desc - CSV: proper encoding/csv writer (was manual fmt.Sprintf) - Markdown: escape pipes and newlines via escMD() - After suggestion: refresh suggestions + count + worklog + journal - Add GetNodeTitle binding - i18n: common.all/no/date/search
This commit is contained in:
parent
c25e75f839
commit
5732264fc5
|
|
@ -81,6 +81,14 @@ func (a *App) GetNodeDetail(nodeID string) (*NodeDTO, error) {
|
||||||
return &dto, nil
|
return &dto, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) GetNodeTitle(nodeID string) (string, error) {
|
||||||
|
n, err := a.nodes.GetActive(nodeID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return n.Title, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) CreateNodeFromTemplate(parentID, title, templateID string) (*NodeDTO, error) {
|
func (a *App) CreateNodeFromTemplate(parentID, title, templateID string) (*NodeDTO, error) {
|
||||||
tmpl, ok := a.templates.Get(templateID)
|
tmpl, ok := a.templates.Get(templateID)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
||||||
|
|
@ -36,13 +36,8 @@ func (a *App) CreateWorklogFull(nodeID, summary, details, date string, minutes i
|
||||||
|
|
||||||
// --- report bindings ---
|
// --- report bindings ---
|
||||||
|
|
||||||
func (a *App) ListWorklogReport(dateFrom, dateTo, nodeID string, includeChildren bool) ([]worklog.ReportRow, error) {
|
func (a *App) ListWorklogReport(dateFrom, dateTo, nodeID string, includeChildren bool, billableFilter, approxFilter string) ([]worklog.ReportRow, error) {
|
||||||
f := worklog.ReportFilter{
|
f := buildWorklogFilter(dateFrom, dateTo, nodeID, includeChildren, billableFilter, approxFilter)
|
||||||
DateFrom: dateFrom,
|
|
||||||
DateTo: dateTo,
|
|
||||||
NodeID: nodeID,
|
|
||||||
IncludeChildren: includeChildren,
|
|
||||||
}
|
|
||||||
rows, err := a.worklog.ListReport(f)
|
rows, err := a.worklog.ListReport(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -51,34 +46,43 @@ func (a *App) ListWorklogReport(dateFrom, dateTo, nodeID string, includeChildren
|
||||||
return rows, nil
|
return rows, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) WorklogReportSummary(dateFrom, dateTo, nodeID string, includeChildren bool) (*worklog.ReportSummary, error) {
|
func (a *App) WorklogReportSummary(dateFrom, dateTo, nodeID string, includeChildren bool, billableFilter, approxFilter string) (*worklog.ReportSummary, error) {
|
||||||
f := worklog.ReportFilter{
|
f := buildWorklogFilter(dateFrom, dateTo, nodeID, includeChildren, billableFilter, approxFilter)
|
||||||
DateFrom: dateFrom,
|
|
||||||
DateTo: dateTo,
|
|
||||||
NodeID: nodeID,
|
|
||||||
IncludeChildren: includeChildren,
|
|
||||||
}
|
|
||||||
return a.worklog.Summary(f)
|
return a.worklog.Summary(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) ExportWorklogCSV(dateFrom, dateTo, nodeID string, includeChildren bool) (string, error) {
|
func (a *App) ExportWorklogCSV(dateFrom, dateTo, nodeID string, includeChildren bool, billableFilter, approxFilter string) (string, error) {
|
||||||
f := worklog.ReportFilter{
|
f := buildWorklogFilter(dateFrom, dateTo, nodeID, includeChildren, billableFilter, approxFilter)
|
||||||
DateFrom: dateFrom,
|
|
||||||
DateTo: dateTo,
|
|
||||||
NodeID: nodeID,
|
|
||||||
IncludeChildren: includeChildren,
|
|
||||||
}
|
|
||||||
return a.worklog.ExportCSV(f)
|
return a.worklog.ExportCSV(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) ExportWorklogMarkdown(dateFrom, dateTo, nodeID string, includeChildren bool) (string, error) {
|
func (a *App) ExportWorklogMarkdown(dateFrom, dateTo, nodeID string, includeChildren bool, billableFilter, approxFilter string) (string, error) {
|
||||||
f := worklog.ReportFilter{
|
f := buildWorklogFilter(dateFrom, dateTo, nodeID, includeChildren, billableFilter, approxFilter)
|
||||||
|
return a.worklog.ExportMarkdown(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolPtr(s string) *bool {
|
||||||
|
switch s {
|
||||||
|
case "yes":
|
||||||
|
v := true
|
||||||
|
return &v
|
||||||
|
case "no":
|
||||||
|
v := false
|
||||||
|
return &v
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildWorklogFilter(dateFrom, dateTo, nodeID string, includeChildren bool, billableFilter, approxFilter string) worklog.ReportFilter {
|
||||||
|
return worklog.ReportFilter{
|
||||||
DateFrom: dateFrom,
|
DateFrom: dateFrom,
|
||||||
DateTo: dateTo,
|
DateTo: dateTo,
|
||||||
NodeID: nodeID,
|
NodeID: nodeID,
|
||||||
IncludeChildren: includeChildren,
|
IncludeChildren: includeChildren,
|
||||||
|
Billable: boolPtr(billableFilter),
|
||||||
|
Approximate: boolPtr(approxFilter),
|
||||||
}
|
}
|
||||||
return a.worklog.ExportMarkdown(f)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- helpers ---
|
// --- helpers ---
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -16,7 +16,7 @@
|
||||||
background: #13131f;
|
background: #13131f;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/main-wlKdkTmp.js"></script>
|
<script type="module" crossorigin src="/assets/main-DU1CFPIY.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-B4G76NhT.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-B4G76NhT.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,10 @@
|
||||||
let journalDateFrom = ''
|
let journalDateFrom = ''
|
||||||
let journalDateTo = ''
|
let journalDateTo = ''
|
||||||
let journalIncludeChildren = false
|
let journalIncludeChildren = false
|
||||||
|
let journalNodeID = ''
|
||||||
|
let journalBillableFilter = 'all'
|
||||||
|
let journalApproxFilter = 'all'
|
||||||
|
let journalFilteredNodeTitle = ''
|
||||||
let activityLoading = false
|
let activityLoading = false
|
||||||
let caseActivity = []
|
let caseActivity = []
|
||||||
let version = ''
|
let version = ''
|
||||||
|
|
@ -928,51 +932,49 @@
|
||||||
worklogMinutes = ''
|
worklogMinutes = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshAfterSuggestion() {
|
||||||
|
suggestions = await wailsCall('GetSuggestions') || []
|
||||||
|
suggestionCount = suggestions.length
|
||||||
|
if (selectedNode) {
|
||||||
|
worklog = await wailsCall('ListWorklog', selectedNode.id) || []
|
||||||
|
}
|
||||||
|
if (selectedSection === 'journal') {
|
||||||
|
await loadJournal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function acceptTodaySuggestion(s) {
|
async function acceptTodaySuggestion(s) {
|
||||||
try {
|
try {
|
||||||
await wailsCall('AcceptSuggestionWith', s, s.suggestedMin, '')
|
await wailsCall('AcceptSuggestionWith', s, s.suggestedMin, '')
|
||||||
suggestions = await wailsCall('GetSuggestions') || []
|
await refreshAfterSuggestion()
|
||||||
suggestionCount = suggestions.length
|
|
||||||
if (selectedNode) {
|
|
||||||
worklog = await wailsCall('ListWorklog', selectedNode.id) || []
|
|
||||||
}
|
|
||||||
} catch (e) { console.error(e) }
|
} catch (e) { console.error(e) }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function acceptJournalSuggestion(s) {
|
async function acceptJournalSuggestion(s) {
|
||||||
try {
|
try {
|
||||||
await wailsCall('AcceptSuggestionWith', s, s.suggestedMin, '')
|
await wailsCall('AcceptSuggestionWith', s, s.suggestedMin, '')
|
||||||
suggestions = await wailsCall('GetSuggestions') || []
|
await refreshAfterSuggestion()
|
||||||
suggestionCount = suggestions.length
|
|
||||||
await loadJournal()
|
|
||||||
} catch (e) { console.error(e) }
|
} catch (e) { console.error(e) }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function acceptSuggestion(s) {
|
|
||||||
try {
|
|
||||||
await wailsCall('AcceptSuggestionWith', s, s.suggestedMin, '')
|
|
||||||
suggestions = await wailsCall('GetSuggestions') || []
|
|
||||||
suggestionCount = suggestions.length
|
|
||||||
if (selectedNode) {
|
|
||||||
worklog = await wailsCall('ListWorklog', selectedNode.id) || []
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('accept suggestion', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Journal =====
|
// ===== Journal =====
|
||||||
async function loadJournal() {
|
async function loadJournal() {
|
||||||
try {
|
try {
|
||||||
const [rows, summary, sugs] = await Promise.all([
|
const [rows, summary, sugs] = await Promise.all([
|
||||||
wailsCall('ListWorklogReport', journalDateFrom, journalDateTo, '', journalIncludeChildren),
|
wailsCall('ListWorklogReport', journalDateFrom, journalDateTo, journalNodeID, journalIncludeChildren, journalBillableFilter, journalApproxFilter),
|
||||||
wailsCall('WorklogReportSummary', journalDateFrom, journalDateTo, '', journalIncludeChildren),
|
wailsCall('WorklogReportSummary', journalDateFrom, journalDateTo, journalNodeID, journalIncludeChildren, journalBillableFilter, journalApproxFilter),
|
||||||
wailsCall('GetSuggestions'),
|
wailsCall('GetSuggestions'),
|
||||||
])
|
])
|
||||||
journalRows = rows || []
|
journalRows = rows || []
|
||||||
journalSummary = summary || null
|
journalSummary = summary || null
|
||||||
suggestions = sugs || []
|
suggestions = sugs || []
|
||||||
suggestionCount = suggestions.length
|
suggestionCount = suggestions.length
|
||||||
|
// Resolve node title if filtered by node.
|
||||||
|
if (journalNodeID && rows && rows.length > 0) {
|
||||||
|
journalFilteredNodeTitle = rows[0].nodeTitle
|
||||||
|
} else if (journalNodeID) {
|
||||||
|
try { const n = await wailsCall('GetNodeTitle', journalNodeID); journalFilteredNodeTitle = n } catch(e) { journalFilteredNodeTitle = '' }
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
journalRows = []
|
journalRows = []
|
||||||
journalSummary = null
|
journalSummary = null
|
||||||
|
|
@ -983,18 +985,35 @@
|
||||||
|
|
||||||
async function exportJournalCSV() {
|
async function exportJournalCSV() {
|
||||||
try {
|
try {
|
||||||
const csv = await wailsCall('ExportWorklogCSV', journalDateFrom, journalDateTo, '', journalIncludeChildren)
|
const csv = await wailsCall('ExportWorklogCSV', journalDateFrom, journalDateTo, journalNodeID, journalIncludeChildren, journalBillableFilter, journalApproxFilter)
|
||||||
downloadFile('worklog.csv', csv, 'text/csv')
|
downloadFile('worklog.csv', csv, 'text/csv')
|
||||||
} catch (e) { console.error(e) }
|
} catch (e) { console.error(e) }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportJournalMarkdown() {
|
async function exportJournalMarkdown() {
|
||||||
try {
|
try {
|
||||||
const md = await wailsCall('ExportWorklogMarkdown', journalDateFrom, journalDateTo, '', journalIncludeChildren)
|
const md = await wailsCall('ExportWorklogMarkdown', journalDateFrom, journalDateTo, journalNodeID, journalIncludeChildren, journalBillableFilter, journalApproxFilter)
|
||||||
downloadFile('worklog.md', md, 'text/markdown')
|
downloadFile('worklog.md', md, 'text/markdown')
|
||||||
} catch (e) { console.error(e) }
|
} catch (e) { console.error(e) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function pickJournalNode() {
|
||||||
|
// Simple prompt — in future make a proper tree picker.
|
||||||
|
const id = prompt('Введите ID дела (nodeId):')
|
||||||
|
if (id && id.trim()) {
|
||||||
|
journalNodeID = id.trim()
|
||||||
|
journalIncludeChildren = true
|
||||||
|
await loadJournal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearJournalNode() {
|
||||||
|
journalNodeID = ''
|
||||||
|
journalIncludeChildren = false
|
||||||
|
journalFilteredNodeTitle = ''
|
||||||
|
loadJournal()
|
||||||
|
}
|
||||||
|
|
||||||
function downloadFile(name, content, mime) {
|
function downloadFile(name, content, mime) {
|
||||||
const blob = new Blob([content], { type: mime })
|
const blob = new Blob([content], { type: mime })
|
||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob)
|
||||||
|
|
@ -1667,10 +1686,33 @@
|
||||||
<label><span class="label-text">{t('journal.dateTo')}</span>
|
<label><span class="label-text">{t('journal.dateTo')}</span>
|
||||||
<input type="date" bind:value={journalDateTo} />
|
<input type="date" bind:value={journalDateTo} />
|
||||||
</label>
|
</label>
|
||||||
|
<label><span class="label-text">{t('journal.node')}</span>
|
||||||
|
<div class="journal-node-picker">
|
||||||
|
<input type="text" placeholder="nodeId" bind:value={journalNodeID} />
|
||||||
|
<button class="btn btn-sm" on:click={pickJournalNode}>{t('common.search')}</button>
|
||||||
|
{#if journalNodeID}
|
||||||
|
<button class="btn btn-sm" on:click={clearJournalNode}>✕</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
<input type="checkbox" bind:checked={journalIncludeChildren} />
|
<input type="checkbox" bind:checked={journalIncludeChildren} disabled={!journalNodeID} />
|
||||||
<span>{t('journal.includeChildren')}</span>
|
<span>{t('journal.includeChildren')}</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label><span class="label-text">{t('journal.billable')}</span>
|
||||||
|
<select bind:value={journalBillableFilter}>
|
||||||
|
<option value="all">{t('common.all')}</option>
|
||||||
|
<option value="yes">{t('journal.billable')}</option>
|
||||||
|
<option value="no">{t('common.no')}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label><span class="label-text">{t('journal.approximate')}</span>
|
||||||
|
<select bind:value={journalApproxFilter}>
|
||||||
|
<option value="all">{t('common.all')}</option>
|
||||||
|
<option value="yes">{t('journal.approximate')}</option>
|
||||||
|
<option value="no">{t('common.no')}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<button class="btn btn-sm" on:click={loadJournal}>{t('journal.filter')}</button>
|
<button class="btn btn-sm" on:click={loadJournal}>{t('journal.filter')}</button>
|
||||||
<button class="btn btn-sm" on:click={exportJournalCSV}>{t('journal.exportCSV')}</button>
|
<button class="btn btn-sm" on:click={exportJournalCSV}>{t('journal.exportCSV')}</button>
|
||||||
<button class="btn btn-sm" on:click={exportJournalMarkdown}>{t('journal.exportMarkdown')}</button>
|
<button class="btn btn-sm" on:click={exportJournalMarkdown}>{t('journal.exportMarkdown')}</button>
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,11 @@ export default {
|
||||||
'worklog.suggestions': 'Suggestions for today',
|
'worklog.suggestions': 'Suggestions for today',
|
||||||
'worklog.apply': 'Apply',
|
'worklog.apply': 'Apply',
|
||||||
|
|
||||||
|
'common.all': 'All',
|
||||||
|
'common.no': 'No',
|
||||||
|
'common.date': 'Date',
|
||||||
|
'common.search': 'Search',
|
||||||
|
|
||||||
'nav.journal': 'Journal',
|
'nav.journal': 'Journal',
|
||||||
|
|
||||||
'journal.title': 'Work Log',
|
'journal.title': 'Work Log',
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,10 @@ export default {
|
||||||
'common.duplicate': 'Дублировать',
|
'common.duplicate': 'Дублировать',
|
||||||
'common.run': 'Запустить',
|
'common.run': 'Запустить',
|
||||||
'common.test': 'Test',
|
'common.test': 'Test',
|
||||||
|
'common.all': 'Все',
|
||||||
|
'common.no': 'Нет',
|
||||||
|
'common.date': 'Дата',
|
||||||
|
'common.search': 'Найти',
|
||||||
'common.testAgain': 'Проверить',
|
'common.testAgain': 'Проверить',
|
||||||
'common.connect': 'Подключиться',
|
'common.connect': 'Подключиться',
|
||||||
'common.disconnect': 'Отключиться',
|
'common.disconnect': 'Отключиться',
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package worklog
|
package worklog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/csv"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -231,9 +233,18 @@ func (s *Service) Summary(f ReportFilter) (*ReportSummary, error) {
|
||||||
for day, min := range dayMap {
|
for day, min := range dayMap {
|
||||||
sm.ByDay = append(sm.ByDay, SummaryGroup{Label: day, Minutes: min, Count: dayCount[day]})
|
sm.ByDay = append(sm.ByDay, SummaryGroup{Label: day, Minutes: min, Count: dayCount[day]})
|
||||||
}
|
}
|
||||||
|
sort.Slice(sm.ByDay, func(i, j int) bool {
|
||||||
|
return sm.ByDay[i].Label > sm.ByDay[j].Label // descending date
|
||||||
|
})
|
||||||
for node, min := range nodeMap {
|
for node, min := range nodeMap {
|
||||||
sm.ByNode = append(sm.ByNode, SummaryGroup{Label: node, Minutes: min, Count: nodeCount[node]})
|
sm.ByNode = append(sm.ByNode, SummaryGroup{Label: node, Minutes: min, Count: nodeCount[node]})
|
||||||
}
|
}
|
||||||
|
sort.Slice(sm.ByNode, func(i, j int) bool {
|
||||||
|
if sm.ByNode[i].Minutes != sm.ByNode[j].Minutes {
|
||||||
|
return sm.ByNode[i].Minutes > sm.ByNode[j].Minutes
|
||||||
|
}
|
||||||
|
return sm.ByNode[i].Label < sm.ByNode[j].Label
|
||||||
|
})
|
||||||
|
|
||||||
return sm, nil
|
return sm, nil
|
||||||
}
|
}
|
||||||
|
|
@ -247,7 +258,8 @@ func (s *Service) ExportCSV(f ReportFilter) (string, error) {
|
||||||
s.BuildReportPaths(rows)
|
s.BuildReportPaths(rows)
|
||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString("Date,Node,Path,Summary,Minutes,Approximate,Billable,Created\n")
|
w := csv.NewWriter(&b)
|
||||||
|
w.Write([]string{"Date", "Node", "Path", "Summary", "Minutes", "Approximate", "Billable", "Created"})
|
||||||
for _, r := range rows {
|
for _, r := range rows {
|
||||||
approx := "0"
|
approx := "0"
|
||||||
if r.Approximate {
|
if r.Approximate {
|
||||||
|
|
@ -257,11 +269,18 @@ func (s *Service) ExportCSV(f ReportFilter) (string, error) {
|
||||||
if r.Billable {
|
if r.Billable {
|
||||||
bill = "1"
|
bill = "1"
|
||||||
}
|
}
|
||||||
summary := strings.ReplaceAll(r.Summary, "\"", "\"\"")
|
w.Write([]string{r.Date, r.NodeTitle, r.NodePath, r.Summary,
|
||||||
b.WriteString(fmt.Sprintf("%s,\"%s\",\"%s\",\"%s\",%d,%s,%s,%s\n",
|
fmt.Sprintf("%d", r.Minutes), approx, bill, r.CreatedAt})
|
||||||
r.Date, r.NodeTitle, r.NodePath, summary, r.Minutes, approx, bill, r.CreatedAt))
|
|
||||||
}
|
}
|
||||||
return b.String(), nil
|
w.Flush()
|
||||||
|
return b.String(), w.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func escMD(s string) string {
|
||||||
|
s = strings.ReplaceAll(s, "|", "\\|")
|
||||||
|
s = strings.ReplaceAll(s, "\n", " ")
|
||||||
|
s = strings.ReplaceAll(s, "\r", "")
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExportMarkdown returns a Markdown report.
|
// ExportMarkdown returns a Markdown report.
|
||||||
|
|
@ -295,7 +314,7 @@ func (s *Service) ExportMarkdown(f ReportFilter) (string, error) {
|
||||||
approx = " ~"
|
approx = " ~"
|
||||||
}
|
}
|
||||||
b.WriteString(fmt.Sprintf("| %s | %s | %s | %s | %d%s |\n",
|
b.WriteString(fmt.Sprintf("| %s | %s | %s | %s | %d%s |\n",
|
||||||
r.Date, r.NodeTitle, r.NodePath, r.Summary, r.Minutes, approx))
|
escMD(r.Date), escMD(r.NodeTitle), escMD(r.NodePath), escMD(r.Summary), r.Minutes, approx))
|
||||||
}
|
}
|
||||||
|
|
||||||
if sm != nil {
|
if sm != nil {
|
||||||
|
|
@ -307,7 +326,7 @@ func (s *Service) ExportMarkdown(f ReportFilter) (string, error) {
|
||||||
b.WriteString("| Date | Minutes | Entries |\n")
|
b.WriteString("| Date | Minutes | Entries |\n")
|
||||||
b.WriteString("|------|---------|--------|\n")
|
b.WriteString("|------|---------|--------|\n")
|
||||||
for _, d := range sm.ByDay {
|
for _, d := range sm.ByDay {
|
||||||
b.WriteString(fmt.Sprintf("| %s | %dh %dm | %d |\n", d.Label, d.Minutes/60, d.Minutes%60, d.Count))
|
b.WriteString(fmt.Sprintf("| %s | %dh %dm | %d |\n", escMD(d.Label), d.Minutes/60, d.Minutes%60, d.Count))
|
||||||
}
|
}
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue