chore: fix linting and type safety across the project

This commit is contained in:
2026-03-26 15:51:45 +08:00
parent ec057ef2e5
commit 0d0dc187a5
46 changed files with 2461 additions and 312 deletions

View File

@@ -2,15 +2,21 @@
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
// Fungsi untuk mencari string terpanjang dalam objek (biasanya balasan AI) // Fungsi untuk mencari string terpanjang dalam objek (biasanya balasan AI)
function findLongestString(obj: any): string { function findLongestString(obj: unknown): string {
let longest = ""; let longest = "";
const search = (item: any) => { const search = (item: unknown) => {
if (typeof item === "string") { if (typeof item === "string") {
if (item.length > longest.length) longest = item; if (item.length > longest.length) {
longest = item;
}
} else if (Array.isArray(item)) { } else if (Array.isArray(item)) {
item.forEach(search); for (const child of item) {
} else if (item && typeof item === "object") { search(child);
Object.values(item).forEach(search); }
} else if (item !== null && typeof item === "object") {
for (const value of Object.values(item)) {
search(value);
}
} }
}; };
search(obj); search(obj);

View File

@@ -383,7 +383,17 @@ export interface operations {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content?: never; content: {
"application/json": {
ok: boolean;
};
"multipart/form-data": {
ok: boolean;
};
"text/plain": {
ok: boolean;
};
};
}; };
}; };
}; };
@@ -400,7 +410,17 @@ export interface operations {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content?: never; content: {
"application/json": {
data: unknown;
};
"multipart/form-data": {
data: unknown;
};
"text/plain": {
data: unknown;
};
};
}; };
}; };
}; };
@@ -894,7 +914,33 @@ export interface operations {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content?: never; content: {
"application/json": {
data: unknown[];
};
"multipart/form-data": {
data: unknown[];
};
"text/plain": {
data: unknown[];
};
};
};
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
"multipart/form-data": {
error: string;
};
"text/plain": {
error: string;
};
};
}; };
}; };
}; };
@@ -911,7 +957,33 @@ export interface operations {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content?: never; content: {
"application/json": {
data: unknown[];
};
"multipart/form-data": {
data: unknown[];
};
"text/plain": {
data: unknown[];
};
};
};
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
"multipart/form-data": {
error: string;
};
"text/plain": {
error: string;
};
};
}; };
}; };
}; };
@@ -928,7 +1000,33 @@ export interface operations {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content?: never; content: {
"application/json": {
data: unknown[];
};
"multipart/form-data": {
data: unknown[];
};
"text/plain": {
data: unknown[];
};
};
};
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
"multipart/form-data": {
error: string;
};
"text/plain": {
error: string;
};
};
}; };
}; };
}; };
@@ -945,7 +1043,48 @@ export interface operations {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content?: never; content: {
"application/json": {
data: {
total: number;
baru: number;
proses: number;
selesai: number;
};
};
"multipart/form-data": {
data: {
total: number;
baru: number;
proses: number;
selesai: number;
};
};
"text/plain": {
data: {
total: number;
baru: number;
proses: number;
selesai: number;
};
};
};
};
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
"multipart/form-data": {
error: string;
};
"text/plain": {
error: string;
};
};
}; };
}; };
}; };
@@ -962,7 +1101,33 @@ export interface operations {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content?: never; content: {
"application/json": {
data: unknown[];
};
"multipart/form-data": {
data: unknown[];
};
"text/plain": {
data: unknown[];
};
};
};
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
"multipart/form-data": {
error: string;
};
"text/plain": {
error: string;
};
};
}; };
}; };
}; };
@@ -979,7 +1144,33 @@ export interface operations {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content?: never; content: {
"application/json": {
data: unknown[];
};
"multipart/form-data": {
data: unknown[];
};
"text/plain": {
data: unknown[];
};
};
};
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
"multipart/form-data": {
error: string;
};
"text/plain": {
error: string;
};
};
}; };
}; };
}; };
@@ -996,7 +1187,33 @@ export interface operations {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content?: never; content: {
"application/json": {
data: unknown[];
};
"multipart/form-data": {
data: unknown[];
};
"text/plain": {
data: unknown[];
};
};
};
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
"multipart/form-data": {
error: string;
};
"text/plain": {
error: string;
};
};
}; };
}; };
}; };
@@ -1013,7 +1230,33 @@ export interface operations {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content?: never; content: {
"application/json": {
data: unknown[];
};
"multipart/form-data": {
data: unknown[];
};
"text/plain": {
data: unknown[];
};
};
};
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
"multipart/form-data": {
error: string;
};
"text/plain": {
error: string;
};
};
}; };
}; };
}; };
@@ -1030,7 +1273,39 @@ export interface operations {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content?: never; content: {
"application/json": {
data: {
count: number;
};
};
"multipart/form-data": {
data: {
count: number;
};
};
"text/plain": {
data: {
count: number;
};
};
};
};
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
"multipart/form-data": {
error: string;
};
"text/plain": {
error: string;
};
};
}; };
}; };
}; };
@@ -1047,7 +1322,45 @@ export interface operations {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content?: never; content: {
"application/json": {
data: {
total: number;
heads: number;
poor: number;
};
};
"multipart/form-data": {
data: {
total: number;
heads: number;
poor: number;
};
};
"text/plain": {
data: {
total: number;
heads: number;
poor: number;
};
};
};
};
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
"multipart/form-data": {
error: string;
};
"text/plain": {
error: string;
};
};
}; };
}; };
}; };
@@ -1064,7 +1377,33 @@ export interface operations {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content?: never; content: {
"application/json": {
data: unknown[];
};
"multipart/form-data": {
data: unknown[];
};
"text/plain": {
data: unknown[];
};
};
};
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
"multipart/form-data": {
error: string;
};
"text/plain": {
error: string;
};
};
}; };
}; };
}; };
@@ -1081,7 +1420,48 @@ export interface operations {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content?: never; content: {
"application/json": {
data: {
religion: unknown[];
gender: unknown[];
occupation: unknown[];
ageGroups: unknown[];
};
};
"multipart/form-data": {
data: {
religion: unknown[];
gender: unknown[];
occupation: unknown[];
ageGroups: unknown[];
};
};
"text/plain": {
data: {
religion: unknown[];
gender: unknown[];
occupation: unknown[];
ageGroups: unknown[];
};
};
};
};
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
"multipart/form-data": {
error: string;
};
"text/plain": {
error: string;
};
};
}; };
}; };
}; };
@@ -1098,7 +1478,33 @@ export interface operations {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content?: never; content: {
"application/json": {
data: unknown[];
};
"multipart/form-data": {
data: unknown[];
};
"text/plain": {
data: unknown[];
};
};
};
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
"multipart/form-data": {
error: string;
};
"text/plain": {
error: string;
};
};
}; };
}; };
}; };
@@ -1115,7 +1521,33 @@ export interface operations {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content?: never; content: {
"application/json": {
data: unknown[];
};
"multipart/form-data": {
data: unknown[];
};
"text/plain": {
data: unknown[];
};
};
};
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
"multipart/form-data": {
error: string;
};
"text/plain": {
error: string;
};
};
}; };
}; };
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -165,7 +165,7 @@ async function seedResidents(banjarIds: string[]) {
gender: Gender.LAKI_LAKI, gender: Gender.LAKI_LAKI,
religion: Religion.HINDU, religion: Religion.HINDU,
occupation: "Wiraswasta", occupation: "Wiraswasta",
banjarId: banjarIds[0], banjarId: banjarIds[0] || "",
rt: "001", rt: "001",
rw: "000", rw: "000",
address: "Jl. Raya Darmasaba No. 1", address: "Jl. Raya Darmasaba No. 1",
@@ -180,7 +180,7 @@ async function seedResidents(banjarIds: string[]) {
gender: Gender.PEREMPUAN, gender: Gender.PEREMPUAN,
religion: Religion.HINDU, religion: Religion.HINDU,
occupation: "Guru", occupation: "Guru",
banjarId: banjarIds[1], banjarId: banjarIds[1] || banjarIds[0] || "",
rt: "002", rt: "002",
rw: "000", rw: "000",
address: "Gg. Manesa No. 5", address: "Gg. Manesa No. 5",
@@ -203,7 +203,7 @@ async function seedActivities(divisionIds: string[]) {
{ {
title: "Rapat Koordinasi 2025", title: "Rapat Koordinasi 2025",
description: "Penyusunan rencana kerja tahunan", description: "Penyusunan rencana kerja tahunan",
divisionId: divisionIds[0], divisionId: divisionIds[0] || "",
progress: 100, progress: 100,
status: ActivityStatus.SELESAI, status: ActivityStatus.SELESAI,
priority: Priority.TINGGI, priority: Priority.TINGGI,
@@ -211,7 +211,7 @@ async function seedActivities(divisionIds: string[]) {
{ {
title: "Pemutakhiran Indeks Desa", title: "Pemutakhiran Indeks Desa",
description: "Pendataan SDG's Desa 2025", description: "Pendataan SDG's Desa 2025",
divisionId: divisionIds[0], divisionId: divisionIds[0] || "",
progress: 65, progress: 65,
status: ActivityStatus.BERJALAN, status: ActivityStatus.BERJALAN,
priority: Priority.SEDANG, priority: Priority.SEDANG,
@@ -219,7 +219,7 @@ async function seedActivities(divisionIds: string[]) {
{ {
title: "Pembangunan Jalan Banjar Cabe", title: "Pembangunan Jalan Banjar Cabe",
description: "Pengaspalan jalan utama", description: "Pengaspalan jalan utama",
divisionId: divisionIds[1], divisionId: divisionIds[1] || divisionIds[0] || "",
progress: 40, progress: 40,
status: ActivityStatus.BERJALAN, status: ActivityStatus.BERJALAN,
priority: Priority.DARURAT, priority: Priority.DARURAT,
@@ -296,7 +296,7 @@ async function seedEvents(adminId: string) {
} }
} }
async function main() { export async function runSeed() {
console.log("Starting seed..."); console.log("Starting seed...");
const adminId = await seedAdminUser(); const adminId = await seedAdminUser();
@@ -315,11 +315,13 @@ async function main() {
console.log("Seed finished successfully!"); console.log("Seed finished successfully!");
} }
main() if (import.meta.main) {
.catch((e) => { runSeed()
console.error(e); .catch((e) => {
process.exit(1); console.error(e);
}) process.exit(1);
.finally(async () => { })
await prisma.$disconnect(); .finally(async () => {
}); await prisma.$disconnect();
});
}

View File

@@ -1,4 +1,4 @@
import Elysia from "elysia"; import Elysia, { t } from "elysia";
import { prisma } from "../utils/db"; import { prisma } from "../utils/db";
import logger from "../utils/logger"; import logger from "../utils/logger";
@@ -23,6 +23,17 @@ export const complaint = new Elysia({
} }
}, },
{ {
response: {
200: t.Object({
data: t.Object({
total: t.Number(),
baru: t.Number(),
proses: t.Number(),
selesai: t.Number(),
}),
}),
500: t.Object({ error: t.String() }),
},
detail: { summary: "Get complaint statistics" }, detail: { summary: "Get complaint statistics" },
}, },
) )
@@ -42,6 +53,12 @@ export const complaint = new Elysia({
} }
}, },
{ {
response: {
200: t.Object({
data: t.Array(t.Any()),
}),
500: t.Object({ error: t.String() }),
},
detail: { summary: "Get recent complaints" }, detail: { summary: "Get recent complaints" },
}, },
) )
@@ -61,6 +78,12 @@ export const complaint = new Elysia({
} }
}, },
{ {
response: {
200: t.Object({
data: t.Array(t.Any()),
}),
500: t.Object({ error: t.String() }),
},
detail: { summary: "Get service letter statistics by type" }, detail: { summary: "Get service letter statistics by type" },
}, },
) )
@@ -80,6 +103,12 @@ export const complaint = new Elysia({
} }
}, },
{ {
response: {
200: t.Object({
data: t.Array(t.Any()),
}),
500: t.Object({ error: t.String() }),
},
detail: { summary: "Get recent innovation ideas" }, detail: { summary: "Get recent innovation ideas" },
}, },
) )
@@ -88,7 +117,9 @@ export const complaint = new Elysia({
async ({ set }) => { async ({ set }) => {
try { try {
// Get last 6 months trends for service letters // Get last 6 months trends for service letters
const trends = await prisma.$queryRaw<any[]>` const trends = await prisma.$queryRaw<
{ month: string; month_num: number; count: number }[]
>`
SELECT SELECT
TO_CHAR("createdAt", 'Mon') as month, TO_CHAR("createdAt", 'Mon') as month,
EXTRACT(MONTH FROM "createdAt") as month_num, EXTRACT(MONTH FROM "createdAt") as month_num,
@@ -106,6 +137,12 @@ export const complaint = new Elysia({
} }
}, },
{ {
response: {
200: t.Object({
data: t.Array(t.Any()),
}),
500: t.Object({ error: t.String() }),
},
detail: { summary: "Get service letter trends for last 6 months" }, detail: { summary: "Get service letter trends for last 6 months" },
}, },
) )
@@ -132,6 +169,14 @@ export const complaint = new Elysia({
} }
}, },
{ {
response: {
200: t.Object({
data: t.Object({
count: t.Number(),
}),
}),
500: t.Object({ error: t.String() }),
},
detail: { summary: "Get service letter count for current week" }, detail: { summary: "Get service letter count for current week" },
}, },
); );

