Add date and ticket filters plus entry list to the project time report.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+106
-6
@@ -23,6 +23,9 @@
|
||||
let msg = $state('')
|
||||
let report = $state(null)
|
||||
let reportGroupBy = $state('ticket')
|
||||
let reportFrom = $state('')
|
||||
let reportTo = $state('')
|
||||
let reportTicketId = $state('')
|
||||
let dash = $state({ projects: [], openTickets: [] })
|
||||
let tokens = $state([])
|
||||
let newTokenSecret = $state('')
|
||||
@@ -180,7 +183,7 @@
|
||||
}
|
||||
|
||||
async function saveTimeEntry() {
|
||||
if (!selected || !editingTimeId) return
|
||||
if (!projectId || !editingTimeId) return
|
||||
if (!editTimeStart || !editTimeEnd) {
|
||||
error = 'Start and end are required'
|
||||
return
|
||||
@@ -197,7 +200,8 @@
|
||||
end: end.toISOString(),
|
||||
})
|
||||
cancelEditTime()
|
||||
await refreshTicketTime()
|
||||
if (projectView === 'report') await loadReport()
|
||||
else await refreshTicketTime()
|
||||
msg = 'Time updated'
|
||||
}
|
||||
|
||||
@@ -205,10 +209,25 @@
|
||||
if (!confirm('Delete time entry?')) return
|
||||
await api.deleteTime(projectId, eid)
|
||||
if (editingTimeId === eid) cancelEditTime()
|
||||
await refreshTicketTime()
|
||||
if (projectView === 'report') await loadReport()
|
||||
else await refreshTicketTime()
|
||||
msg = 'Time deleted'
|
||||
}
|
||||
|
||||
function ticketTitle(tid) {
|
||||
const t = tickets.find((x) => x.id === tid)
|
||||
return t?.title || tid || '—'
|
||||
}
|
||||
|
||||
function reportQuery() {
|
||||
return {
|
||||
groupBy: reportGroupBy,
|
||||
from: reportFrom,
|
||||
to: reportTo,
|
||||
ticketId: reportTicketId,
|
||||
}
|
||||
}
|
||||
|
||||
async function doLogin() {
|
||||
error = ''
|
||||
try {
|
||||
@@ -414,11 +433,15 @@
|
||||
await refreshTicketTime()
|
||||
}
|
||||
|
||||
async function loadReport() {
|
||||
report = await api.report(projectId, reportQuery())
|
||||
}
|
||||
|
||||
async function showReport() {
|
||||
topTab = 'projects'
|
||||
projectView = 'report'
|
||||
error = ''
|
||||
report = await api.report(projectId, reportGroupBy)
|
||||
await loadReport()
|
||||
}
|
||||
|
||||
async function openReport() {
|
||||
@@ -428,7 +451,27 @@
|
||||
|
||||
async function changeReportGroup(g) {
|
||||
reportGroupBy = g
|
||||
report = await api.report(projectId, reportGroupBy)
|
||||
await loadReport()
|
||||
}
|
||||
|
||||
async function applyReportFilters() {
|
||||
error = ''
|
||||
try {
|
||||
await loadReport()
|
||||
} catch (e) {
|
||||
error = e.message || String(e)
|
||||
}
|
||||
}
|
||||
|
||||
function clearReportFilters() {
|
||||
reportFrom = ''
|
||||
reportTo = ''
|
||||
reportTicketId = ''
|
||||
}
|
||||
|
||||
async function resetReportFilters() {
|
||||
clearReportFilters()
|
||||
await applyReportFilters()
|
||||
}
|
||||
|
||||
async function showProjectSettings() {
|
||||
@@ -826,7 +869,7 @@
|
||||
</section>
|
||||
{:else if topTab === 'projects' && projectView === 'report'}
|
||||
<section class="panel stack">
|
||||
<div class="row" style="justify-content: space-between;">
|
||||
<div class="row" style="justify-content: space-between; flex-wrap: wrap; gap: 0.75rem;">
|
||||
<h2>Report — {projectName}</h2>
|
||||
<div class="row">
|
||||
{#each [['ticket', 'Ticket'], ['status', 'Status'], ['day', 'Day']] as [g, label]}
|
||||
@@ -834,8 +877,27 @@
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="flex-wrap: wrap; gap: 0.75rem; align-items: flex-end;">
|
||||
<label style="width: auto;">From
|
||||
<input type="date" bind:value={reportFrom} style="width: auto;" />
|
||||
</label>
|
||||
<label style="width: auto;">To
|
||||
<input type="date" bind:value={reportTo} style="width: auto;" />
|
||||
</label>
|
||||
<label style="width: auto; flex: 1; min-width: 12rem;">Ticket
|
||||
<select bind:value={reportTicketId}>
|
||||
<option value="">All tickets</option>
|
||||
{#each tickets as t}
|
||||
<option value={t.id}>{t.title}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<button class="primary" onclick={applyReportFilters}>Filter</button>
|
||||
<button onclick={resetReportFilters}>Reset</button>
|
||||
</div>
|
||||
{#if report}
|
||||
<p class="muted">{report.entryCount} entries · total {formatDuration(report.totalDurationNs)}</p>
|
||||
<h3>Summary</h3>
|
||||
<table>
|
||||
<thead><tr><th>Group</th><th>Duration</th></tr></thead>
|
||||
<tbody>
|
||||
@@ -846,6 +908,44 @@
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Entries</h3>
|
||||
<table>
|
||||
<thead><tr><th>When</th><th>Ticket</th><th>Duration</th><th>Source</th><th>Note</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{#each report.entries || [] as e}
|
||||
{#if editingTimeId === e.id}
|
||||
<tr>
|
||||
<td colspan="6">
|
||||
<div class="row" style="flex-wrap: wrap; gap: 0.5rem;">
|
||||
<label class="row" style="width: auto;">Start
|
||||
<input type="datetime-local" bind:value={editTimeStart} style="width: auto;" />
|
||||
</label>
|
||||
<label class="row" style="width: auto;">End
|
||||
<input type="datetime-local" bind:value={editTimeEnd} style="width: auto;" />
|
||||
</label>
|
||||
<button class="primary" onclick={saveTimeEntry}>Save</button>
|
||||
<button onclick={cancelEditTime}>Cancel</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td class="muted">{e.start ? new Date(e.start).toLocaleString() : '—'}</td>
|
||||
<td>{ticketTitle(e.ticketId)}</td>
|
||||
<td>{formatDuration(e.duration)}</td>
|
||||
<td>{e.source}</td>
|
||||
<td class="muted">{e.note || ''}</td>
|
||||
<td class="row" style="width: auto; justify-content: flex-end;">
|
||||
<button onclick={() => beginEditTime(e)}>Edit</button>
|
||||
<button class="danger" onclick={() => removeTimeEntry(e.id)}>Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{:else}
|
||||
<tr><td colspan="6" class="muted">No entries for this filter.</td></tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</section>
|
||||
{:else if topTab === 'projects' && projectView === 'settings'}
|
||||
|
||||
+8
-2
@@ -71,8 +71,14 @@ export const api = {
|
||||
body: '{}',
|
||||
}),
|
||||
|
||||
report: (projectId, groupBy = 'ticket') =>
|
||||
request(`/projects/${projectId}/report?groupBy=${encodeURIComponent(groupBy)}`),
|
||||
report: (projectId, { groupBy = 'ticket', from = '', to = '', ticketId = '' } = {}) => {
|
||||
const q = new URLSearchParams()
|
||||
q.set('groupBy', groupBy)
|
||||
if (from) q.set('from', from)
|
||||
if (to) q.set('to', to)
|
||||
if (ticketId) q.set('ticketId', ticketId)
|
||||
return request(`/projects/${projectId}/report?${q}`)
|
||||
},
|
||||
listTime: (projectId) => request(`/projects/${projectId}/time`),
|
||||
createTime: (projectId, body) =>
|
||||
request(`/projects/${projectId}/time`, { method: 'POST', body: JSON.stringify(body) }),
|
||||
|
||||
Reference in New Issue
Block a user