diff --git a/generated/api.ts b/generated/api.ts index 8d46986..043b3d5 100644 --- a/generated/api.ts +++ b/generated/api.ts @@ -222,6 +222,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/complaint/innovation-ideas": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get recent innovation ideas */ + get: operations["getApiComplaintInnovation-ideas"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/resident/stats": { parameters: { query?: never; @@ -263,7 +280,7 @@ export interface paths { path?: never; cookie?: never; }; - /** Get religious and gender demographics */ + /** Get demographics including religion, gender, occupation and age */ get: operations["getApiResidentDemographics"]; put?: never; post?: never; @@ -932,6 +949,23 @@ export interface operations { }; }; }; + "getApiComplaintInnovation-ideas": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; getApiResidentStats: { parameters: { query?: never; diff --git a/generated/schema.json b/generated/schema.json index d30e21f..5f74e5d 100644 --- a/generated/schema.json +++ b/generated/schema.json @@ -1468,6 +1468,15 @@ } } }, + "/api/complaint/innovation-ideas": { + "get": { + "operationId": "getApiComplaintInnovation-ideas", + "summary": "Get recent innovation ideas", + "responses": { + "200": {} + } + } + }, "/api/resident/stats": { "get": { "operationId": "getApiResidentStats", @@ -1489,7 +1498,7 @@ "/api/resident/demographics": { "get": { "operationId": "getApiResidentDemographics", - "summary": "Get religious and gender demographics", + "summary": "Get demographics including religion, gender, occupation and age", "responses": { "200": {} } diff --git a/src/api/complaint.ts b/src/api/complaint.ts index 77b977c..6247e4c 100644 --- a/src/api/complaint.ts +++ b/src/api/complaint.ts @@ -63,4 +63,23 @@ export const complaint = new Elysia({ { detail: { summary: "Get service letter statistics by type" }, }, + ) + .get( + "/innovation-ideas", + async ({ set }) => { + try { + const ideas = await prisma.innovationIdea.findMany({ + orderBy: { createdAt: "desc" }, + take: 5, + }); + return { data: ideas }; + } catch (error) { + logger.error({ error }, "Failed to fetch innovation ideas"); + set.status = 500; + return { error: "Internal Server Error" }; + } + }, + { + detail: { summary: "Get recent innovation ideas" }, + }, ); diff --git a/src/api/resident.ts b/src/api/resident.ts index 36eba90..91e59c2 100644 --- a/src/api/resident.ts +++ b/src/api/resident.ts @@ -53,7 +53,7 @@ export const resident = new Elysia({ "/demographics", async ({ set }) => { try { - const [religion, gender] = await Promise.all([ + const [religion, gender, occupation, ageGroups] = await Promise.all([ prisma.resident.groupBy({ by: ["religion"], _count: { _all: true }, @@ -62,8 +62,31 @@ export const resident = new Elysia({ by: ["gender"], _count: { _all: true }, }), + prisma.resident.groupBy({ + by: ["occupation"], + _count: { _all: true }, + orderBy: { _count: { occupation: "desc" } }, + take: 10, + }), + // Group by age ranges (simplified calculation) + prisma.$queryRaw` + SELECT + CASE + WHEN date_part('year', age(now(), "birthDate")) BETWEEN 0 AND 16 THEN '0-16' + WHEN date_part('year', age(now(), "birthDate")) BETWEEN 17 AND 25 THEN '17-25' + WHEN date_part('year', age(now(), "birthDate")) BETWEEN 26 AND 35 THEN '26-35' + WHEN date_part('year', age(now(), "birthDate")) BETWEEN 36 AND 45 THEN '36-45' + WHEN date_part('year', age(now(), "birthDate")) BETWEEN 46 AND 55 THEN '46-55' + WHEN date_part('year', age(now(), "birthDate")) BETWEEN 56 AND 65 THEN '56-65' + ELSE '65+' + END as range, + COUNT(*) as count + FROM resident + GROUP BY range + ORDER BY range ASC + `, ]); - return { data: { religion, gender } }; + return { data: { religion, gender, occupation, ageGroups } }; } catch (error) { logger.error({ error }, "Failed to fetch demographics"); set.status = 500; @@ -71,6 +94,9 @@ export const resident = new Elysia({ } }, { - detail: { summary: "Get religious and gender demographics" }, + detail: { + summary: + "Get demographics including religion, gender, occupation and age", + }, }, ); diff --git a/src/components/dashboard-content.tsx b/src/components/dashboard-content.tsx index 1173e83..a8c2c9b 100644 --- a/src/components/dashboard-content.tsx +++ b/src/components/dashboard-content.tsx @@ -55,7 +55,11 @@ export function DashboardContent() { proses: 0, selesai: 0, }, - residents: (residentRes.data as any)?.data || { total: 0, heads: 0, poor: 0 }, + residents: (residentRes.data as any)?.data || { + total: 0, + heads: 0, + poor: 0, + }, loading: false, }); } catch (error) { diff --git a/src/components/demografi-pekerjaan.tsx b/src/components/demografi-pekerjaan.tsx index 33e5d31..3f63fec 100644 --- a/src/components/demografi-pekerjaan.tsx +++ b/src/components/demografi-pekerjaan.tsx @@ -5,6 +5,7 @@ import { Grid, GridCol, Group, + Loader, Progress, Stack, Text, @@ -21,6 +22,7 @@ import { TrendingDown, Users, } from "lucide-react"; +import { useEffect, useState } from "react"; import { Bar, BarChart, @@ -33,106 +35,9 @@ import { XAxis, YAxis, } from "recharts"; +import { apiClient } from "@/utils/api-client"; -// KPI Data -const kpiData = [ - { - id: 1, - title: "Total Penduduk", - value: "5.634", - subtitle: "Aktif terdaftar", - icon: Users, - }, - { - id: 2, - title: "Kepala Keluarga", - value: "1.354", - subtitle: "Total KK", - icon: Home, - }, - { - id: 3, - title: "Kelahiran", - value: "23", - subtitle: "Tahun ini", - icon: Baby, - }, - { - id: 4, - title: "Kemiskinan", - value: "324", - subtitle: "-10% dari tahun lalu", - trend: "positive", - icon: TrendingDown, - }, -]; - -// Age Distribution Data -const ageDistributionData = [ - { ageRange: "17-25", total: 850 }, - { ageRange: "26-35", total: 1200 }, - { ageRange: "36-45", total: 1100 }, - { ageRange: "46-55", total: 950 }, - { ageRange: "56-65", total: 750 }, - { ageRange: "65+", total: 484 }, -]; - -// Job Distribution Data -const jobDistributionData = [ - { job: "Sipil", total: 1200 }, - { job: "Guru", total: 850 }, - { job: "Petani", total: 950 }, - { job: "Pedagang", total: 750 }, - { job: "Wiraswasta", total: 984 }, -]; - -// Religion Data -const religionData = [ - { name: "Hindu", value: 4234, color: "#EF4444" }, - { name: "Islam", value: 856, color: "#3B82F6" }, - { name: "Kristen", value: 412, color: "#22C55E" }, - { name: "Buddha", value: 202, color: "#FACC15" }, -]; - -// Banjar Data -const banjarData = [ - { banjar: "Darmasaba", population: 1200, kk: 300, poor: 45 }, - { banjar: "Manesa", population: 950, kk: 240, poor: 32 }, - { banjar: "Cabe", population: 800, kk: 200, poor: 28 }, - { banjar: "Penenjoan", population: 1100, kk: 280, poor: 38 }, - { banjar: "Baler Pasar", population: 984, kk: 250, poor: 42 }, - { banjar: "Bucu", population: 600, kk: 184, poor: 25 }, -]; - -// Dynamic Stats Data -const dynamicStats = [ - { - title: "Kelahiran", - value: "23", - icon: Baby, - color: "#22C55E", - }, - { - title: "Kematian", - value: "12", - icon: TrendingDown, - color: "#EF4444", - }, - { - title: "Pindah Masuk", - value: "45", - icon: Users, - color: "#3B82F6", - }, - { - title: "Pindah Keluar", - value: "32", - icon: Users, - color: "#3B82F6", - }, -]; - -// Sektor Unggulan Data +// Sektor Unggulan Data (Mock for now) const sektorUnggulanData = [ { sektor: "Pertanian", value: 65 }, { sektor: "Perdagangan", value: 45 }, @@ -144,6 +49,134 @@ const DemografiPekerjaan = () => { const { colorScheme } = useMantineColorScheme(); const dark = colorScheme === "dark"; + const [stats, setStats] = useState({ + total: 0, + heads: 0, + poor: 0, + }); + const [ageData, setAgeData] = useState([]); + const [jobData, setJobData] = useState([]); + const [religionData, setReligionData] = useState([]); + const [banjarData, setBanjarData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchData() { + try { + const [statsRes, banjarRes, demoRes] = await Promise.all([ + apiClient.GET("/api/resident/stats"), + apiClient.GET("/api/resident/banjar-stats"), + apiClient.GET("/api/resident/demographics"), + ]); + + if (statsRes.data?.data) setStats(statsRes.data.data); + if (banjarRes.data?.data) setBanjarData(banjarRes.data.data); + if (demoRes.data?.data) { + const { religion, occupation, ageGroups } = demoRes.data.data; + + const religionColors: Record = { + HINDU: "#EF4444", + ISLAM: "#3B82F6", + KRISTEN: "#22C55E", + KATOLIK: "#A855F7", + BUDDHA: "#FACC15", + KONGHUCU: "#F97316", + LAINNYA: "#94A3B8", + }; + + setReligionData( + (religion as any[]).map((r) => ({ + name: r.religion, + value: r._count._all, + color: religionColors[r.religion] || "#94A3B8", + })), + ); + + setJobData( + (occupation as any[]).map((o) => ({ + job: o.occupation || "Lainnya", + total: o._count._all, + })), + ); + + setAgeData( + (ageGroups as any[]).map((a) => ({ + ageRange: a.range, + total: Number(a.count), + })), + ); + } + } catch (error) { + console.error("Failed to fetch demografi data", error); + } finally { + setLoading(false); + } + } + + fetchData(); + }, []); + + // KPI Data + const kpiData = [ + { + id: 1, + title: "Total Penduduk", + value: stats.total.toLocaleString(), + subtitle: "Aktif terdaftar", + icon: Users, + }, + { + id: 2, + title: "Kepala Keluarga", + value: stats.heads.toLocaleString(), + subtitle: "Total KK", + icon: Home, + }, + { + id: 3, + title: "Kelahiran", + value: "0", + subtitle: "Tahun ini", + icon: Baby, + }, + { + id: 4, + title: "Kemiskinan", + value: stats.poor.toLocaleString(), + subtitle: "Keluarga Prasejahtera", + trend: "positive", + icon: TrendingDown, + }, + ]; + + // Dynamic Stats Data (Mock for now as no records in DB yet) + const dynamicStats = [ + { + title: "Kelahiran", + value: "0", + icon: Baby, + color: "#22C55E", + }, + { + title: "Kematian", + value: "0", + icon: TrendingDown, + color: "#EF4444", + }, + { + title: "Pindah Masuk", + value: "0", + icon: Users, + color: "#3B82F6", + }, + { + title: "Pindah Keluar", + value: "0", + icon: Users, + color: "#3B82F6", + }, + ]; + return ( {/* TOP SECTION - 4 STAT CARDS */} @@ -226,44 +259,50 @@ const DemografiPekerjaan = () => { - - - - - - - + {loading ? ( + + + + ) : ( + + + + + + + + )} @@ -290,46 +329,52 @@ const DemografiPekerjaan = () => { - - - - - - - + {loading ? ( + + + + ) : ( + + + + + + + + )} @@ -420,50 +465,57 @@ const DemografiPekerjaan = () => { - - - {religionData.map((entry, index) => ( - - ))} - - - + {loading ? ( + + + + ) : ( + + + {religionData.map((entry, index) => ( + + ))} + + + + )} - {religionData.map((item, index) => ( - - - - - {item.name} + {!loading && + religionData.map((item, index) => ( + + + + + {item.name} + + + + {item.value.toLocaleString()} - - {item.value.toLocaleString()} - - - ))} + ))} @@ -490,118 +542,124 @@ const DemografiPekerjaan = () => { - - - - - - - - - - - {banjarData.map((item, index) => ( - - + {banjarData.map((item, index) => ( + + + + + + + ))} + +
- Banjar - - Penduduk - - KK - - Miskin -
+ + + ) : ( + + + + - - + Banjar + + + + - ))} - -
- {item.banjar} - - - {item.population.toLocaleString()} - - {item.kk.toLocaleString()} - - {item.poor.toLocaleString()} - + Penduduk + + KK + + Miskin +
+ +
+ {item.name} + + {(item.totalPopulation || 0).toLocaleString()} + + {(item.totalKK || 0).toLocaleString()} + + {(item.totalPoor || 0).toLocaleString()} +
+ )}
diff --git a/src/components/kinerja-divisi/division-list.tsx b/src/components/kinerja-divisi/division-list.tsx index 2b310e0..4e86893 100644 --- a/src/components/kinerja-divisi/division-list.tsx +++ b/src/components/kinerja-divisi/division-list.tsx @@ -27,10 +27,12 @@ export function DivisionList() { try { const { data } = await apiClient.GET("/api/division/"); if (data?.data) { - const mapped = data.data.map((div: { name: string; _count?: { activities: number } }) => ({ - name: div.name, - count: div._count?.activities || 0, - })); + const mapped = data.data.map( + (div: { name: string; _count?: { activities: number } }) => ({ + name: div.name, + count: div._count?.activities || 0, + }), + ); setDivisions(mapped); } } catch (error) {