View File

@@ -1,4 +1,4 @@
import Elysia from "elysia"; import Elysia, { t } from "elysia";
import { prisma } from "../utils/db"; import { prisma } from "../utils/db";
import logger from "../utils/logger"; import logger from "../utils/logger";
@@ -24,6 +24,12 @@ export const division = new Elysia({
} }
}, },
{ {
response: {
200: t.Object({
data: t.Array(t.Any()),
}),
500: t.Object({ error: t.String() }),
},
detail: { summary: "Get all divisions" }, detail: { summary: "Get all divisions" },
}, },
) )
@@ -48,6 +54,12 @@ export const division = new Elysia({
} }
}, },
{ {
response: {
200: t.Object({
data: t.Array(t.Any()),
}),
500: t.Object({ error: t.String() }),
},
detail: { summary: "Get recent activities" }, detail: { summary: "Get recent activities" },
}, },
) )
@@ -66,6 +78,12 @@ export const division = new Elysia({
} }
}, },
{ {
response: {
200: t.Object({
data: t.Array(t.Any()),
}),
500: t.Object({ error: t.String() }),
},
detail: { summary: "Get division performance metrics" }, detail: { summary: "Get division performance metrics" },
}, },
); );

View File

@@ -1,4 +1,4 @@
import Elysia from "elysia"; import Elysia, { t } from "elysia";
import { prisma } from "../utils/db"; import { prisma } from "../utils/db";
import logger from "../utils/logger"; import logger from "../utils/logger";
@@ -21,6 +21,12 @@ export const event = new Elysia({
} }
}, },
{ {
response: {
200: t.Object({
data: t.Array(t.Any()),
}),
500: t.Object({ error: t.String() }),
},
detail: { summary: "Get upcoming events" }, detail: { summary: "Get upcoming events" },
}, },
) )
@@ -49,6 +55,12 @@ export const event = new Elysia({
} }
}, },
{ {
response: {
200: t.Object({
data: t.Array(t.Any()),
}),
500: t.Object({ error: t.String() }),
},
detail: { summary: "Get events for today" }, detail: { summary: "Get events for today" },
}, },
); );

