feat: connect kinerja divisi components to live database

New API Endpoints (src/api/division.ts):
- GET /api/division/discussions - Fetch recent discussions with sender info
- GET /api/division/documents/stats - Fetch document counts by type
- GET /api/division/activities/stats - Fetch activity status breakdown with percentages

Components Connected to Database:
- discussion-panel.tsx: Now fetches from /api/division/discussions
- document-chart.tsx: Now fetches from /api/division/documents/stats
- progress-chart.tsx: Now fetches from /api/division/activities/stats

Features Added:
- Loading states with Loader component
- Empty states with friendly messages
- Date formatting using date-fns with Indonesian locale
- Real-time data from database instead of hardcoded values
- Proper TypeScript typing for API responses

Files changed:
- src/api/division.ts: Added 3 new API endpoints
- src/components/kinerja-divisi/discussion-panel.tsx
- src/components/kinerja-divisi/document-chart.tsx
- src/components/kinerja-divisi/progress-chart.tsx

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-03-27 15:43:58 +08:00
parent b77822f2dd
commit 75c7bc249e
4 changed files with 390 additions and 141 deletions

View File

@@ -64,15 +64,36 @@ export const division = new Elysia({
},
)
.get(
"/metrics",
"/activities/stats",
async ({ set }) => {
try {
const metrics = await prisma.divisionMetric.findMany({
include: { division: true },
});
return { data: metrics };
// Get activity count by status
const [selesai, berjalan, tertunda, dibatalkan] = await Promise.all([
prisma.activity.count({ where: { status: "SELESAI" } }),
prisma.activity.count({ where: { status: "BERJALAN" } }),
prisma.activity.count({ where: { status: "TERTUNDA" } }),
prisma.activity.count({ where: { status: "DIBATALKAN" } }),
]);
const total = selesai + berjalan + tertunda + dibatalkan;
// Calculate percentages
const percentages = {
selesai: total > 0 ? (selesai / total) * 100 : 0,
berjalan: total > 0 ? (berjalan / total) * 100 : 0,
tertunda: total > 0 ? (tertunda / total) * 100 : 0,
dibatalkan: total > 0 ? (dibatalkan / total) * 100 : 0,
};
return {
data: {
total,
counts: { selesai, berjalan, tertunda, dibatalkan },
percentages,
},
};
} catch (error) {
logger.error({ error }, "Failed to fetch division metrics");
logger.error({ error }, "Failed to fetch activity stats");
set.status = 500;
return { error: "Internal Server Error" };
}
@@ -80,10 +101,117 @@ export const division = new Elysia({
{
response: {
200: t.Object({
data: t.Array(t.Any()),
data: t.Object({
total: t.Number(),
counts: t.Object({
selesai: t.Number(),
berjalan: t.Number(),
tertunda: t.Number(),
dibatalkan: t.Number(),
}),
percentages: t.Object({
selesai: t.Number(),
berjalan: t.Number(),
tertunda: t.Number(),
dibatalkan: t.Number(),
}),
}),
}),
500: t.Object({ error: t.String() }),
},
detail: { summary: "Get division performance metrics" },
detail: { summary: "Get activity statistics by status" },
},
)
.get(
"/documents/stats",
async ({ set }) => {
try {
// Group documents by type
const [gambarCount, dokumenCount] = await Promise.all([
prisma.document.count({ where: { type: "Gambar" } }),
prisma.document.count({ where: { type: "Dokumen" } }),
]);
return {
data: [
{ name: "Gambar", jumlah: gambarCount, color: "#FACC15" },
{ name: "Dokumen", jumlah: dokumenCount, color: "#22C55E" },
],
};
} catch (error) {
logger.error({ error }, "Failed to fetch document stats");
set.status = 500;
return { error: "Internal Server Error" };
}
},
{
response: {
200: t.Object({
data: t.Array(
t.Object({
name: t.String(),
jumlah: t.Number(),
color: t.String(),
}),
),
}),
500: t.Object({ error: t.String() }),
},
detail: { summary: "Get document statistics by type" },
},
)
.get(
"/discussions",
async ({ set }) => {
try {
// Get recent discussions with sender info
const discussions = await prisma.discussion.findMany({
where: { parentId: null }, // Only top-level discussions
include: {
sender: {
select: { name: true, email: true },
},
division: {
select: { name: true },
},
},
orderBy: { createdAt: "desc" },
take: 10,
});
// Format for frontend
const formattedDiscussions = discussions.map((d) => ({
id: d.id,
message: d.message,
sender: d.sender.name || d.sender.email,
date: d.createdAt.toISOString(),
division: d.division?.name || null,
isResolved: d.isResolved,
}));
return { data: formattedDiscussions };
} catch (error) {
logger.error({ error }, "Failed to fetch discussions");
set.status = 500;
return { error: "Internal Server Error" };
}
},
{
response: {
200: t.Object({
data: t.Array(
t.Object({
id: t.String(),
message: t.String(),
sender: t.String(),
date: t.String(),
division: t.Nullable(t.String()),
isResolved: t.Boolean(),
}),
),
}),
500: t.Object({ error: t.String() }),
},
detail: { summary: "Get recent discussions" },
},
);