feat(database): implement resident and complaint API and connect DemografiPekerjaan
This commit is contained in:
@@ -222,6 +222,23 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: 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": {
|
"/api/resident/stats": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -263,7 +280,7 @@ export interface paths {
|
|||||||
path?: never;
|
path?: never;
|
||||||
cookie?: never;
|
cookie?: never;
|
||||||
};
|
};
|
||||||
/** Get religious and gender demographics */
|
/** Get demographics including religion, gender, occupation and age */
|
||||||
get: operations["getApiResidentDemographics"];
|
get: operations["getApiResidentDemographics"];
|
||||||
put?: never;
|
put?: never;
|
||||||
post?: 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: {
|
getApiResidentStats: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|||||||
@@ -1468,6 +1468,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/complaint/innovation-ideas": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getApiComplaintInnovation-ideas",
|
||||||
|
"summary": "Get recent innovation ideas",
|
||||||
|
"responses": {
|
||||||
|
"200": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/resident/stats": {
|
"/api/resident/stats": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getApiResidentStats",
|
"operationId": "getApiResidentStats",
|
||||||
@@ -1489,7 +1498,7 @@
|
|||||||
"/api/resident/demographics": {
|
"/api/resident/demographics": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getApiResidentDemographics",
|
"operationId": "getApiResidentDemographics",
|
||||||
"summary": "Get religious and gender demographics",
|
"summary": "Get demographics including religion, gender, occupation and age",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {}
|
"200": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,4 +63,23 @@ export const complaint = new Elysia({
|
|||||||
{
|
{
|
||||||
detail: { summary: "Get service letter statistics by type" },
|
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" },
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const resident = new Elysia({
|
|||||||
"/demographics",
|
"/demographics",
|
||||||
async ({ set }) => {
|
async ({ set }) => {
|
||||||
try {
|
try {
|
||||||
const [religion, gender] = await Promise.all([
|
const [religion, gender, occupation, ageGroups] = await Promise.all([
|
||||||
prisma.resident.groupBy({
|
prisma.resident.groupBy({
|
||||||
by: ["religion"],
|
by: ["religion"],
|
||||||
_count: { _all: true },
|
_count: { _all: true },
|
||||||
@@ -62,8 +62,31 @@ export const resident = new Elysia({
|
|||||||
by: ["gender"],
|
by: ["gender"],
|
||||||
_count: { _all: true },
|
_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<any[]>`
|
||||||
|
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) {
|
} catch (error) {
|
||||||
logger.error({ error }, "Failed to fetch demographics");
|
logger.error({ error }, "Failed to fetch demographics");
|
||||||
set.status = 500;
|
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",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -55,7 +55,11 @@ export function DashboardContent() {
|
|||||||
proses: 0,
|
proses: 0,
|
||||||
selesai: 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,
|
loading: false,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Grid,
|
Grid,
|
||||||
GridCol,
|
GridCol,
|
||||||
Group,
|
Group,
|
||||||
|
Loader,
|
||||||
Progress,
|
Progress,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
TrendingDown,
|
TrendingDown,
|
||||||
Users,
|
Users,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Bar,
|
Bar,
|
||||||
BarChart,
|
BarChart,
|
||||||
@@ -33,106 +35,9 @@ import {
|
|||||||
XAxis,
|
XAxis,
|
||||||
YAxis,
|
YAxis,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
|
import { apiClient } from "@/utils/api-client";
|
||||||
|
|
||||||
// KPI Data
|
// Sektor Unggulan Data (Mock for now)
|
||||||
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
|
|
||||||
const sektorUnggulanData = [
|
const sektorUnggulanData = [
|
||||||
{ sektor: "Pertanian", value: 65 },
|
{ sektor: "Pertanian", value: 65 },
|
||||||
{ sektor: "Perdagangan", value: 45 },
|
{ sektor: "Perdagangan", value: 45 },
|
||||||
@@ -144,6 +49,134 @@ const DemografiPekerjaan = () => {
|
|||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
const dark = colorScheme === "dark";
|
const dark = colorScheme === "dark";
|
||||||
|
|
||||||
|
const [stats, setStats] = useState({
|
||||||
|
total: 0,
|
||||||
|
heads: 0,
|
||||||
|
poor: 0,
|
||||||
|
});
|
||||||
|
const [ageData, setAgeData] = useState<any[]>([]);
|
||||||
|
const [jobData, setJobData] = useState<any[]>([]);
|
||||||
|
const [religionData, setReligionData] = useState<any[]>([]);
|
||||||
|
const [banjarData, setBanjarData] = useState<any[]>([]);
|
||||||
|
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<string, string> = {
|
||||||
|
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 (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
{/* TOP SECTION - 4 STAT CARDS */}
|
{/* TOP SECTION - 4 STAT CARDS */}
|
||||||
@@ -226,44 +259,50 @@ const DemografiPekerjaan = () => {
|
|||||||
</Title>
|
</Title>
|
||||||
</Group>
|
</Group>
|
||||||
<ResponsiveContainer width="100%" height={250}>
|
<ResponsiveContainer width="100%" height={250}>
|
||||||
<BarChart data={ageDistributionData}>
|
{loading ? (
|
||||||
<CartesianGrid
|
<Group justify="center" align="center" h="100%">
|
||||||
strokeDasharray="3 3"
|
<Loader />
|
||||||
vertical={false}
|
</Group>
|
||||||
stroke={dark ? "#334155" : "#e5e7eb"}
|
) : (
|
||||||
/>
|
<BarChart data={ageData}>
|
||||||
<XAxis
|
<CartesianGrid
|
||||||
dataKey="ageRange"
|
strokeDasharray="3 3"
|
||||||
axisLine={false}
|
vertical={false}
|
||||||
tickLine={false}
|
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||||
tick={{
|
/>
|
||||||
fill: dark ? "#E2E8F0" : "#374151",
|
<XAxis
|
||||||
fontSize: 12,
|
dataKey="ageRange"
|
||||||
}}
|
axisLine={false}
|
||||||
/>
|
tickLine={false}
|
||||||
<YAxis
|
tick={{
|
||||||
axisLine={false}
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
tickLine={false}
|
fontSize: 12,
|
||||||
tick={{
|
}}
|
||||||
fill: dark ? "#E2E8F0" : "#374151",
|
/>
|
||||||
fontSize: 12,
|
<YAxis
|
||||||
}}
|
axisLine={false}
|
||||||
/>
|
tickLine={false}
|
||||||
<Tooltip
|
tick={{
|
||||||
contentStyle={{
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
backgroundColor: dark ? "#1E293B" : "white",
|
fontSize: 12,
|
||||||
borderColor: dark ? "#334155" : "#e5e7eb",
|
}}
|
||||||
borderRadius: "8px",
|
/>
|
||||||
}}
|
<Tooltip
|
||||||
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
|
contentStyle={{
|
||||||
/>
|
backgroundColor: dark ? "#1E293B" : "white",
|
||||||
<Bar
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
dataKey="total"
|
borderRadius: "8px",
|
||||||
fill="#396aaaff"
|
}}
|
||||||
radius={[8, 8, 0, 0]}
|
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
|
||||||
maxBarSize={40}
|
/>
|
||||||
/>
|
<Bar
|
||||||
</BarChart>
|
dataKey="total"
|
||||||
|
fill="#396aaaff"
|
||||||
|
radius={[8, 8, 0, 0]}
|
||||||
|
maxBarSize={40}
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
)}
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
@@ -290,46 +329,52 @@ const DemografiPekerjaan = () => {
|
|||||||
</Title>
|
</Title>
|
||||||
</Group>
|
</Group>
|
||||||
<ResponsiveContainer width="100%" height={250}>
|
<ResponsiveContainer width="100%" height={250}>
|
||||||
<BarChart data={jobDistributionData} layout="vertical">
|
{loading ? (
|
||||||
<CartesianGrid
|
<Group justify="center" align="center" h="100%">
|
||||||
strokeDasharray="3 3"
|
<Loader />
|
||||||
horizontal={false}
|
</Group>
|
||||||
stroke={dark ? "#334155" : "#e5e7eb"}
|
) : (
|
||||||
/>
|
<BarChart data={jobData} layout="vertical">
|
||||||
<XAxis
|
<CartesianGrid
|
||||||
type="number"
|
strokeDasharray="3 3"
|
||||||
axisLine={false}
|
horizontal={false}
|
||||||
tickLine={false}
|
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||||
tick={{
|
/>
|
||||||
fill: dark ? "#E2E8F0" : "#374151",
|
<XAxis
|
||||||
fontSize: 12,
|
type="number"
|
||||||
}}
|
axisLine={false}
|
||||||
/>
|
tickLine={false}
|
||||||
<YAxis
|
tick={{
|
||||||
type="category"
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
dataKey="job"
|
fontSize: 12,
|
||||||
axisLine={false}
|
}}
|
||||||
tickLine={false}
|
/>
|
||||||
tick={{
|
<YAxis
|
||||||
fill: dark ? "#E2E8F0" : "#374151",
|
type="category"
|
||||||
fontSize: 12,
|
dataKey="job"
|
||||||
}}
|
axisLine={false}
|
||||||
width={90}
|
tickLine={false}
|
||||||
/>
|
tick={{
|
||||||
<Tooltip
|
fill: dark ? "#E2E8F0" : "#374151",
|
||||||
contentStyle={{
|
fontSize: 12,
|
||||||
backgroundColor: dark ? "#1E293B" : "white",
|
}}
|
||||||
borderColor: dark ? "#334155" : "#e5e7eb",
|
width={90}
|
||||||
borderRadius: "8px",
|
/>
|
||||||
}}
|
<Tooltip
|
||||||
/>
|
contentStyle={{
|
||||||
<Bar
|
backgroundColor: dark ? "#1E293B" : "white",
|
||||||
dataKey="total"
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
fill="#396aaaff"
|
borderRadius: "8px",
|
||||||
radius={[0, 8, 8, 0]}
|
}}
|
||||||
maxBarSize={30}
|
/>
|
||||||
/>
|
<Bar
|
||||||
</BarChart>
|
dataKey="total"
|
||||||
|
fill="#396aaaff"
|
||||||
|
radius={[0, 8, 8, 0]}
|
||||||
|
maxBarSize={30}
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
)}
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
@@ -420,50 +465,57 @@ const DemografiPekerjaan = () => {
|
|||||||
</Title>
|
</Title>
|
||||||
</Group>
|
</Group>
|
||||||
<ResponsiveContainer width="100%" height={250}>
|
<ResponsiveContainer width="100%" height={250}>
|
||||||
<PieChart>
|
{loading ? (
|
||||||
<Pie
|
<Group justify="center" align="center" h="100%">
|
||||||
data={religionData}
|
<Loader />
|
||||||
cx="50%"
|
</Group>
|
||||||
cy="50%"
|
) : (
|
||||||
innerRadius={60}
|
<PieChart>
|
||||||
outerRadius={90}
|
<Pie
|
||||||
paddingAngle={2}
|
data={religionData}
|
||||||
dataKey="value"
|
cx="50%"
|
||||||
>
|
cy="50%"
|
||||||
{religionData.map((entry, index) => (
|
innerRadius={60}
|
||||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
outerRadius={90}
|
||||||
))}
|
paddingAngle={2}
|
||||||
</Pie>
|
dataKey="value"
|
||||||
<Tooltip
|
>
|
||||||
contentStyle={{
|
{religionData.map((entry, index) => (
|
||||||
backgroundColor: dark ? "#1E293B" : "white",
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||||
borderColor: dark ? "#334155" : "#e5e7eb",
|
))}
|
||||||
borderRadius: "8px",
|
</Pie>
|
||||||
}}
|
<Tooltip
|
||||||
/>
|
contentStyle={{
|
||||||
</PieChart>
|
backgroundColor: dark ? "#1E293B" : "white",
|
||||||
|
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PieChart>
|
||||||
|
)}
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
<Stack gap="xs" mt="md">
|
<Stack gap="xs" mt="md">
|
||||||
{religionData.map((item, index) => (
|
{!loading &&
|
||||||
<Group key={index} justify="space-between">
|
religionData.map((item, index) => (
|
||||||
<Group gap="xs">
|
<Group key={index} justify="space-between">
|
||||||
<Box
|
<Group gap="xs">
|
||||||
w={10}
|
<Box
|
||||||
h={10}
|
w={10}
|
||||||
style={{
|
h={10}
|
||||||
backgroundColor: item.color,
|
style={{
|
||||||
borderRadius: 2,
|
backgroundColor: item.color,
|
||||||
}}
|
borderRadius: 2,
|
||||||
/>
|
}}
|
||||||
<Text size="sm" c={dark ? "white" : "gray.7"}>
|
/>
|
||||||
{item.name}
|
<Text size="sm" c={dark ? "white" : "gray.7"}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="sm" fw={600} c={dark ? "white" : "gray.9"}>
|
||||||
|
{item.value.toLocaleString()}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="sm" fw={600} c={dark ? "white" : "gray.9"}>
|
))}
|
||||||
{item.value.toLocaleString()}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
))}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
@@ -490,118 +542,124 @@ const DemografiPekerjaan = () => {
|
|||||||
</Title>
|
</Title>
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: "auto" }}>
|
<Box style={{ overflowX: "auto" }}>
|
||||||
<table style={{ width: "100%", borderCollapse: "collapse" }}>
|
{loading ? (
|
||||||
<thead>
|
<Group justify="center" py="xl">
|
||||||
<tr>
|
<Loader />
|
||||||
<th
|
</Group>
|
||||||
style={{
|
) : (
|
||||||
textAlign: "left",
|
<table style={{ width: "100%", borderCollapse: "collapse" }}>
|
||||||
padding: "8px",
|
<thead>
|
||||||
fontSize: "12px",
|
<tr>
|
||||||
fontWeight: 600,
|
<th
|
||||||
color: dark ? "#94A3B8" : "#64748B",
|
|
||||||
borderBottom: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Banjar
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
style={{
|
|
||||||
textAlign: "right",
|
|
||||||
padding: "8px",
|
|
||||||
fontSize: "12px",
|
|
||||||
fontWeight: 600,
|
|
||||||
color: dark ? "#94A3B8" : "#64748B",
|
|
||||||
borderBottom: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Penduduk
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
style={{
|
|
||||||
textAlign: "right",
|
|
||||||
padding: "8px",
|
|
||||||
fontSize: "12px",
|
|
||||||
fontWeight: 600,
|
|
||||||
color: dark ? "#94A3B8" : "#64748B",
|
|
||||||
borderBottom: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
KK
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
style={{
|
|
||||||
textAlign: "right",
|
|
||||||
padding: "8px",
|
|
||||||
fontSize: "12px",
|
|
||||||
fontWeight: 600,
|
|
||||||
color: dark ? "#94A3B8" : "#64748B",
|
|
||||||
borderBottom: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Miskin
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{banjarData.map((item, index) => (
|
|
||||||
<tr
|
|
||||||
key={index}
|
|
||||||
style={{
|
|
||||||
backgroundColor:
|
|
||||||
index % 2 === 0
|
|
||||||
? dark
|
|
||||||
? "#334155"
|
|
||||||
: "#F8FAFC"
|
|
||||||
: "transparent",
|
|
||||||
transition: "background-color 0.15s ease",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<td
|
|
||||||
style={{
|
style={{
|
||||||
padding: "10px 8px",
|
textAlign: "left",
|
||||||
fontSize: "13px",
|
padding: "8px",
|
||||||
fontWeight: 500,
|
fontSize: "12px",
|
||||||
color: dark ? "#E2E8F0" : "#1E293B",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.banjar}
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
style={{
|
|
||||||
padding: "10px 8px",
|
|
||||||
textAlign: "right",
|
|
||||||
fontSize: "13px",
|
|
||||||
color: dark ? "#E2E8F0" : "#1E293B",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.population.toLocaleString()}
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
style={{
|
|
||||||
padding: "10px 8px",
|
|
||||||
textAlign: "right",
|
|
||||||
fontSize: "13px",
|
|
||||||
color: dark ? "#E2E8F0" : "#1E293B",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.kk.toLocaleString()}
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
style={{
|
|
||||||
padding: "10px 8px",
|
|
||||||
textAlign: "right",
|
|
||||||
fontSize: "13px",
|
|
||||||
color: "#EF4444",
|
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
|
color: dark ? "#94A3B8" : "#64748B",
|
||||||
|
borderBottom: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.poor.toLocaleString()}
|
Banjar
|
||||||
</td>
|
</th>
|
||||||
|
<th
|
||||||
|
style={{
|
||||||
|
textAlign: "right",
|
||||||
|
padding: "8px",
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: dark ? "#94A3B8" : "#64748B",
|
||||||
|
borderBottom: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Penduduk
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style={{
|
||||||
|
textAlign: "right",
|
||||||
|
padding: "8px",
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: dark ? "#94A3B8" : "#64748B",
|
||||||
|
borderBottom: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
KK
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style={{
|
||||||
|
textAlign: "right",
|
||||||
|
padding: "8px",
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: dark ? "#94A3B8" : "#64748B",
|
||||||
|
borderBottom: `1px solid ${dark ? "#334155" : "#e5e7eb"}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Miskin
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{banjarData.map((item, index) => (
|
||||||
|
<tr
|
||||||
|
key={item.id || index}
|
||||||
|
style={{
|
||||||
|
backgroundColor:
|
||||||
|
index % 2 === 0
|
||||||
|
? dark
|
||||||
|
? "#334155"
|
||||||
|
: "#F8FAFC"
|
||||||
|
: "transparent",
|
||||||
|
transition: "background-color 0.15s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
padding: "10px 8px",
|
||||||
|
fontSize: "13px",
|
||||||
|
fontWeight: 500,
|
||||||
|
color: dark ? "#E2E8F0" : "#1E293B",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
padding: "10px 8px",
|
||||||
|
textAlign: "right",
|
||||||
|
fontSize: "13px",
|
||||||
|
color: dark ? "#E2E8F0" : "#1E293B",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(item.totalPopulation || 0).toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
padding: "10px 8px",
|
||||||
|
textAlign: "right",
|
||||||
|
fontSize: "13px",
|
||||||
|
color: dark ? "#E2E8F0" : "#1E293B",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(item.totalKK || 0).toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
padding: "10px 8px",
|
||||||
|
textAlign: "right",
|
||||||
|
fontSize: "13px",
|
||||||
|
color: "#EF4444",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(item.totalPoor || 0).toLocaleString()}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
|||||||
@@ -27,10 +27,12 @@ export function DivisionList() {
|
|||||||
try {
|
try {
|
||||||
const { data } = await apiClient.GET("/api/division/");
|
const { data } = await apiClient.GET("/api/division/");
|
||||||
if (data?.data) {
|
if (data?.data) {
|
||||||
const mapped = data.data.map((div: { name: string; _count?: { activities: number } }) => ({
|
const mapped = data.data.map(
|
||||||
name: div.name,
|
(div: { name: string; _count?: { activities: number } }) => ({
|
||||||
count: div._count?.activities || 0,
|
name: div.name,
|
||||||
}));
|
count: div._count?.activities || 0,
|
||||||
|
}),
|
||||||
|
);
|
||||||
setDivisions(mapped);
|
setDivisions(mapped);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user