View File

@@ -1,6 +1,6 @@
import { cors } from "@elysiajs/cors"; import { cors } from "@elysiajs/cors";
import { swagger } from "@elysiajs/swagger"; import { swagger } from "@elysiajs/swagger";
import Elysia from "elysia"; import Elysia, { t } from "elysia";
import { apiMiddleware } from "../middleware/apiMiddleware"; import { apiMiddleware } from "../middleware/apiMiddleware";
import { auth } from "../utils/auth"; import { auth } from "../utils/auth";
import { apikey } from "./apikey"; import { apikey } from "./apikey";
@@ -16,12 +16,24 @@ const api = new Elysia({
prefix: "/api", prefix: "/api",
}) })
.use(cors()) .use(cors())
.get("/health", () => ({ ok: true })) .get("/health", () => ({ ok: true }), {
.all("/auth/*", ({ request }) => auth.handler(request)) response: {
.get("/session", async ({ request }) => { 200: t.Object({ ok: t.Boolean() }),
const data = await auth.api.getSession({ headers: request.headers }); },
return { data };
}) })
.all("/auth/*", ({ request }) => auth.handler(request))
.get(
"/session",
async ({ request }) => {
const data = await auth.api.getSession({ headers: request.headers });
return { data };
},
{
response: {
200: t.Object({ data: t.Any() }),
},
},
)
.use(apiMiddleware) .use(apiMiddleware)
.use(apikey) .use(apikey)
.use(profile) .use(profile)

View File

@@ -1,80 +1,69 @@
import Elysia, { t } from "elysia"; import Elysia, { t } from "elysia";
import { apiMiddleware } from "../middleware/apiMiddleware";
import { prisma } from "../utils/db"; import { prisma } from "../utils/db";
import logger from "../utils/logger"; import logger from "../utils/logger";
interface AuthenticatedUser {
id: string;
email: string;
name?: string | null;
}
export const profile = new Elysia({ export const profile = new Elysia({
prefix: "/profile", prefix: "/profile",
}).post( })
"/update", .use(apiMiddleware)
async ({ .post(
body, "/update",
set, async ({ body, set, user }) => {
user, try {
}: { if (!user) {
body: { name?: string; image?: string }; set.status = 401;
set: any; return { error: "Unauthorized" };
user?: AuthenticatedUser; }
}) => {
try { const { name, image } = body;
if (!user) {
set.status = 401; const updatedUser = await prisma.user.update({
return { error: "Unauthorized" }; where: { id: user.id },
data: {
name: name || undefined,
image: image || undefined,
},
select: {
id: true,
name: true,
email: true,
image: true,
role: true,
},
});
logger.info({ userId: user.id }, "Profile updated successfully");
return { user: updatedUser };
} catch (error) {
logger.error({ error, userId: user?.id }, "Failed to update profile");
set.status = 500;
return { error: "Failed to update profile" };
} }
},
const { name, image } = body; {
body: t.Object({
const updatedUser = await prisma.user.update({ name: t.Optional(t.String()),
where: { id: user.id }, image: t.Optional(t.String()),
data: {
name: name || undefined,
image: image || undefined,
},
select: {
id: true,
name: true,
email: true,
image: true,
role: true,
},
});
logger.info({ userId: user.id }, "Profile updated successfully");
return { user: updatedUser };
} catch (error) {
logger.error({ error, userId: user?.id }, "Failed to update profile");
set.status = 500;
return { error: "Failed to update profile" };
}
},
{
body: t.Object({
name: t.Optional(t.String()),
image: t.Optional(t.String()),
}),
response: {
200: t.Object({
user: t.Object({
id: t.String(),
name: t.Any(),
email: t.String(),
image: t.Any(),
role: t.Any(),
}),
}), }),
401: t.Object({ error: t.String() }), response: {
500: t.Object({ error: t.String() }), 200: t.Object({
}, user: t.Object({
id: t.String(),
name: t.Any(),
email: t.String(),
image: t.Any(),
role: t.Any(),
}),
}),
401: t.Object({ error: t.String() }),
500: t.Object({ error: t.String() }),
},
detail: { detail: {
summary: "Update user profile", summary: "Update user profile",
description: "Update the authenticated user's name or profile image", description: "Update the authenticated user's name or profile image",
},
}, },
}, );
);

View File

