feat: bug statistics + village detail dashboard enhancement
- Tambah GET /api/bugs/stats dengan summary cards & chart trend/bugs per app - Tambah date range picker di village activity chart - Tambah tabel Recent Activity (action + description) di village detail - Update API graph-log-villages support dateFrom/dateTo custom range
This commit is contained in:
82
src/app.ts
82
src/app.ts
@@ -1100,6 +1100,88 @@ export function createApp() {
|
||||
},
|
||||
})
|
||||
|
||||
// ─── Bug Statistics API ────────────────────────────
|
||||
.get('/api/bugs/stats', async ({ query }) => {
|
||||
const range = [7, 30, 90].includes(Number(query.range)) ? Number(query.range) : 7
|
||||
const now = new Date()
|
||||
const rangeStart = new Date(now.getTime() - range * 24 * 60 * 60 * 1000)
|
||||
|
||||
const [totalBugs, openBugs, statusGroups, appGroups, sourceGroups, resolvedBugs, trendData] = await Promise.all([
|
||||
prisma.bug.count(),
|
||||
prisma.bug.count({ where: { status: 'OPEN' } }),
|
||||
prisma.bug.groupBy({ by: ['status'], _count: { id: true } }),
|
||||
prisma.bug.groupBy({ by: ['appId'], _count: { id: true } }),
|
||||
prisma.bug.groupBy({ by: ['source'], _count: { id: true } }),
|
||||
prisma.bug.findMany({
|
||||
where: { status: { in: ['RESOLVED', 'CLOSED'] } },
|
||||
select: { createdAt: true, updatedAt: true },
|
||||
}),
|
||||
prisma.bug.findMany({
|
||||
where: { createdAt: { gte: rangeStart } },
|
||||
select: { createdAt: true },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
}),
|
||||
])
|
||||
|
||||
const byStatus = Object.fromEntries(statusGroups.map((g) => [g.status, g._count.id]))
|
||||
const byApp = appGroups.map((g) => ({ appId: g.appId, count: g._count.id }))
|
||||
const bySource = Object.fromEntries(sourceGroups.map((g) => [g.source, g._count.id]))
|
||||
|
||||
const totalResolutionMs = resolvedBugs.reduce((sum, b) => sum + (b.updatedAt.getTime() - b.createdAt.getTime()), 0)
|
||||
const avgResolutionHours = resolvedBugs.length > 0
|
||||
? Math.round(totalResolutionMs / resolvedBugs.length / (1000 * 60 * 60) * 10) / 10
|
||||
: 0
|
||||
|
||||
const resolvedCount = (byStatus['RESOLVED'] || 0) + (byStatus['CLOSED'] || 0)
|
||||
const resolutionRate = totalBugs > 0 ? Math.round((resolvedCount / totalBugs) * 100) : 0
|
||||
|
||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||||
const trendMap: Record<string, number> = {}
|
||||
const keyToLabel: Record<string, string> = {}
|
||||
|
||||
for (let i = 0; i < range; i++) {
|
||||
const d = new Date(now)
|
||||
d.setDate(d.getDate() - i)
|
||||
const key = d.toISOString().slice(0, 10)
|
||||
const label = `${d.getDate()} ${months[d.getMonth()]}`
|
||||
keyToLabel[key] = label
|
||||
trendMap[key] = 0
|
||||
}
|
||||
for (const b of trendData) {
|
||||
const key = b.createdAt.toISOString().slice(0, 10)
|
||||
if (key in trendMap) trendMap[key]++
|
||||
}
|
||||
const trend: { date: string; count: number }[] = []
|
||||
for (let i = 0; i < range; i++) {
|
||||
const d = new Date(now)
|
||||
d.setDate(d.getDate() - i)
|
||||
const key = d.toISOString().slice(0, 10)
|
||||
trend.push({ date: keyToLabel[key] ?? key, count: trendMap[key] ?? 0 })
|
||||
}
|
||||
trend.reverse()
|
||||
|
||||
return {
|
||||
totalBugs,
|
||||
openBugs,
|
||||
byStatus,
|
||||
byApp,
|
||||
bySource,
|
||||
avgResolutionHours,
|
||||
resolutionRate,
|
||||
trend,
|
||||
range,
|
||||
}
|
||||
}, {
|
||||
query: t.Object({
|
||||
range: t.Optional(t.String({ description: 'Rentang hari: 7, 30, atau 90 (default: 30)' })),
|
||||
}),
|
||||
detail: {
|
||||
summary: 'Bug Statistics',
|
||||
description: 'Statistik bug: total, distribusi status, per app, per source, avg resolution time, dan trend.',
|
||||
tags: ['Bugs'],
|
||||
},
|
||||
})
|
||||
|
||||
// ─── System Status API ─────────────────────────────
|
||||
.get('/api/system/status', async () => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user