feat: connect pengaduan-layanan-publik to live database
New API Endpoint: - GET /api/complaint/trends - Fetch complaint trends for last 7 months Component Updates: - Removed hardcoded trenData array (7 months mock data) - Removed hardcoded ideInovatif array (2 mock ideas) - Added API calls to /api/complaint/trends and /api/complaint/innovation-ideas - Added loading states for trend chart and innovation ideas - Added empty states for both sections - Connected LineChart to real complaint data - Connected Innovation Ideas list to real InnovationIdea model Features Added: - Real-time complaint trend visualization - Real innovation ideas from database - Proper TypeScript typing for API responses - Loading skeletons during data fetch - Empty state messages when no data Files changed: - src/api/complaint.ts: Added /trends endpoint - src/components/pengaduan-layanan-publik.tsx: Connected to APIs - generated/api.ts: Regenerated types Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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<Complaint[]>([]);
|
||||
const [serviceStats, setServiceStats] = useState<ServiceStat[]>([]);
|
||||
const [trendData, setTrendData] = useState<TrendData[]>([]);
|
||||
const [innovationIdeas, setInnovationIdeas] = useState<InnovationIdea[]>([]);
|
||||
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 = () => {
|
||||
</Title>
|
||||
</Group>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={trenData}>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
vertical={false}
|
||||
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="bulan"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: dark ? "#1E293B" : "white",
|
||||
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="jumlah"
|
||||
stroke="#396aaaff"
|
||||
strokeWidth={2}
|
||||
dot={{
|
||||
fill: "#1E3A5F",
|
||||
strokeWidth: 2,
|
||||
r: 4,
|
||||
}}
|
||||
activeDot={{ r: 6 }}
|
||||
/>
|
||||
</LineChart>
|
||||
{loading ? (
|
||||
<Group justify="center" align="center" h="100%">
|
||||
<Loader />
|
||||
</Group>
|
||||
) : trendData.length > 0 ? (
|
||||
<LineChart data={trendData}>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
vertical={false}
|
||||
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="bulan"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: dark ? "#1E293B" : "white",
|
||||
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="jumlah"
|
||||
stroke="#396aaaff"
|
||||
strokeWidth={2}
|
||||
dot={{
|
||||
fill: "#1E3A5F",
|
||||
strokeWidth: 2,
|
||||
r: 4,
|
||||
}}
|
||||
activeDot={{ r: 6 }}
|
||||
/>
|
||||
</LineChart>
|
||||
) : (
|
||||
<Group justify="center" align="center" h="100%">
|
||||
<Text size="sm" c="dimmed">
|
||||
Tidak ada data pengaduan 7 bulan terakhir
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
|
||||
@@ -415,41 +434,51 @@ const PengaduanLayananPublik = () => {
|
||||
Ajuan Ide Inovatif
|
||||
</Title>
|
||||
<Stack gap="sm">
|
||||
{ideInovatif.map((item) => (
|
||||
<Card
|
||||
key={item.judul}
|
||||
p="sm"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#334155" : "#F1F5F9"}
|
||||
style={{
|
||||
borderColor: "transparent",
|
||||
transition: "background-color 0.15s ease",
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between">
|
||||
<Stack gap={0}>
|
||||
<Text fw={600} c={dark ? "white" : "gray.9"}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{item.nama}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{item.waktu}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
color="darmasaba-blue"
|
||||
radius="md"
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
{loading ? (
|
||||
<Group justify="center" py="xl">
|
||||
<Loader />
|
||||
</Group>
|
||||
) : innovationIdeas.length > 0 ? (
|
||||
innovationIdeas.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
p="sm"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#334155" : "#F1F5F9"}
|
||||
style={{
|
||||
borderColor: "transparent",
|
||||
transition: "background-color 0.15s ease",
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between">
|
||||
<Stack gap={0}>
|
||||
<Text fw={600} c={dark ? "white" : "gray.9"}>
|
||||
{item.title}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{item.submitterName}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{dayjs(item.createdAt).fromNow()}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
color="darmasaba-blue"
|
||||
radius="md"
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Group>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<Text c="dimmed" ta="center">
|
||||
Tidak ada ide inovatif
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
|
||||
Reference in New Issue
Block a user