diff --git a/generated/api.ts b/generated/api.ts index 0fa3037..fd2e984 100644 --- a/generated/api.ts +++ b/generated/api.ts @@ -239,6 +239,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/complaint/trends": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get complaint trends for last 7 months */ + get: operations["getApiComplaintTrends"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/complaint/service-stats": { parameters: { query?: never; @@ -1374,6 +1391,61 @@ export interface operations { }; }; }; + getApiComplaintTrends: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + data: { + month: string; + month_num: number; + count: number; + }[]; + }; + "multipart/form-data": { + data: { + month: string; + month_num: number; + count: number; + }[]; + }; + "text/plain": { + data: { + month: string; + month_num: number; + count: number; + }[]; + }; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error: string; + }; + "multipart/form-data": { + error: string; + }; + "text/plain": { + error: string; + }; + }; + }; + }; + }; "getApiComplaintService-stats": { parameters: { query?: never; diff --git a/generated/schema.json b/generated/schema.json index ac5ceb8..3f3c0c3 100644 --- a/generated/schema.json +++ b/generated/schema.json @@ -2561,6 +2561,157 @@ "summary": "Get recent complaints" } }, + "/api/complaint/trends": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "month", + "month_num", + "count" + ], + "properties": { + "month": { + "type": "string" + }, + "month_num": { + "type": "number" + }, + "count": { + "type": "number" + } + } + } + } + }, + "required": [ + "data" + ] + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "month", + "month_num", + "count" + ], + "properties": { + "month": { + "type": "string" + }, + "month_num": { + "type": "number" + }, + "count": { + "type": "number" + } + } + } + } + }, + "required": [ + "data" + ] + } + }, + "text/plain": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "month", + "month_num", + "count" + ], + "properties": { + "month": { + "type": "string" + }, + "month_num": { + "type": "number" + }, + "count": { + "type": "number" + } + } + } + } + }, + "required": [ + "data" + ] + } + } + } + }, + "500": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + }, + "text/plain": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + } + } + } + }, + "operationId": "getApiComplaintTrends", + "summary": "Get complaint trends for last 7 months" + } + }, "/api/complaint/service-stats": { "get": { "responses": { diff --git a/src/api/complaint.ts b/src/api/complaint.ts index 97e181d..2ece8ad 100644 --- a/src/api/complaint.ts +++ b/src/api/complaint.ts @@ -62,6 +62,46 @@ export const complaint = new Elysia({ detail: { summary: "Get recent complaints" }, }, ) + .get( + "/trends", + async ({ set }) => { + try { + // Get last 7 months complaint trends + const trends = await prisma.$queryRaw< + { month: string; month_num: number; count: number }[] + >` + SELECT + TO_CHAR("createdAt", 'Mon') as month, + EXTRACT(MONTH FROM "createdAt") as month_num, + COUNT(*)::INTEGER as count + FROM complaint + WHERE "createdAt" > NOW() - INTERVAL '7 months' + GROUP BY month, month_num + ORDER BY month_num ASC + `; + return { data: trends }; + } catch (error) { + logger.error({ error }, "Failed to fetch complaint trends"); + set.status = 500; + return { error: "Internal Server Error" }; + } + }, + { + response: { + 200: t.Object({ + data: t.Array( + t.Object({ + month: t.String(), + month_num: t.Number(), + count: t.Number(), + }), + ), + }), + 500: t.Object({ error: t.String() }), + }, + detail: { summary: "Get complaint trends for last 7 months" }, + }, + ) .get( "/service-stats", async ({ set }) => { diff --git a/src/components/pengaduan-layanan-publik.tsx b/src/components/pengaduan-layanan-publik.tsx index e012a0b..ff357c5 100644 --- a/src/components/pengaduan-layanan-publik.tsx +++ b/src/components/pengaduan-layanan-publik.tsx @@ -30,37 +30,18 @@ import { apiClient } from "@/utils/api-client"; dayjs.extend(relativeTime); -// Tren pengaduan data (Mock for now) -const trenData = [ - { bulan: "Apr", jumlah: 35 }, - { bulan: "Mei", jumlah: 48 }, - { bulan: "Jun", jumlah: 42 }, - { bulan: "Jul", jumlah: 55 }, - { bulan: "Agu", jumlah: 50 }, - { bulan: "Sep", jumlah: 58 }, - { bulan: "Okt", jumlah: 52 }, -]; +interface TrendData { + bulan: string; + jumlah: number; +} -// Ide inovatif data (Mock for now) -const ideInovatif = [ - { - nama: "Andi Prasetyo", - judul: "Penerapan Smart Village", - waktu: "3 hari yang lalu", - kategori: "Teknologi", - }, - { - nama: "Rina Kusuma", - judul: "Program Ekowisata Desa", - waktu: "5 hari yang lalu", - kategori: "Ekonomi", - }, -]; - -interface Complaint { +interface InnovationIdea { id: string; title: string; + description: string; category: string; + submitterName: string; + submitterContact?: string; status: string; createdAt: string; } @@ -77,6 +58,14 @@ interface ServiceApiResponse { }; } +interface Complaint { + id: string; + title: string; + category: string; + status: string; + createdAt: string; +} + const getStatusColor = (status: string) => { switch (status.toLowerCase()) { case "baru": @@ -103,16 +92,21 @@ const PengaduanLayananPublik = () => { }); const [recentComplaints, setRecentComplaints] = useState([]); const [serviceStats, setServiceStats] = useState([]); + const [trendData, setTrendData] = useState([]); + const [innovationIdeas, setInnovationIdeas] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { async function fetchData() { try { - const [statsRes, recentRes, serviceRes] = await Promise.all([ - apiClient.GET("/api/complaint/stats"), - apiClient.GET("/api/complaint/recent"), - apiClient.GET("/api/complaint/service-stats"), - ]); + const [statsRes, recentRes, serviceRes, trendsRes, ideasRes] = + await Promise.all([ + apiClient.GET("/api/complaint/stats"), + apiClient.GET("/api/complaint/recent"), + apiClient.GET("/api/complaint/service-stats"), + apiClient.GET("/api/complaint/trends"), + apiClient.GET("/api/complaint/innovation-ideas"), + ]); if (statsRes.data?.data) setStats(statsRes.data.data); if (recentRes.data?.data) @@ -126,6 +120,18 @@ const PengaduanLayananPublik = () => { })); setServiceStats(mappedService); } + if (trendsRes.data?.data) { + const mappedTrends = ( + trendsRes.data.data as { month: string; count: number }[] + ).map((item) => ({ + bulan: item.month, + jumlah: item.count, + })); + setTrendData(mappedTrends); + } + if (ideasRes.data?.data) { + setInnovationIdeas(ideasRes.data.data as InnovationIdea[]); + } } catch (error) { console.error("Failed to fetch complaint data", error); } finally { @@ -228,44 +234,57 @@ const PengaduanLayananPublik = () => { - - - - - - - + {loading ? ( + + + + ) : trendData.length > 0 ? ( + + + + + + + + ) : ( + + + Tidak ada data pengaduan 7 bulan terakhir + + + )} @@ -415,41 +434,51 @@ const PengaduanLayananPublik = () => { Ajuan Ide Inovatif - {ideInovatif.map((item) => ( - - - - - {item.judul} - - - {item.nama} - - - {item.waktu} - - - - - - ))} + {loading ? ( + + + + ) : innovationIdeas.length > 0 ? ( + innovationIdeas.map((item) => ( + + + + + {item.title} + + + {item.submitterName} + + + {dayjs(item.createdAt).fromNow()} + + + + + + )) + ) : ( + + Tidak ada ide inovatif + + )}