@@ -1,4 +1,4 @@
import Elysia from "elysia"; import Elysia, { t } from "elysia";
import { prisma } from "../utils/db"; import { prisma } from "../utils/db";
import logger from "../utils/logger"; import logger from "../utils/logger";
@@ -22,6 +22,16 @@ export const resident = new Elysia({
} }
}, },
{ {
response: {
200: t.Object({
data: t.Object({
total: t.Number(),
heads: t.Number(),
poor: t.Number(),
}),
}),
500: t.Object({ error: t.String() }),
},
detail: { summary: "Get resident statistics" }, detail: { summary: "Get resident statistics" },
}, },
) )
@@ -46,6 +56,12 @@ export const resident = new Elysia({
} }
}, },
{ {
response: {
200: t.Object({
data: t.Array(t.Any()),
}),
500: t.Object({ error: t.String() }),
},
detail: { summary: "Get population data per banjar" }, detail: { summary: "Get population data per banjar" },
}, },
) )
@@ -69,7 +85,7 @@ export const resident = new Elysia({
take: 10, take: 10,
}), }),
// Group by age ranges (simplified calculation) // Group by age ranges (simplified calculation)
prisma.$queryRaw<any[]>` prisma.$queryRaw<{ range: string; count: number }[]>`
SELECT SELECT
CASE CASE
WHEN date_part('year', age(now(), "birthDate")) BETWEEN 0 AND 16 THEN '0-16' WHEN date_part('year', age(now(), "birthDate")) BETWEEN 0 AND 16 THEN '0-16'
@@ -94,6 +110,17 @@ export const resident = new Elysia({
} }
}, },
{ {
response: {
200: t.Object({
data: t.Object({
religion: t.Array(t.Any()),
gender: t.Array(t.Any()),
occupation: t.Array(t.Any()),
ageGroups: t.Array(t.Any()),
}),
}),
500: t.Object({ error: t.String() }),
},
detail: { detail: {
summary: summary:
"Get demographics including religion, gender, occupation and age", "Get demographics including religion, gender, occupation and age",

View File

@@ -53,18 +53,22 @@ export function DashboardContent() {
); );
setStats({ setStats({
complaints: (complaintRes.data as any)?.data || { complaints: (complaintRes.data as { data: typeof stats.complaints })
?.data || {
total: 0, total: 0,
baru: 0, baru: 0,
proses: 0, proses: 0,
selesai: 0, selesai: 0,
}, },
residents: (residentRes.data as any)?.data || { residents: (residentRes.data as { data: typeof stats.residents })
?.data || {
total: 0, total: 0,
heads: 0, heads: 0,
poor: 0, poor: 0,
}, },
weeklyService: (weeklyServiceRes.data as any)?.data?.count || 0, weeklyService:
(weeklyServiceRes.data as { data: { count: number } })?.data
?.count || 0,
loading: false, loading: false,
}); });
} catch (error) { } catch (error) {

View File

@@ -31,10 +31,12 @@ export function ActivityList() {
const res = await apiClient.GET("/api/event/"); const res = await apiClient.GET("/api/event/");
if (res.data?.data) { if (res.data?.data) {
setData( setData(
(res.data.data as any[]).map((e) => ({ (res.data.data as { startDate: string; title: string }[]).map(
date: dayjs(e.startDate).format("D MMMM YYYY"), (e) => ({
title: e.title, date: dayjs(e.startDate).format("D MMMM YYYY"),
})), title: e.title,
}),
),
); );
} }
} catch (error) { } catch (error) {

View File

@@ -55,7 +55,10 @@ export function ChartAPBDes() {
<XAxis type="number" hide domain={[0, 100]} /> <XAxis type="number" hide domain={[0, 100]} />
<YAxis type="category" hide dataKey="name" /> <YAxis type="category" hide dataKey="name" />
<Tooltip <Tooltip
formatter={(value: number) => [`${value}%`, ""]} formatter={(value: number | string | undefined) => [
`${value}%`,
"",
]}
contentStyle={{ contentStyle={{
backgroundColor: dark ? "#1E293B" : "white", backgroundColor: dark ? "#1E293B" : "white",
borderColor: dark ? "#334155" : "#e5e7eb", borderColor: dark ? "#334155" : "#e5e7eb",

View File

@@ -20,11 +20,16 @@ import {
} from "recharts"; } from "recharts";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
interface ChartData {
month: string;
value: number;
}
export function ChartSurat() { export function ChartSurat() {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark"; const dark = colorScheme === "dark";
const [data, setData] = useState<any[]>([]); const [data, setData] = useState<ChartData[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
@@ -33,7 +38,7 @@ export function ChartSurat() {
const res = await apiClient.GET("/api/complaint/service-trends"); const res = await apiClient.GET("/api/complaint/service-trends");
if (res.data?.data) { if (res.data?.data) {
setData( setData(
(res.data.data as any[]).map((d) => ({ (res.data.data as { month: string; count: number }[]).map((d) => ({
month: d.month, month: d.month,
value: Number(d.count), value: Number(d.count),
})), })),
@@ -79,7 +84,10 @@ export function ChartSurat() {
viewBox="0 0 20 20" viewBox="0 0 20 20"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
role="img"
aria-label="Tampilkan Detail"
> >
<title>Tampilkan Detail</title>
<path <path
d="M8 5L13 10L8 15" d="M8 5L13 10L8 15"
stroke="currentColor" stroke="currentColor"

View File

@@ -17,6 +17,14 @@ interface DivisionData {
value: number; value: number;
} }
interface DivisionApiResponse {
id: string;
name: string;
_count?: {
activities: number;
};
}
export function DivisionProgress() { export function DivisionProgress() {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark"; const dark = colorScheme === "dark";
@@ -30,7 +38,7 @@ export function DivisionProgress() {
const res = await apiClient.GET("/api/division/"); const res = await apiClient.GET("/api/division/");
if (res.data?.data) { if (res.data?.data) {
setData( setData(
(res.data.data as any[]).map((d) => ({ (res.data.data as DivisionApiResponse[]).map((d) => ({
name: d.name, name: d.name,
value: d._count?.activities || 0, value: d._count?.activities || 0,
})), })),

View File

@@ -1,12 +1,9 @@
import { import {
Badge,
Box, Box,
Card, Card,
Grid, Grid,
GridCol,
Group, Group,
Loader, Loader,
Progress,
Stack, Stack,
Text, Text,
ThemeIcon, ThemeIcon,
@@ -45,6 +42,49 @@ const sektorUnggulanData = [
{ sektor: "Jasa", value: 52 }, { sektor: "Jasa", value: 52 },
]; ];
interface AgeData {
ageRange: string;
total: number;
}
interface JobData {
job: string;
total: number;
}
interface ReligionData {
name: string;
value: number;
color: string;
}
interface BanjarData {
id: string;
name: string;
totalPopulation: number;
totalKK: number;
totalPoor: number;
}
interface ReligionResponse {
religion: string;
_count: {
_all: number;
};
}
interface OccupationResponse {
occupation: string | null;
_count: {
_all: number;
};
}
interface AgeGroupResponse {
range: string;
count: number | string;
}
const DemografiPekerjaan = () => { const DemografiPekerjaan = () => {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark"; const dark = colorScheme === "dark";
@@ -54,10 +94,10 @@ const DemografiPekerjaan = () => {
heads: 0, heads: 0,
poor: 0, poor: 0,
}); });
const [ageData, setAgeData] = useState<any[]>([]); const [ageData, setAgeData] = useState<AgeData[]>([]);
const [jobData, setJobData] = useState<any[]>([]); const [jobData, setJobData] = useState<JobData[]>([]);
const [religionData, setReligionData] = useState<any[]>([]); const [religionData, setReligionData] = useState<ReligionData[]>([]);
const [banjarData, setBanjarData] = useState<any[]>([]); const [banjarData, setBanjarData] = useState<BanjarData[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
@@ -70,7 +110,8 @@ const DemografiPekerjaan = () => {
]); ]);
if (statsRes.data?.data) setStats(statsRes.data.data); if (statsRes.data?.data) setStats(statsRes.data.data);
if (banjarRes.data?.data) setBanjarData(banjarRes.data.data); if (banjarRes.data?.data)
setBanjarData(banjarRes.data.data as BanjarData[]);
if (demoRes.data?.data) { if (demoRes.data?.data) {
const { religion, occupation, ageGroups } = demoRes.data.data; const { religion, occupation, ageGroups } = demoRes.data.data;
@@ -85,7 +126,7 @@ const DemografiPekerjaan = () => {
}; };
setReligionData( setReligionData(
(religion as any[]).map((r) => ({ (religion as ReligionResponse[]).map((r) => ({
name: r.religion, name: r.religion,
value: r._count._all, value: r._count._all,
color: religionColors[r.religion] || "#94A3B8", color: religionColors[r.religion] || "#94A3B8",
@@ -93,14 +134,14 @@ const DemografiPekerjaan = () => {
); );
setJobData( setJobData(
(occupation as any[]).map((o) => ({ (occupation as OccupationResponse[]).map((o) => ({
job: o.occupation || "Lainnya", job: o.occupation || "Lainnya",
total: o._count._all, total: o._count._all,
})), })),
); );
setAgeData( setAgeData(
(ageGroups as any[]).map((a) => ({ (ageGroups as AgeGroupResponse[]).map((a) => ({
ageRange: a.range, ageRange: a.range,
total: Number(a.count), total: Number(a.count),
})), })),
@@ -401,8 +442,8 @@ const DemografiPekerjaan = () => {
</Title> </Title>
</Group> </Group>
<Grid gutter="sm"> <Grid gutter="sm">
{dynamicStats.map((stat, index) => ( {dynamicStats.map((stat) => (
<Grid.Col key={index} span={6}> <Grid.Col key={stat.title} span={6}>
<Card <Card
p="sm" p="sm"
radius="lg" radius="lg"
@@ -480,8 +521,8 @@ const DemografiPekerjaan = () => {
paddingAngle={2} paddingAngle={2}
dataKey="value" dataKey="value"
> >
{religionData.map((entry, index) => ( {religionData.map((entry) => (
<Cell key={`cell-${index}`} fill={entry.color} /> <Cell key={`cell-${entry.name}`} fill={entry.color} />
))} ))}
</Pie> </Pie>
<Tooltip <Tooltip
@@ -496,8 +537,8 @@ const DemografiPekerjaan = () => {
</ResponsiveContainer> </ResponsiveContainer>
<Stack gap="xs" mt="md"> <Stack gap="xs" mt="md">
{!loading && {!loading &&
religionData.map((item, index) => ( religionData.map((item) => (
<Group key={index} justify="space-between"> <Group key={item.name} justify="space-between">
<Group gap="xs"> <Group gap="xs">
<Box <Box
w={10} w={10}
@@ -601,12 +642,12 @@ const DemografiPekerjaan = () => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{banjarData.map((item, index) => ( {banjarData.map((item) => (
<tr <tr
key={item.id || index} key={item.id}
style={{ style={{
backgroundColor: backgroundColor:
index % 2 === 0 banjarData.indexOf(item) % 2 === 0
? dark ? dark
? "#334155" ? "#334155"
: "#F8FAFC" : "#F8FAFC"
@@ -725,8 +766,8 @@ const DemografiPekerjaan = () => {
radius={[0, 8, 8, 0]} radius={[0, 8, 8, 0]}
maxBarSize={40} maxBarSize={40}
> >
{sektorUnggulanData.map((entry, index) => ( {sektorUnggulanData.map((entry) => (
<Cell key={`cell-${index}`} fill="#396aaaff" /> <Cell key={`cell-${entry.sektor}`} fill="#396aaaff" />
))} ))}
</Bar> </Bar>
</BarChart> </BarChart>

View File

@@ -129,7 +129,6 @@ export function DevInspector({ children }: { children: React.ReactNode }) {
tt.style.left = `${rect.left + window.scrollX}px`; tt.style.left = `${rect.left + window.scrollX}px`;
}, []); }, []);
// biome-ignore lint/correctness/useExhaustiveDependencies: updateOverlay is stable
useEffect(() => { useEffect(() => {
if (!active) return; if (!active) return;

View File

@@ -23,10 +23,10 @@ export function ImageWithFallback(
<div className="flex items-center justify-center w-full h-full"> <div className="flex items-center justify-center w-full h-full">
<img <img
src={ERROR_IMG_SRC} src={ERROR_IMG_SRC}
alt="Error loading image" alt="Error loading content"
{...rest} {...rest}
data-original-url={src} data-original-url={src}
/> />{" "}
</div> </div>
</div> </div>
) : ( ) : (

View File

@@ -3,7 +3,6 @@ import {
Box, Box,
Card, Card,
Grid, Grid,
GridCol,
Group, Group,
Stack, Stack,
Text, Text,
@@ -354,8 +353,8 @@ const KeuanganAnggaran = () => {
Pendapatan Pendapatan
</Title> </Title>
<Stack gap="xs"> <Stack gap="xs">
{apbdReport.income.map((item, index) => ( {apbdReport.income.map((item) => (
<Group key={index} justify="space-between"> <Group key={item.category} justify="space-between">
<Text size="sm" c={dark ? "gray.3" : "gray.7"}> <Text size="sm" c={dark ? "gray.3" : "gray.7"}>
{item.category} {item.category}
</Text> </Text>
@@ -390,8 +389,8 @@ const KeuanganAnggaran = () => {
Belanja Belanja
</Title> </Title>
<Stack gap="xs"> <Stack gap="xs">
{apbdReport.expenses.map((item, index) => ( {apbdReport.expenses.map((item) => (
<Group key={index} justify="space-between"> <Group key={item.category} justify="space-between">
<Text size="sm" c={dark ? "gray.3" : "gray.7"}> <Text size="sm" c={dark ? "gray.3" : "gray.7"}>
{item.category} {item.category}
</Text> </Text>
@@ -473,9 +472,9 @@ const KeuanganAnggaran = () => {
</Title> </Title>
</Group> </Group>
<Stack gap="sm"> <Stack gap="sm">
{assistanceFundData.map((fund, index) => ( {assistanceFundData.map((fund) => (
<Card <Card
key={index} key={fund.source}
p="sm" p="sm"
radius="lg" radius="lg"
bg={dark ? "#334155" : "#F1F5F9"} bg={dark ? "#334155" : "#F1F5F9"}

View File

@@ -18,9 +18,23 @@ const archiveData = [
{ name: "Notulensi Rapat" }, { name: "Notulensi Rapat" },
]; ];
interface Activity {
id: string;
title: string;
createdAt: string;
progress: number;
status: "SELESAI" | "BERJALAN" | "TERTUNDA";
}
interface EventData {
id: string;
title: string;
startDate: string;
}
const KinerjaDivisi = () => { const KinerjaDivisi = () => {
const [activities, setActivities] = useState<any[]>([]); const [activities, setActivities] = useState<Activity[]>([]);
const [todayEvents, setTodayEvents] = useState<any[]>([]); const [todayEvents, setTodayEvents] = useState<EventData[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
@@ -32,10 +46,10 @@ const KinerjaDivisi = () => {
]); ]);
if (activityRes.data?.data) { if (activityRes.data?.data) {
setActivities(activityRes.data.data); setActivities(activityRes.data.data as Activity[]);
} }
if (eventRes.data?.data) { if (eventRes.data?.data) {
setTodayEvents(eventRes.data.data); setTodayEvents(eventRes.data.data as EventData[]);
} }
} catch (error) { } catch (error) {
console.error("Failed to fetch kinerja divisi data", error); console.error("Failed to fetch kinerja divisi data", error);
@@ -57,11 +71,8 @@ const KinerjaDivisi = () => {
<Stack gap="lg"> <Stack gap="lg">
{/* SECTION 1 — PROGRAM KEGIATAN */} {/* SECTION 1 — PROGRAM KEGIATAN */}
<Grid gutter="md"> <Grid gutter="md">
{activities.slice(0, 4).map((kegiatan, index) => ( {activities.slice(0, 4).map((kegiatan) => (
<Grid.Col <Grid.Col key={kegiatan.id} span={{ base: 12, md: 6, lg: 3 }}>
key={kegiatan.id || index}
span={{ base: 12, md: 6, lg: 3 }}
>
<ActivityCard <ActivityCard
title={kegiatan.title} title={kegiatan.title}
date={dayjs(kegiatan.createdAt).format("D MMMM YYYY")} date={dayjs(kegiatan.createdAt).format("D MMMM YYYY")}
@@ -111,8 +122,8 @@ const KinerjaDivisi = () => {
{/* SECTION 5 — ARSIP DIGITAL PERANGKAT DESA */} {/* SECTION 5 — ARSIP DIGITAL PERANGKAT DESA */}
<Grid gutter="md"> <Grid gutter="md">
{archiveData.map((item, index) => ( {archiveData.map((item) => (
<Grid.Col key={index} span={{ base: 12, md: 6 }}> <Grid.Col key={item.name} span={{ base: 12, md: 6 }}>
<ArchiveCard item={item} /> <ArchiveCard item={item} />
</Grid.Col> </Grid.Col>
))} ))}

View File

@@ -1,11 +1,4 @@
import { import { Card, Group, Stack, Text, useMantineColorScheme } from "@mantine/core";
Box,
Card,
Group,
Stack,
Text,
useMantineColorScheme,
} from "@mantine/core";
import { MessageCircle } from "lucide-react"; import { MessageCircle } from "lucide-react";
interface DiscussionItem { interface DiscussionItem {
@@ -57,9 +50,9 @@ export function DiscussionPanel() {
</Text> </Text>
</Group> </Group>
<Stack gap="sm"> <Stack gap="sm">
{discussions.map((discussion, index) => ( {discussions.map((discussion) => (
<Card <Card
key={index} key={`${discussion.sender}-${discussion.date}`}
p="sm" p="sm"
radius="md" radius="md"
withBorder withBorder

View File

@@ -27,12 +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( const mapped = (
(div: { name: string; _count?: { activities: number } }) => ({ data.data as { name: string; _count?: { activities: number } }[]
name: div.name, ).map((div) => ({
count: div._count?.activities || 0, name: div.name,
}), count: div._count?.activities || 0,
); }));
setDivisions(mapped); setDivisions(mapped);
} }
} catch (error) { } catch (error) {
@@ -68,9 +68,9 @@ export function DivisionList() {
<Loader size="sm" /> <Loader size="sm" />
</Group> </Group>
) : divisions.length > 0 ? ( ) : divisions.length > 0 ? (
divisions.map((division, index) => ( divisions.map((division) => (
<Group <Group
key={index} key={division.name}
justify="space-between" justify="space-between"
align="center" align="center"
style={{ style={{

View File

@@ -63,8 +63,8 @@ export function DocumentChart() {
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }} labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
/> />
<Bar dataKey="jumlah" radius={[4, 4, 0, 0]}> <Bar dataKey="jumlah" radius={[4, 4, 0, 0]}>
{documentData.map((entry, index) => ( {documentData.map((entry) => (
<Cell key={`cell-${index}`} fill={entry.color} /> <Cell key={`cell-${entry.name}`} fill={entry.color} />
))} ))}
</Bar> </Bar>
</BarChart> </BarChart>

View File

@@ -43,8 +43,12 @@ export function EventCard({ agendas = [] }: EventCardProps) {
</Group> </Group>
{agendas.length > 0 ? ( {agendas.length > 0 ? (
<Stack gap="sm"> <Stack gap="sm">
{agendas.map((agenda, index) => ( {agendas.map((agenda) => (
<Group key={index} align="flex-start" gap="md"> <Group
key={`${agenda.time}-${agenda.event}`}
align="flex-start"
gap="md"
>
<Box w={60}> <Box w={60}>
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"}> <Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"}>
{agenda.time} {agenda.time}

View File

@@ -47,8 +47,8 @@ export function ProgressChart() {
paddingAngle={2} paddingAngle={2}
dataKey="value" dataKey="value"
> >
{progressData.map((entry, index) => ( {progressData.map((entry) => (
<Cell key={`cell-${index}`} fill={entry.color} /> <Cell key={`cell-${entry.name}`} fill={entry.color} />
))} ))}
</Pie> </Pie>
<Tooltip <Tooltip
@@ -61,8 +61,8 @@ export function ProgressChart() {
</PieChart> </PieChart>
</ResponsiveContainer> </ResponsiveContainer>
<Stack gap="xs" mt="md"> <Stack gap="xs" mt="md">
{progressData.map((item, index) => ( {progressData.map((item) => (
<Group key={index} justify="space-between"> <Group key={item.name} justify="space-between">
<Group gap="xs"> <Group gap="xs">
<Box <Box
w={12} w={12}

View File

@@ -57,6 +57,26 @@ const ideInovatif = [
}, },
]; ];
interface Complaint {
id: string;
title: string;
category: string;
status: string;
createdAt: string;
}
interface ServiceStat {
jenis: string;
jumlah: number;
}
interface ServiceApiResponse {
letterType: string;
_count: {
_all: number;
};
}
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
switch (status.toLowerCase()) { switch (status.toLowerCase()) {
case "baru": case "baru":
@@ -81,8 +101,8 @@ const PengaduanLayananPublik = () => {
proses: 0, proses: 0,
selesai: 0, selesai: 0,
}); });
const [recentComplaints, setRecentComplaints] = useState<any[]>([]); const [recentComplaints, setRecentComplaints] = useState<Complaint[]>([]);
const [serviceStats, setServiceStats] = useState<any[]>([]); const [serviceStats, setServiceStats] = useState<ServiceStat[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
@@ -95,9 +115,12 @@ const PengaduanLayananPublik = () => {
]); ]);
if (statsRes.data?.data) setStats(statsRes.data.data); if (statsRes.data?.data) setStats(statsRes.data.data);
if (recentRes.data?.data) setRecentComplaints(recentRes.data.data); if (recentRes.data?.data)
setRecentComplaints(recentRes.data.data as Complaint[]);
if (serviceRes.data?.data) { if (serviceRes.data?.data) {
const mappedService = serviceRes.data.data.map((item: any) => ({ const mappedService = (
serviceRes.data.data as ServiceApiResponse[]
).map((item) => ({
jenis: item.letterType, jenis: item.letterType,
jumlah: item._count?._all || 0, jumlah: item._count?._all || 0,
})); }));
@@ -148,8 +171,8 @@ const PengaduanLayananPublik = () => {
<Stack gap="lg"> <Stack gap="lg">
{/* TOP SECTION - 4 STAT CARDS */} {/* TOP SECTION - 4 STAT CARDS */}
<Grid gutter="md"> <Grid gutter="md">
{summaryData.map((item, index) => ( {summaryData.map((item) => (
<Grid.Col key={index} span={{ base: 12, sm: 6, lg: 3 }}> <Grid.Col key={item.title} span={{ base: 12, sm: 6, lg: 3 }}>
<Card <Card
p="md" p="md"
radius="xl" radius="xl"
@@ -330,9 +353,9 @@ const PengaduanLayananPublik = () => {
<Loader /> <Loader />
</Group> </Group>
) : recentComplaints.length > 0 ? ( ) : recentComplaints.length > 0 ? (
recentComplaints.map((item, index) => ( recentComplaints.map((item) => (
<Card <Card
key={item.id || index} key={item.id}
p="sm" p="sm"
radius="md" radius="md"
withBorder withBorder
@@ -392,9 +415,9 @@ const PengaduanLayananPublik = () => {
Ajuan Ide Inovatif Ajuan Ide Inovatif
</Title> </Title>
<Stack gap="sm"> <Stack gap="sm">
{ideInovatif.map((item, index) => ( {ideInovatif.map((item) => (
<Card <Card
key={index} key={item.judul}
p="sm" p="sm"
radius="md" radius="md"
withBorder withBorder

View File

@@ -1,24 +1,18 @@
import { import {
Alert,
Box,
Button, Button,
Card,
Checkbox,
Grid, Grid,
GridCol, GridCol,
Group, Group,
Space,
Stack, Stack,
Switch, Switch,
Text, Text,
Title, Title,
useMantineColorScheme, useMantineColorScheme,
} from "@mantine/core"; } from "@mantine/core";
import { IconInfoCircle } from "@tabler/icons-react";
const NotifikasiSettings = () => { const NotifikasiSettings = () => {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark"; const _dark = colorScheme === "dark";
return ( return (
<Stack pr={"20%"} gap={"xs"}> <Stack pr={"20%"} gap={"xs"}>
<Grid gutter={{ base: 5, xs: "md", md: "xl", xl: 50 }}> <Grid gutter={{ base: 5, xs: "md", md: "xl", xl: 50 }}>

View File

@@ -1,13 +1,10 @@
import { import {
Badge,
Box, Box,
Collapse, Collapse,
Group,
Image, Image,
Input, Input,
NavLink as MantineNavLink, NavLink as MantineNavLink,
Stack, Stack,
Text,
useMantineColorScheme, useMantineColorScheme,
} from "@mantine/core"; } from "@mantine/core";
import { useLocation, useNavigate } from "@tanstack/react-router"; import { useLocation, useNavigate } from "@tanstack/react-router";
@@ -80,11 +77,11 @@ export function Sidebar({ className }: SidebarProps) {
{/* Menu Items */} {/* Menu Items */}
<Stack gap={0} px="xs" style={{ overflowY: "auto" }}> <Stack gap={0} px="xs" style={{ overflowY: "auto" }}>
{menuItems.map((item, index) => { {menuItems.map((item) => {
const isActive = location.pathname === item.path; const isActive = location.pathname === item.path;
return ( return (
<MantineNavLink <MantineNavLink
key={index} key={item.path}
onClick={() => navigate({ to: item.path })} onClick={() => navigate({ to: item.path })}
label={item.name} label={item.name}
active={isActive} active={isActive}
@@ -146,11 +143,11 @@ export function Sidebar({ className }: SidebarProps) {
ml="lg" ml="lg"
style={{ overflowY: "auto", maxHeight: "200px" }} style={{ overflowY: "auto", maxHeight: "200px" }}
> >
{settingsItems.map((item, index) => { {settingsItems.map((item) => {
const isActive = location.pathname === item.path; const isActive = location.pathname === item.path;
return ( return (
<MantineNavLink <MantineNavLink
key={index} key={item.path}
onClick={() => navigate({ to: item.path })} onClick={() => navigate({ to: item.path })}
label={item.name} label={item.name}
active={isActive} active={isActive}

View File

@@ -4,7 +4,6 @@ import {
Stack, Stack,
Text, Text,
ThemeIcon, ThemeIcon,
Title,
useMantineColorScheme, useMantineColorScheme,
} from "@mantine/core"; } from "@mantine/core";
import { IconAward } from "@tabler/icons-react"; import { IconAward } from "@tabler/icons-react";

View File

@@ -49,8 +49,8 @@ export const HealthStats = ({ data }: HealthStatsProps) => {
Statistik Kesehatan Statistik Kesehatan
</Title> </Title>
<Stack gap="md"> <Stack gap="md">
{displayData.map((item, index) => ( {displayData.map((item) => (
<div key={index}> <div key={item.label}>
<Group justify="space-between" mb={5}> <Group justify="space-between" mb={5}>
<Text size="sm" fw={500} c={dark ? "dark.0" : "#1e3a5f"}> <Text size="sm" fw={500} c={dark ? "dark.0" : "#1e3a5f"}>
{item.label} {item.label}

View File

@@ -53,8 +53,6 @@ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return ( return (
<span <span
data-slot="breadcrumb-page" data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page" aria-current="page"
className={cn("text-foreground font-normal", className)} className={cn("text-foreground font-normal", className)}
{...props} {...props}

View File

@@ -46,7 +46,6 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
mantineVariant = "transparent"; mantineVariant = "transparent";
mantineColor = "blue"; // Assuming primary maps to blue in Mantine for now mantineColor = "blue"; // Assuming primary maps to blue in Mantine for now
break; break;
case "default":
default: default:
mantineVariant = "filled"; mantineVariant = "filled";
mantineColor = "blue"; // Assuming primary maps to blue in Mantine for now mantineColor = "blue"; // Assuming primary maps to blue in Mantine for now

View File

@@ -117,16 +117,16 @@ function Carousel({
canScrollNext, canScrollNext,
}} }}
> >
<div {/* biome-ignore lint/a11y/useAriaPropsSupportedByRole: section with aria-roledescription is standard for carousels. */}
<section
onKeyDownCapture={handleKeyDown} onKeyDownCapture={handleKeyDown}
className={cn("relative", className)} className={cn("relative", className)}
role="region"
aria-roledescription="carousel" aria-roledescription="carousel"
data-slot="carousel" data-slot="carousel"
{...props} {...props}
> >
{children} {children}
</div> </section>{" "}
</CarouselContext.Provider> </CarouselContext.Provider>
); );
} }
@@ -156,6 +156,7 @@ function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel(); const { orientation } = useCarousel();
return ( return (
// biome-ignore lint/a11y/useSemanticElements: role='group' and aria-roledescription='slide' is standard for carousel slides.
<div <div
role="group" role="group"
aria-roledescription="slide" aria-roledescription="slide"

View File

@@ -80,6 +80,7 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
return ( return (
<style <style
// biome-ignore lint/security/noDangerouslySetInnerHtml: This is a safe use of dangerouslySetInnerHTML for generating dynamic CSS variables for charts.
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: Object.entries(THEMES) __html: Object.entries(THEMES)
.map( .map(
@@ -125,6 +126,11 @@ function ChartTooltipContent({
indicator?: "line" | "dot" | "dashed"; indicator?: "line" | "dot" | "dashed";
nameKey?: string; nameKey?: string;
labelKey?: string; labelKey?: string;
// biome-ignore lint/suspicious/noExplicitAny: Recharts payload is complex and better handled as any[] for this wrapper.
payload?: any[];
// biome-ignore lint/suspicious/noExplicitAny: Recharts label can be any type.
label?: any;
active?: boolean;
}) { }) {
const { config } = useChart(); const { config } = useChart();
@@ -257,9 +263,11 @@ function ChartLegendContent({
verticalAlign = "bottom", verticalAlign = "bottom",
nameKey, nameKey,
}: React.ComponentProps<"div"> & }: React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & { Pick<RechartsPrimitive.LegendProps, "verticalAlign"> & {
hideIcon?: boolean; hideIcon?: boolean;
nameKey?: string; nameKey?: string;
// biome-ignore lint/suspicious/noExplicitAny: Recharts legend payload.
payload?: any[];
}) { }) {
const { config } = useChart(); const { config } = useChart();

View File

@@ -68,7 +68,7 @@ function InputOTPSlot({
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) { function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
return ( return (
<div data-slot="input-otp-separator" role="separator" {...props}> <div data-slot="input-otp-separator" {...props}>
<MinusIcon /> <MinusIcon />
</div> </div>
); );

View File

@@ -4,7 +4,6 @@ import {
MoreHorizontalIcon, MoreHorizontalIcon,
} from "lucide-react"; } from "lucide-react";
import type * as React from "react"; import type * as React from "react";
import { Button } from "./button";
import { cn } from "./utils"; import { cn } from "./utils";
const baseClasses = const baseClasses =
@@ -13,7 +12,6 @@ const baseClasses =
function Pagination({ className, ...props }: React.ComponentProps<"nav">) { function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return ( return (
<nav <nav
role="navigation"
aria-label="pagination" aria-label="pagination"
data-slot="pagination" data-slot="pagination"
className={cn("mx-auto flex w-full justify-center", className)} className={cn("mx-auto flex w-full justify-center", className)}

View File

@@ -2,7 +2,6 @@ import {
Progress as MantineProgress, Progress as MantineProgress,
type ProgressProps as MantineProgressProps, type ProgressProps as MantineProgressProps,
} from "@mantine/core"; } from "@mantine/core";
import React from "react";
import { cn } from "./utils"; import { cn } from "./utils";
// Original ProgressProps likely had 'value' and 'max'. // Original ProgressProps likely had 'value' and 'max'.

View File

@@ -77,6 +77,7 @@ function SidebarProvider({
} }
// This sets the cookie to keep the sidebar state. // This sets the cookie to keep the sidebar state.
// biome-ignore lint/suspicious/noDocumentCookie: This is a safe use of document.cookie for persisting sidebar state across page loads.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
}, },
[setOpenProp, open], [setOpenProp, open],
@@ -85,7 +86,7 @@ function SidebarProvider({
// Helper to toggle the sidebar. // Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => { const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open); return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]); }, [isMobile, setOpen]);
// Adds a keyboard shortcut to toggle the sidebar. // Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => { React.useEffect(() => {
@@ -117,7 +118,7 @@ function SidebarProvider({
setOpenMobile, setOpenMobile,
toggleSidebar, toggleSidebar,
}), }),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar], [state, open, setOpen, isMobile, openMobile, toggleSidebar],
); );
return ( return (

View File

@@ -52,6 +52,7 @@ function Slider({
{Array.from({ length: _values.length }, (_, index) => ( {Array.from({ length: _values.length }, (_, index) => (
<SliderPrimitive.Thumb <SliderPrimitive.Thumb
data-slot="slider-thumb" data-slot="slider-thumb"
// biome-ignore lint/suspicious/noArrayIndexKey: slider thumbs are stable and index is an appropriate key here.
key={index} key={index}
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50" className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
/> />

View File

@@ -1,14 +1,18 @@
"use client"; "use client";
import { useTheme } from "next-themes"; import { useMantineColorScheme } from "@mantine/core";
import { Toaster as Sonner, type ToasterProps } from "sonner"; import { Toaster as Sonner, type ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => { const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme(); const { colorScheme } = useMantineColorScheme();
return ( return (
<Sonner <Sonner
theme={theme as ToasterProps["theme"]} theme={
colorScheme === "auto"
? "system"
: (colorScheme as ToasterProps["theme"])
}
className="toaster group" className="toaster group"
style={ style={
{ {

View File

@@ -1,7 +1,6 @@
"use client"; "use client";
import { Tooltip as MantineTooltip, type TooltipProps } from "@mantine/core"; import { Tooltip as MantineTooltip, type TooltipProps } from "@mantine/core";
import React from "react";
import { cn } from "./utils"; import { cn } from "./utils";
interface CustomTooltipProps extends TooltipProps { interface CustomTooltipProps extends TooltipProps {

View File

@@ -9,7 +9,6 @@ import {
useMantineColorScheme, useMantineColorScheme,
} from "@mantine/core"; } from "@mantine/core";
import { import {
IconCategory,
IconCurrencyDollar, IconCurrencyDollar,
IconTrendingUp, IconTrendingUp,
IconUsers, IconUsers,
@@ -149,8 +148,8 @@ export const SummaryCards = ({ data }: SummaryCardsProps) => {
return ( return (
<Grid gutter="md"> <Grid gutter="md">
{kpiData.map((kpi, index) => ( {kpiData.map((kpi) => (
<GridCol key={index} span={{ base: 12, sm: 6, lg: 3 }}> <GridCol key={kpi.title} span={{ base: 12, sm: 6, lg: 3 }}>
<KpiCard {...kpi} /> <KpiCard {...kpi} />
</GridCol> </GridCol>
))} ))}

View File

@@ -25,7 +25,7 @@ function PengaturanLayout() {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const isMobile = useMediaQuery("(max-width: 48em)"); const isMobile = useMediaQuery("(max-width: 48em)");
const routerState = useRouterState(); const _routerState = useRouterState();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E"; const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white"; const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
@@ -36,7 +36,7 @@ function PengaturanLayout() {
if (isMobile && opened) { if (isMobile && opened) {
toggleMobile(); toggleMobile();
} }
}, [routerState.location.pathname, isMobile, opened, toggleMobile]); }, [isMobile, opened, toggleMobile]);
return ( return (
<AppShell <AppShell

View File

@@ -50,7 +50,7 @@ function EditProfile() {
authStore.user = { authStore.user = {
...authStore.user, ...authStore.user,
...data.user, ...data.user,
} as any; } as NonNullable<typeof authStore.user>;
navigate({ to: "/profile" }); navigate({ to: "/profile" });
} else if (error) { } else if (error) {
console.error("Update error:", error); console.error("Update error:", error);

View File

@@ -77,13 +77,21 @@ function Profile() {
} }
}; };
interface InfoFieldProps {
icon: React.ElementType;
label: string;
value: string | null | undefined;
copyable?: boolean;
id?: string;
}
const InfoField = ({ const InfoField = ({
icon: Icon, icon: Icon,
label, label,
value, value,
copyable = false, copyable = false,
id = "", id = "",
}: any) => ( }: InfoFieldProps) => (
<Paper <Paper
withBorder withBorder
p="md" p="md"

View File

@@ -24,6 +24,9 @@ export function inspectorPlugin(): Plugin {
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
let line = lines[i]; let line = lines[i];
if (line === undefined) {
continue;
}
// Match JSX opening tags: <Component, <div, or <item.icon // Match JSX opening tags: <Component, <div, or <item.icon
// Allow dots and hyphens in the tag name // Allow dots and hyphens in the tag name
const jsxPattern = /(<(?:[A-Za-z][a-zA-Z0-9.-]*))\b/g; const jsxPattern = /(<(?:[A-Za-z][a-zA-Z0-9.-]*))\b/g;
@@ -32,7 +35,8 @@ export function inspectorPlugin(): Plugin {
// biome-ignore lint/suspicious/noAssignInExpressions: match loop // biome-ignore lint/suspicious/noAssignInExpressions: match loop
while ((match = jsxPattern.exec(line)) !== null) { while ((match = jsxPattern.exec(line)) !== null) {
// Skip if character before `<` is an identifier char (likely a TypeScript generic) // Skip if character before `<` is an identifier char (likely a TypeScript generic)
const charBefore = match.index > 0 ? line[match.index - 1] : ""; const charBefore =
match.index > 0 ? (line[match.index - 1] ?? "") : "";
if (/[a-zA-Z0-9_$.]/.test(charBefore)) continue; if (/[a-zA-Z0-9_$.]/.test(charBefore)) continue;
const col = match.index + 1; const col = match.index + 1;