feat(noc): implement sync management UI and backend integration

This commit is contained in:
2026-03-30 14:48:47 +08:00
parent 3125bc1002
commit 65844bac7e
28 changed files with 2558 additions and 1339 deletions

View File

@@ -1,7 +1,5 @@
{
"permissions": {
"allow": [
"Bash(bun *)"
]
}
}
"permissions": {
"allow": ["Bash(bun *)"]
}
}

View File

@@ -33,7 +33,9 @@ describe("NOC API Module", () => {
it("should return diagram jumlah document", async () => {
const response = await api.handle(
new Request(`http://localhost/api/noc/diagram-jumlah-document?idDesa=${idDesa}`),
new Request(
`http://localhost/api/noc/diagram-jumlah-document?idDesa=${idDesa}`,
),
);
expect(response.status).toBe(200);
const data = await response.json();
@@ -42,7 +44,9 @@ describe("NOC API Module", () => {
it("should return diagram progres kegiatan", async () => {
const response = await api.handle(
new Request(`http://localhost/api/noc/diagram-progres-kegiatan?idDesa=${idDesa}`),
new Request(
`http://localhost/api/noc/diagram-progres-kegiatan?idDesa=${idDesa}`,
),
);
expect(response.status).toBe(200);
const data = await response.json();
@@ -51,7 +55,9 @@ describe("NOC API Module", () => {
it("should return latest discussion", async () => {
const response = await api.handle(
new Request(`http://localhost/api/noc/latest-discussion?idDesa=${idDesa}`),
new Request(
`http://localhost/api/noc/latest-discussion?idDesa=${idDesa}`,
),
);
expect(response.status).toBe(200);
const data = await response.json();

View File

@@ -36,6 +36,134 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/noc/sync": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post: operations["postApiNocSync"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/noc/last-sync": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["getApiNocLast-sync"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/noc/active-divisions": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["getApiNocActive-divisions"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/noc/latest-projects": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["getApiNocLatest-projects"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/noc/upcoming-events": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["getApiNocUpcoming-events"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/noc/diagram-jumlah-document": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["getApiNocDiagram-jumlah-document"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/noc/diagram-progres-kegiatan": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["getApiNocDiagram-progres-kegiatan"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/noc/latest-discussion": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["getApiNocLatest-discussion"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/apikey/": {
parameters: {
query?: never;
@@ -457,102 +585,6 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/noc/active-divisions": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["getApiNocActive-divisions"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/noc/latest-projects": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["getApiNocLatest-projects"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/noc/upcoming-events": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["getApiNocUpcoming-events"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/noc/diagram-jumlah-document": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["getApiNocDiagram-jumlah-document"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/noc/diagram-progres-kegiatan": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["getApiNocDiagram-progres-kegiatan"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/noc/latest-discussion": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["getApiNocLatest-discussion"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
@@ -619,6 +651,362 @@ export interface operations {
};
};
};
postApiNocSync: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
success: boolean;
message?: string;
error?: string;
lastSyncedAt?: string;
};
"multipart/form-data": {
success: boolean;
message?: string;
error?: string;
lastSyncedAt?: string;
};
"text/plain": {
success: boolean;
message?: string;
error?: string;
lastSyncedAt?: string;
};
};
};
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
"multipart/form-data": {
error: string;
};
"text/plain": {
error: string;
};
};
};
};
};
"getApiNocLast-sync": {
parameters: {
query: {
idDesa: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
lastSyncedAt: (string | null) | null;
};
"multipart/form-data": {
lastSyncedAt: (string | null) | null;
};
"text/plain": {
lastSyncedAt: (string | null) | null;
};
};
};
};
};
"getApiNocActive-divisions": {
parameters: {
query: {
idDesa: string;
limit?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
data: {
id: string;
name: string;
activityCount: number;
color: string;
}[];
};
"multipart/form-data": {
data: {
id: string;
name: string;
activityCount: number;
color: string;
}[];
};
"text/plain": {
data: {
id: string;
name: string;
activityCount: number;
color: string;
}[];
};
};
};
};
};
"getApiNocLatest-projects": {
parameters: {
query: {
idDesa: string;
limit?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
data: {
id: string;
title: string;
status: string;
progress: number;
divisionName: string;
createdAt: string;
}[];
};
"multipart/form-data": {
data: {
id: string;
title: string;
status: string;
progress: number;
divisionName: string;
createdAt: string;
}[];
};
"text/plain": {
data: {
id: string;
title: string;
status: string;
progress: number;
divisionName: string;
createdAt: string;
}[];
};
};
};
};
};
"getApiNocUpcoming-events": {
parameters: {
query: {
idDesa: string;
limit?: string;
filter?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
data: {
id: string;
title: string;
startDate: string;
location: (string | null) | null;
eventType: string;
}[];
};
"multipart/form-data": {
data: {
id: string;
title: string;
startDate: string;
location: (string | null) | null;
eventType: string;
}[];
};
"text/plain": {
data: {
id: string;
title: string;
startDate: string;
location: (string | null) | null;
eventType: string;
}[];
};
};
};
};
};
"getApiNocDiagram-jumlah-document": {
parameters: {
query: {
idDesa: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
data: {
category: string;
count: number;
}[];
};
"multipart/form-data": {
data: {
category: string;
count: number;
}[];
};
"text/plain": {
data: {
category: string;
count: number;
}[];
};
};
};
};
};
"getApiNocDiagram-progres-kegiatan": {
parameters: {
query: {
idDesa: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
data: {
status: string;
avgProgress: number;
count: number;
}[];
};
"multipart/form-data": {
data: {
status: string;
avgProgress: number;
count: number;
}[];
};
"text/plain": {
data: {
status: string;
avgProgress: number;
count: number;
}[];
};
};
};
};
};
"getApiNocLatest-discussion": {
parameters: {
query: {
idDesa: string;
limit?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
data: {
id: string;
message: string;
senderName: string;
senderImage: (string | null) | null;
divisionName: string;
createdAt: string;
}[];
};
"multipart/form-data": {
data: {
id: string;
message: string;
senderName: string;
senderImage: (string | null) | null;
divisionName: string;
createdAt: string;
}[];
};
"text/plain": {
data: {
id: string;
message: string;
senderName: string;
senderImage: (string | null) | null;
divisionName: string;
createdAt: string;
}[];
};
};
};
};
};
getApiApikey: {
parameters: {
query?: never;
@@ -2070,279 +2458,4 @@ export interface operations {
};
};
};
"getApiNocActive-divisions": {
parameters: {
query: {
idDesa: string;
limit?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
data: {
id: string;
name: string;
activityCount: number;
color: string;
}[];
};
"multipart/form-data": {
data: {
id: string;
name: string;
activityCount: number;
color: string;
}[];
};
"text/plain": {
data: {
id: string;
name: string;
activityCount: number;
color: string;
}[];
};
};
};
};
};
"getApiNocLatest-projects": {
parameters: {
query: {
idDesa: string;
limit?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
data: {
id: string;
title: string;
status: string;
progress: number;
divisionName: string;
createdAt: string;
}[];
};
"multipart/form-data": {
data: {
id: string;
title: string;
status: string;
progress: number;
divisionName: string;
createdAt: string;
}[];
};
"text/plain": {
data: {
id: string;
title: string;
status: string;
progress: number;
divisionName: string;
createdAt: string;
}[];
};
};
};
};
};
"getApiNocUpcoming-events": {
parameters: {
query: {
idDesa: string;
limit?: string;
filter?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
data: {
id: string;
title: string;
startDate: string;
location: (string | null) | null;
eventType: string;
}[];
};
"multipart/form-data": {
data: {
id: string;
title: string;
startDate: string;
location: (string | null) | null;
eventType: string;
}[];
};
"text/plain": {
data: {
id: string;
title: string;
startDate: string;
location: (string | null) | null;
eventType: string;
}[];
};
};
};
};
};
"getApiNocDiagram-jumlah-document": {
parameters: {
query: {
idDesa: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
data: {
category: string;
count: number;
}[];
};
"multipart/form-data": {
data: {
category: string;
count: number;
}[];
};
"text/plain": {
data: {
category: string;
count: number;
}[];
};
};
};
};
};
"getApiNocDiagram-progres-kegiatan": {
parameters: {
query: {
idDesa: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
data: {
status: string;
avgProgress: number;
count: number;
}[];
};
"multipart/form-data": {
data: {
status: string;
avgProgress: number;
count: number;
}[];
};
"text/plain": {
data: {
status: string;
avgProgress: number;
count: number;
}[];
};
};
};
};
};
"getApiNocLatest-discussion": {
parameters: {
query: {
idDesa: string;
limit?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
data: {
id: string;
message: string;
senderName: string;
senderImage: (string | null) | null;
divisionName: string;
createdAt: string;
}[];
};
"multipart/form-data": {
data: {
id: string;
message: string;
senderName: string;
senderImage: (string | null) | null;
divisionName: string;
createdAt: string;
}[];
};
"text/plain": {
data: {
id: string;
message: string;
senderName: string;
senderImage: (string | null) | null;
divisionName: string;
createdAt: string;
}[];
};
};
};
};
};
}

269
generated/noc-external.ts Normal file
View File

@@ -0,0 +1,269 @@
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/
export interface paths {
"/api/noc/active-divisions": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Divisi Teraktif
* @description Menu Beranda - Mendapatkan daftar divisi teraktif berdasarkan jumlah proyek pada desa tertentu.
*/
get: operations["getApiNocActive-divisions"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/noc/latest-projects": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Latest Projects General
* @description Menu kinerja divisi - Mendapatkan daftar proyek umum terbaru dari berbagai grup pada desa tertentu.
*/
get: operations["getApiNocLatest-projects"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/noc/upcoming-events": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Events (Today & Upcoming)
* @description Menu beranda dan kinerja divisi - Mendapatkan daftar event pada hari ini dan yang akan datang untuk semua divisi pada desa tertentu.
*/
get: operations["getApiNocUpcoming-events"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/noc/diagram-jumlah-document": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Diagram Jumlah Document
* @description Menu kinerja divisi - Mendapatkan diagram jumlah document pada desa tertentu.
*/
get: operations["getApiNocDiagram-jumlah-document"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/noc/diagram-progres-kegiatan": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Diagram Progres Kegiatan
* @description Menu kinerja divisi - Mendapatkan diagram progres kegiatan pada desa tertentu.
*/
get: operations["getApiNocDiagram-progres-kegiatan"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/noc/latest-discussion": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Latest Discussion
* @description Menu kinerja divisi - Mendapatkan latest discussion pada desa tertentu.
*/
get: operations["getApiNocLatest-discussion"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: never;
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export interface operations {
"getApiNocActive-divisions": {
parameters: {
query: {
/** @description ID Desa yang ingin dicari */
idDesa: string;
/** @description Jumlah maksimal data (default: 5) */
limit?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
"getApiNocLatest-projects": {
parameters: {
query: {
/** @description ID Desa yang ingin dicari */
idDesa: string;
/** @description Jumlah maksimal proyek (default: 5, maks: 50) */
limit?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
"getApiNocUpcoming-events": {
parameters: {
query: {
/** @description ID Desa yang ingin dicari */
idDesa: string;
/** @description Jumlah maksimal event (default: 10, maks: 50) */
limit?: string;
/** @description Filter event: 'today' atau 'upcoming' */
filter?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
"getApiNocDiagram-jumlah-document": {
parameters: {
query: {
/** @description ID Desa yang ingin dicari */
idDesa: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
"getApiNocDiagram-progres-kegiatan": {
parameters: {
query: {
/** @description ID Desa yang ingin dicari */
idDesa: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
"getApiNocLatest-discussion": {
parameters: {
query: {
/** @description ID Desa yang ingin dicari */
idDesa: string;
/** @description Limit data */
limit?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@
"check": "biome check --write .",
"format": "biome format --write .",
"gen:api": "bun scripts/generate-schema.ts && bun x openapi-typescript generated/schema.json -o generated/api.ts",
"sync:noc": "bun scripts/sync-noc.ts",
"test": "bun test __tests__/api",
"test:ui": "bun test --ui __tests__/api",
"test:e2e": "bun run build && playwright test",

View File

@@ -0,0 +1,44 @@
/*
Warnings:
- A unique constraint covering the columns `[externalId]` on the table `activity` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[externalId]` on the table `discussion` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[externalId]` on the table `division` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[externalId]` on the table `document` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[externalId]` on the table `event` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "activity" ADD COLUMN "externalId" TEXT,
ADD COLUMN "villageId" TEXT DEFAULT 'darmasaba';
-- AlterTable
ALTER TABLE "discussion" ADD COLUMN "externalId" TEXT,
ADD COLUMN "villageId" TEXT DEFAULT 'darmasaba';
-- AlterTable
ALTER TABLE "division" ADD COLUMN "externalId" TEXT,
ADD COLUMN "villageId" TEXT DEFAULT 'darmasaba';
-- AlterTable
ALTER TABLE "document" ADD COLUMN "externalId" TEXT,
ADD COLUMN "villageId" TEXT DEFAULT 'darmasaba';
-- AlterTable
ALTER TABLE "event" ADD COLUMN "externalId" TEXT,
ADD COLUMN "villageId" TEXT DEFAULT 'darmasaba';
-- CreateIndex
CREATE UNIQUE INDEX "activity_externalId_key" ON "activity"("externalId");
-- CreateIndex
CREATE UNIQUE INDEX "discussion_externalId_key" ON "discussion"("externalId");
-- CreateIndex
CREATE UNIQUE INDEX "division_externalId_key" ON "division"("externalId");
-- CreateIndex
CREATE UNIQUE INDEX "document_externalId_key" ON "document"("externalId");
-- CreateIndex
CREATE UNIQUE INDEX "event_externalId_key" ON "event"("externalId");

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "division" ADD COLUMN "lastSyncedAt" TIMESTAMP(3);

View File

@@ -42,10 +42,13 @@ model User {
model Division {
id String @id @default(cuid())
externalId String? @unique // ID asli dari server NOC
villageId String? @default("darmasaba") // ID Desa dari sistem NOC
name String @unique
description String?
color String @default("#1E3A5F")
isActive Boolean @default(true)
lastSyncedAt DateTime? // Terakhir kali sinkronisasi dilakukan
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -59,6 +62,8 @@ model Division {
model Activity {
id String @id @default(cuid())
externalId String? @unique // ID asli dari server NOC
villageId String? @default("darmasaba")
title String
description String?
divisionId String
@@ -82,6 +87,8 @@ model Activity {
model Document {
id String @id @default(cuid())
externalId String? @unique // ID asli dari server NOC
villageId String? @default("darmasaba")
title String
category DocumentCategory
type String // "Gambar", "Dokumen", "PDF", etc
@@ -101,6 +108,8 @@ model Document {
model Discussion {
id String @id @default(cuid())
externalId String? @unique // ID asli dari server NOC
villageId String? @default("darmasaba")
message String
senderId String
parentId String? // For threaded discussions
@@ -121,6 +130,8 @@ model Discussion {
model Event {
id String @id @default(cuid())
externalId String? @unique // ID asli dari server NOC
villageId String? @default("darmasaba")
title String
description String?
eventType EventType

View File

@@ -2,20 +2,32 @@ import "dotenv/config";
import { PrismaClient } from "../generated/prisma";
// Import all seeders
import { seedAdminUser, seedDemoUsers, seedApiKeys } from "./seeders/seed-auth";
import { seedBanjars, seedResidents, getBanjarIds } from "./seeders/seed-demographics";
import { seedDivisions, seedActivities, getDivisionIds } from "./seeders/seed-division-performance";
import { seedAdminUser, seedApiKeys, seedDemoUsers } from "./seeders/seed-auth";
import { seedDashboardMetrics } from "./seeders/seed-dashboard-metrics";
import {
getBanjarIds,
seedBanjars,
seedResidents,
} from "./seeders/seed-demographics";
import {
seedDiscussions,
seedDivisionMetrics,
seedDocuments,
} from "./seeders/seed-discussions";
import {
getDivisionIds,
seedActivities,
seedDivisions,
} from "./seeders/seed-division-performance";
import { seedPhase2 } from "./seeders/seed-phase2";
import {
getComplaintIds,
seedComplaints,
seedServiceLetters,
seedComplaintUpdates,
seedEvents,
seedInnovationIdeas,
seedComplaintUpdates,
getComplaintIds,
seedServiceLetters,
} from "./seeders/seed-public-services";
import { seedDocuments, seedDiscussions, seedDivisionMetrics } from "./seeders/seed-discussions";
import { seedDashboardMetrics } from "./seeders/seed-dashboard-metrics";
import { seedPhase2 } from "./seeders/seed-phase2";
const prisma = new PrismaClient();
@@ -45,7 +57,9 @@ export async function runSeed() {
// Check if data already exists
const existingData = await hasExistingData();
if (existingData) {
console.log("⏭️ Existing data detected. Skipping seed to prevent duplicates.\n");
console.log(
"⏭️ Existing data detected. Skipping seed to prevent duplicates.\n",
);
console.log("💡 To re-seed, either:");
console.log(" 1. Run: bun x prisma migrate reset (resets database)");
console.log(" 2. Manually delete data from tables\n");
@@ -114,7 +128,9 @@ export async function runSpecificSeeder(name: string) {
// Check if data already exists for specific seeder
const existingData = await hasExistingData();
if (existingData && name !== "auth") {
console.log("⚠️ Warning: Existing data detected for this seeder category.\n");
console.log(
"⚠️ Warning: Existing data detected for this seeder category.\n",
);
console.log("💡 To re-seed, either:");
console.log(" 1. Run: bun x prisma migrate reset (resets database)");
console.log(" 2. Manually delete data from tables\n");
@@ -124,33 +140,36 @@ export async function runSpecificSeeder(name: string) {
switch (name) {
case "auth":
case "users":
case "users": {
console.log("📁 Authentication & Users");
const adminId = await seedAdminUser();
await seedDemoUsers();
await seedApiKeys(adminId);
break;
}
case "demographics":
case "population":
case "population": {
console.log("📁 Demographics & Population");
await seedBanjars();
const banjarIds = await getBanjarIds();
await seedResidents(banjarIds);
break;
}
case "divisions":
case "performance":
case "performance": {
console.log("📁 Division Performance");
const divisions = await seedDivisions();
const divisionIds = divisions.map((d) => d.id);
await seedActivities(divisionIds);
await seedDivisionMetrics(divisionIds);
break;
}
case "complaints":
case "services":
case "public":
case "public": {
console.log("📁 Public Services");
const pubAdminId = await seedAdminUser();
await seedComplaints(pubAdminId);
@@ -160,9 +179,10 @@ export async function runSpecificSeeder(name: string) {
const compIds = await getComplaintIds();
await seedComplaintUpdates(compIds, pubAdminId);
break;
}
case "documents":
case "discussions":
case "discussions": {
console.log("📁 Documents & Discussions");
const docAdminId = await seedAdminUser();
const divs = await seedDivisions();
@@ -170,6 +190,7 @@ export async function runSpecificSeeder(name: string) {
await seedDocuments(divIds, docAdminId);
await seedDiscussions(divIds, docAdminId);
break;
}
case "dashboard":
case "metrics":
@@ -178,17 +199,20 @@ export async function runSpecificSeeder(name: string) {
break;
case "phase2":
case "features":
case "features": {
console.log("📁 Phase 2+ Features");
const p2AdminId = await seedAdminUser();
await seedBanjars();
const p2BanjarIds = await getBanjarIds();
await seedPhase2(p2BanjarIds, p2AdminId);
break;
}
default:
console.error(`❌ Unknown seeder: ${name}`);
console.log("Available seeders: auth, demographics, divisions, complaints, documents, dashboard, phase2");
console.log(
"Available seeders: auth, demographics, divisions, complaints, documents, dashboard, phase2",
);
process.exit(1);
}

View File

@@ -1,4 +1,4 @@
import { PrismaClient, Gender, Religion } from "../../generated/prisma";
import { Gender, PrismaClient, Religion } from "../../generated/prisma";
const prisma = new PrismaClient();

View File

@@ -1,8 +1,4 @@
import {
ActivityStatus,
Priority,
PrismaClient,
} from "../../generated/prisma";
import { ActivityStatus, Priority, PrismaClient } from "../../generated/prisma";
const prisma = new PrismaClient();

View File

@@ -25,13 +25,14 @@ export async function seedComplaints(adminId: string) {
console.log("Seeding Complaints...");
const now = new Date();
const complaints = [
// Recent complaints (this month)
{
complaintNumber: `COMP-20260327-001`,
title: "Lampu Jalan Mati",
description: "Lampu jalan di depan Balai Banjar Manesa mati sejak 3 hari lalu.",
description:
"Lampu jalan di depan Balai Banjar Manesa mati sejak 3 hari lalu.",
category: ComplaintCategory.INFRASTRUKTUR,
status: ComplaintStatus.BARU,
priority: Priority.SEDANG,
@@ -187,7 +188,9 @@ export async function seedComplaints(adminId: string) {
});
}
console.log("✅ Complaints seeded successfully (12 complaints across 7 months)");
console.log(
"✅ Complaints seeded successfully (12 complaints across 7 months)",
);
}
/**
@@ -354,7 +357,10 @@ export async function seedInnovationIdeas(adminId: string) {
* Seed Complaint Updates
* Creates status update history for complaints
*/
export async function seedComplaintUpdates(complaintIds: string[], userId: string) {
export async function seedComplaintUpdates(
complaintIds: string[],
userId: string,
) {
console.log("Seeding Complaint Updates...");
if (complaintIds.length === 0) {

225
scripts/sync-noc.ts Normal file
View File

@@ -0,0 +1,225 @@
import { prisma } from "../src/utils/db";
import { nocExternalClient } from "../src/utils/noc-external-client";
import logger from "../src/utils/logger";
const ID_DESA = "darmasaba";
/**
* Helper untuk mendapatkan system user ID untuk relasi
*/
async function getSystemUserId() {
const user = await prisma.user.findFirst({
where: { role: "admin" },
});
if (!user) {
// Buat system user jika tidak ada
const newUser = await prisma.user.create({
data: {
email: "system@darmasaba.id",
name: "System Sync",
role: "admin",
},
});
return newUser.id;
}
return user.id;
}
/**
* 1. Sync Divisions
*/
async function syncActiveDivisions() {
logger.info("Syncing Divisions...");
const { data, error } = await nocExternalClient.GET("/api/noc/active-divisions", {
params: { query: { idDesa: ID_DESA } },
});
if (error || !data) {
logger.error({ error }, "Failed to fetch divisions from NOC");
return;
}
// biome-ignore lint/suspicious/noExplicitAny: External API response is untyped
const divisions = (data as any).data;
for (const div of divisions) {
await prisma.division.upsert({
where: { externalId: div.id },
update: {
name: div.name,
color: div.color,
villageId: ID_DESA,
},
create: {
externalId: div.id,
name: div.name,
color: div.color,
villageId: ID_DESA,
},
});
}
logger.info(`Synced ${divisions.length} divisions`);
}
/**
* 2. Sync Activities
*/
async function syncLatestProjects() {
logger.info("Syncing Activities...");
const { data, error } = await nocExternalClient.GET("/api/noc/latest-projects", {
params: { query: { idDesa: ID_DESA, limit: "50" } },
});
if (error || !data) {
logger.error({ error }, "Failed to fetch projects from NOC");
return;
}
// biome-ignore lint/suspicious/noExplicitAny: External API response
const projects = (data as any).data;
for (const proj of projects) {
// Temukan divisi lokal berdasarkan nama atau externalId (asumsi externalId divisi sinkron)
// Karena kita sinkron divisi dulu, kita cari berdasarkan nama jika externalId belum pasti
const division = await prisma.division.findFirst({
where: { name: proj.divisionName },
});
if (!division) continue;
await prisma.activity.upsert({
where: { externalId: proj.id },
update: {
title: proj.title,
status: proj.status as any,
progress: proj.progress,
divisionId: division.id,
villageId: ID_DESA,
},
create: {
externalId: proj.id,
title: proj.title,
status: proj.status as any,
progress: proj.progress,
divisionId: division.id,
villageId: ID_DESA,
},
});
}
logger.info(`Synced ${projects.length} activities`);
}
/**
* 3. Sync Events
*/
async function syncUpcomingEvents() {
logger.info("Syncing Events...");
const systemUserId = await getSystemUserId();
const { data, error } = await nocExternalClient.GET("/api/noc/upcoming-events", {
params: { query: { idDesa: ID_DESA, limit: "50" } },
});
if (error || !data) {
logger.error({ error }, "Failed to fetch events from NOC");
return;
}
// biome-ignore lint/suspicious/noExplicitAny: External API response
const events = (data as any).data;
for (const event of events) {
await prisma.event.upsert({
where: { externalId: event.id },
update: {
title: event.title,
startDate: new Date(event.startDate),
location: event.location,
eventType: event.eventType as any,
villageId: ID_DESA,
},
create: {
externalId: event.id,
title: event.title,
startDate: new Date(event.startDate),
location: event.location,
eventType: event.eventType as any,
createdBy: systemUserId,
villageId: ID_DESA,
},
});
}
logger.info(`Synced ${events.length} events`);
}
/**
* 4. Sync Discussions
*/
async function syncLatestDiscussion() {
logger.info("Syncing Discussions...");
const systemUserId = await getSystemUserId();
const { data, error } = await nocExternalClient.GET("/api/noc/latest-discussion", {
params: { query: { idDesa: ID_DESA, limit: "50" } },
});
if (error || !data) {
logger.error({ error }, "Failed to fetch discussions from NOC");
return;
}
// biome-ignore lint/suspicious/noExplicitAny: External API response
const discussions = (data as any).data;
for (const disc of discussions) {
const division = await prisma.division.findFirst({
where: { name: disc.divisionName },
});
await prisma.discussion.upsert({
where: { externalId: disc.id },
update: {
message: disc.message,
divisionId: division?.id,
villageId: ID_DESA,
},
create: {
externalId: disc.id,
message: disc.message,
senderId: systemUserId,
divisionId: division?.id,
villageId: ID_DESA,
},
});
}
logger.info(`Synced ${discussions.length} discussions`);
}
/**
* 5. Update lastSyncedAt timestamp
*/
async function syncLastTimestamp() {
logger.info("Updating sync timestamp...");
await prisma.division.updateMany({
where: { villageId: ID_DESA },
data: { lastSyncedAt: new Date() },
});
}
/**
* Main Sync Function
*/
async function main() {
try {
logger.info("Starting NOC Data Synchronization...");
await syncActiveDivisions();
await syncLatestProjects();
await syncUpcomingEvents();
await syncLatestDiscussion();
await syncLastTimestamp();
logger.info("NOC Data Synchronization Completed Successfully");
} catch (err) {
logger.error({ err }, "Fatal error during NOC synchronization");
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
main();

View File

@@ -8,9 +8,9 @@ import { complaint } from "./complaint";
import { dashboard } from "./dashboard";
import { division } from "./division";
import { event } from "./event";
import { noc } from "./noc";
import { profile } from "./profile";
import { resident } from "./resident";
import { noc } from "./noc";
const isProduction = process.env.NODE_ENV === "production";

View File

@@ -1,13 +1,67 @@
import { Elysia, t } from "elysia";
import { prisma } from "../utils/db";
import { $ } from "bun";
export const noc = new Elysia({ prefix: "/noc" })
.post(
"/sync",
async ({ set, user }) => {
if (!user || user.role !== "admin") {
set.status = 401;
return { error: "Unauthorized" };
}
try {
// Jalankan script sinkronisasi
await $`bun run sync:noc`.quiet();
return {
success: true,
message: "Sinkronisasi berhasil diselesaikan",
lastSyncedAt: new Date().toISOString(),
};
} catch (error) {
return { success: false, error: "Sinkronisasi gagal dijalankan" };
}
},
{
response: {
200: t.Object({
success: t.Boolean(),
message: t.Optional(t.String()),
error: t.Optional(t.String()),
lastSyncedAt: t.Optional(t.String()),
}),
401: t.Object({ error: t.String() }),
},
},
)
.get(
"/last-sync",
async ({ query }) => {
const { idDesa } = query;
const latest = await prisma.division.findFirst({
where: { villageId: idDesa },
select: { lastSyncedAt: true },
orderBy: { lastSyncedAt: "desc" },
});
return { lastSyncedAt: latest?.lastSyncedAt?.toISOString() || null };
},
{
query: t.Object({ idDesa: t.String() }),
response: {
200: t.Object({
lastSyncedAt: t.Nullable(t.String()),
}),
},
},
)
.get(
"/active-divisions",
async ({ query }) => {
const { idDesa, limit } = query;
// TODO: Filter by idDesa once schema supports it
const data = await prisma.division.findMany({
where: { villageId: idDesa },
include: {
_count: {
select: { activities: true },
@@ -53,8 +107,8 @@ export const noc = new Elysia({ prefix: "/noc" })
"/latest-projects",
async ({ query }) => {
const { idDesa, limit } = query;
// TODO: Filter by idDesa once schema supports it
const data = await prisma.activity.findMany({
where: { villageId: idDesa },
orderBy: { createdAt: "desc" },
take: limit ? Number.parseInt(limit) : 5,
include: { division: true },
@@ -96,9 +150,8 @@ export const noc = new Elysia({ prefix: "/noc" })
"/upcoming-events",
async ({ query }) => {
const { idDesa, limit, filter } = query;
// TODO: Filter by idDesa once schema supports it
const now = new Date();
const where: any = {};
const where: any = { villageId: idDesa };
if (filter === "today") {
const startOfDay = new Date(now.setHours(0, 0, 0, 0));
@@ -154,8 +207,8 @@ export const noc = new Elysia({ prefix: "/noc" })
"/diagram-jumlah-document",
async ({ query }) => {
const { idDesa } = query;
// TODO: Filter by idDesa once schema supports it
const data = await prisma.document.groupBy({
where: { villageId: idDesa },
by: ["category"],
_count: {
_all: true,
@@ -189,8 +242,8 @@ export const noc = new Elysia({ prefix: "/noc" })
"/diagram-progres-kegiatan",
async ({ query }) => {
const { idDesa } = query;
// TODO: Filter by idDesa once schema supports it
const data = await prisma.activity.groupBy({
where: { villageId: idDesa },
by: ["status"],
_avg: {
progress: true,
@@ -229,8 +282,8 @@ export const noc = new Elysia({ prefix: "/noc" })
"/latest-discussion",
async ({ query }) => {
const { idDesa, limit } = query;
// TODO: Filter by idDesa once schema supports it
const data = await prisma.discussion.findMany({
where: { villageId: idDesa },
orderBy: { createdAt: "desc" },
take: limit ? Number.parseInt(limit) : 5,
include: {
@@ -273,4 +326,5 @@ export const noc = new Elysia({ prefix: "/noc" })
),
}),
},
});
},
);

View File

@@ -1,4 +1,4 @@
import { Grid, Image, Loader, Stack, Center } from "@mantine/core";
import { Center, Grid, Image, Loader, Stack } from "@mantine/core";
import { CheckCircle, FileText, MessageCircle, Users } from "lucide-react";
import { useEffect, useState } from "react";
import { apiClient } from "@/utils/api-client";
@@ -18,20 +18,21 @@ export function DashboardContent() {
loading: true,
});
const [sdgsData, setSdgsData] = useState<{ title: string; score: number; image: string | null }[]>([]);
const [sdgsData, setSdgsData] = useState<
{ title: string; score: number; image: string | null }[]
>([]);
const [sdgsLoading, setSdgsLoading] = useState(true);
useEffect(() => {
async function fetchStats() {
try {
const [complaintRes, residentRes, weeklyServiceRes, sdgsRes] = await Promise.all(
[
const [complaintRes, residentRes, weeklyServiceRes, sdgsRes] =
await Promise.all([
apiClient.GET("/api/complaint/stats"),
apiClient.GET("/api/resident/stats"),
apiClient.GET("/api/complaint/service-weekly"),
apiClient.GET("/api/dashboard/sdgs"),
],
);
]);
setStats({
complaints: (complaintRes.data as { data: typeof stats.complaints })
@@ -138,7 +139,9 @@ export function DashboardContent() {
{sdgsData.map((sdg) => (
<Grid.Col key={sdg.title} span={{ base: 9, md: 3 }}>
<SDGSCard
image={sdg.image ? <Image src={sdg.image} alt={sdg.title} /> : null}
image={
sdg.image ? <Image src={sdg.image} alt={sdg.title} /> : null
}
title={sdg.title}
score={sdg.score}
/>

View File

@@ -96,7 +96,13 @@ export function ChartAPBDes() {
</Bar>
</BarChart>
</ResponsiveContainer>
<Text size="sm" fw={600} w={40} ta="right" c={dark ? "white" : "gray.9"}>
<Text
size="sm"
fw={600}
w={40}
ta="right"
c={dark ? "white" : "gray.9"}
>
{item.value}%
</Text>
</Group>

View File

@@ -51,8 +51,14 @@ export function ChartSurat() {
console.log("📊 Service trends response:", res);
// Check if response has data
if (res.data?.data && Array.isArray(res.data.data) && res.data.data.length > 0) {
const chartData = (res.data.data as { month: string; count: number }[]).map((d) => ({
if (
res.data?.data &&
Array.isArray(res.data.data) &&
res.data.data.length > 0
) {
const chartData = (
res.data.data as { month: string; count: number }[]
).map((d) => ({
month: d.month,
value: Number(d.count),
}));

View File

@@ -1,9 +1,16 @@
import { Card, Group, Loader, Stack, Text, useMantineColorScheme } from "@mantine/core";
import {
Card,
Group,
Loader,
Stack,
Text,
useMantineColorScheme,
} from "@mantine/core";
import { format } from "date-fns";
import { id } from "date-fns/locale";
import { MessageCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { apiClient } from "@/utils/api-client";
import { format } from "date-fns";
import { id } from "date-fns/locale";
interface DiscussionItem {
id: string;

View File

@@ -1,4 +1,10 @@
import { Card, Group, Loader, Text, useMantineColorScheme } from "@mantine/core";
import {
Card,
Group,
Loader,
Text,
useMantineColorScheme,
} from "@mantine/core";
import { useEffect, useState } from "react";
import {
Bar,

View File

@@ -47,10 +47,26 @@ export function ProgressChart() {
if (res.data?.data) {
const stats = res.data.data as ActivityStats;
const chartData: ProgressData[] = [
{ name: "Selesai", value: stats.percentages.selesai, color: "#22C55E" },
{ name: "Dikerjakan", value: stats.percentages.berjalan, color: "#F59E0B" },
{ name: "Segera Dikerjakan", value: stats.percentages.tertunda, color: "#3B82F6" },
{ name: "Dibatalkan", value: stats.percentages.dibatalkan, color: "#EF4444" },
{
name: "Selesai",
value: stats.percentages.selesai,
color: "#22C55E",
},
{
name: "Dikerjakan",
value: stats.percentages.berjalan,
color: "#F59E0B",
},
{
name: "Segera Dikerjakan",
value: stats.percentages.tertunda,
color: "#3B82F6",
},
{
name: "Dibatalkan",
value: stats.percentages.dibatalkan,
color: "#EF4444",
},
];
setData(chartData);
}

View File

@@ -0,0 +1,176 @@
import {
Box,
Button,
Card,
Group,
Stack,
Text,
Title,
Alert,
Loader,
Badge,
Divider,
} from "@mantine/core";
import { IconRefresh, IconCheck, IconAlertCircle, IconClock } from "@tabler/icons-react";
import { useState, useEffect } from "react";
import { apiClient } from "@/utils/api-client";
import dayjs from "dayjs";
import "dayjs/locale/id";
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime);
dayjs.locale("id");
const SinkronisasiSettings = () => {
const [loading, setLoading] = useState(false);
const [lastSync, setLastSync] = useState<string | null>(null);
const [status, setStatus] = useState<{
type: "success" | "error" | null;
message: string;
}>({ type: null, message: "" });
const fetchLastSync = async () => {
const { data } = await apiClient.GET("/api/noc/last-sync", {
params: { query: { idDesa: "darmasaba" } },
});
if (data?.lastSyncedAt) {
setLastSync(data.lastSyncedAt);
}
};
useEffect(() => {
fetchLastSync();
}, []);
const handleSync = async () => {
setLoading(true);
setStatus({ type: null, message: "" });
try {
const { data, error } = await apiClient.POST("/api/noc/sync");
if (error) {
setStatus({
type: "error",
message: (error as any).error || "Gagal melakukan sinkronisasi",
});
} else if (data?.success) {
setStatus({
type: "success",
message: data.message || "Sinkronisasi berhasil dilakukan",
});
if (data.lastSyncedAt) {
setLastSync(data.lastSyncedAt);
}
}
} catch (err) {
setStatus({
type: "error",
message: "Terjadi kesalahan sistem saat sinkronisasi",
});
} finally {
setLoading(false);
}
};
return (
<Box pr={"50%"}>
<Title order={2} mb="lg">
Sinkronisasi Data NOC
</Title>
<Text c="dimmed" mb="xl">
Gunakan fitur ini untuk memperbarui data dashboard dengan data terbaru dari
server Network Operation Center (NOC) darmasaba.muku.id.
</Text>
<Card withBorder padding="lg" radius="md" mb="xl">
<Stack gap="md">
<Group justify="space-between">
<Group>
<IconClock size={20} color="gray" />
<Text fw={500}>Status Terakhir</Text>
</Group>
<Badge color={lastSync ? "green" : "gray"} variant="light">
{lastSync ? "Terkoneksi" : "Belum Pernah Sinkron"}
</Badge>
</Group>
<Divider />
<Box>
<Text size="sm" c="dimmed">
Waktu Sinkronisasi Terakhir:
</Text>
<Text fw={700} size="lg">
{lastSync
? dayjs(lastSync).format("DD MMMM YYYY, HH:mm:ss")
: "Belum pernah dilakukan"}
</Text>
{lastSync && (
<Text size="xs" c="dimmed" mt={4}>
({dayjs(lastSync).fromNow()})
</Text>
)}
</Box>
{status.type && (
<Alert
icon={
status.type === "success" ? (
<IconCheck size={16} />
) : (
<IconAlertCircle size={16} />
)
}
title={status.type === "success" ? "Berhasil" : "Kesalahan"}
color={status.type === "success" ? "green" : "red"}
onClose={() => setStatus({ type: null, message: "" })}
withCloseButton
>
{status.message}
</Alert>
)}
<Button
leftSection={
loading ? <Loader size={16} color="white" /> : <IconRefresh size={16} />
}
onClick={handleSync}
loading={loading}
fullWidth
mt="md"
>
Sinkronkan Sekarang
</Button>
</Stack>
</Card>
<Title order={2} mb="lg">
Informasi API
</Title>
<Card withBorder padding="md" radius="md" bg="gray.0">
<Stack gap="xs">
<Group>
<Text fw={600} size="sm" w={100}>URL Sumber:</Text>
<Text size="sm" style={{ wordBreak: 'break-all' }}>https://darmasaba.muku.id/api/noc/</Text>
</Group>
<Group>
<Text fw={600} size="sm" w={100}>ID Desa:</Text>
<Text size="sm">darmasaba</Text>
</Group>
<Group>
<Text fw={600} size="sm" w={100}>Model Data:</Text>
<Badge size="xs" variant="outline">Divisi</Badge>
<Badge size="xs" variant="outline">Kegiatan</Badge>
<Badge size="xs" variant="outline">Event</Badge>
<Badge size="xs" variant="outline">Diskusi</Badge>
</Group>
</Stack>
</Card>
</Box>
);
};
export default SinkronisasiSettings;

View File

@@ -48,6 +48,7 @@ export function Sidebar({ className }: SidebarProps) {
{ name: "Notifikasi", path: "/pengaturan/notifikasi" },
{ name: "Keamanan", path: "/pengaturan/keamanan" },
{ name: "Akses & Tim", path: "/pengaturan/akses-dan-tim" },
{ name: "Sinkronisasi NOC", path: "/pengaturan/sinkronisasi" },
];
// Check if any settings submenu is active

View File

@@ -30,6 +30,7 @@ import { Route as AdminIndexRouteImport } from './routes/admin/index'
import { Route as UsersIdRouteImport } from './routes/users/$id'
import { Route as ProfileEditRouteImport } from './routes/profile/edit'
import { Route as PengaturanUmumRouteImport } from './routes/pengaturan/umum'
import { Route as PengaturanSinkronisasiRouteImport } from './routes/pengaturan/sinkronisasi'
import { Route as PengaturanNotifikasiRouteImport } from './routes/pengaturan/notifikasi'
import { Route as PengaturanKeamananRouteImport } from './routes/pengaturan/keamanan'
import { Route as PengaturanAksesDanTimRouteImport } from './routes/pengaturan/akses-dan-tim'
@@ -142,6 +143,11 @@ const PengaturanUmumRoute = PengaturanUmumRouteImport.update({
path: '/umum',
getParentRoute: () => PengaturanRouteRoute,
} as any)
const PengaturanSinkronisasiRoute = PengaturanSinkronisasiRouteImport.update({
id: '/sinkronisasi',
path: '/sinkronisasi',
getParentRoute: () => PengaturanRouteRoute,
} as any)
const PengaturanNotifikasiRoute = PengaturanNotifikasiRouteImport.update({
id: '/notifikasi',
path: '/notifikasi',
@@ -195,6 +201,7 @@ export interface FileRoutesByFullPath {
'/pengaturan/akses-dan-tim': typeof PengaturanAksesDanTimRoute
'/pengaturan/keamanan': typeof PengaturanKeamananRoute
'/pengaturan/notifikasi': typeof PengaturanNotifikasiRoute
'/pengaturan/sinkronisasi': typeof PengaturanSinkronisasiRoute
'/pengaturan/umum': typeof PengaturanUmumRoute
'/profile/edit': typeof ProfileEditRoute
'/users/$id': typeof UsersIdRoute
@@ -222,6 +229,7 @@ export interface FileRoutesByTo {
'/pengaturan/akses-dan-tim': typeof PengaturanAksesDanTimRoute
'/pengaturan/keamanan': typeof PengaturanKeamananRoute
'/pengaturan/notifikasi': typeof PengaturanNotifikasiRoute
'/pengaturan/sinkronisasi': typeof PengaturanSinkronisasiRoute
'/pengaturan/umum': typeof PengaturanUmumRoute
'/profile/edit': typeof ProfileEditRoute
'/users/$id': typeof UsersIdRoute
@@ -252,6 +260,7 @@ export interface FileRoutesById {
'/pengaturan/akses-dan-tim': typeof PengaturanAksesDanTimRoute
'/pengaturan/keamanan': typeof PengaturanKeamananRoute
'/pengaturan/notifikasi': typeof PengaturanNotifikasiRoute
'/pengaturan/sinkronisasi': typeof PengaturanSinkronisasiRoute
'/pengaturan/umum': typeof PengaturanUmumRoute
'/profile/edit': typeof ProfileEditRoute
'/users/$id': typeof UsersIdRoute
@@ -283,6 +292,7 @@ export interface FileRouteTypes {
| '/pengaturan/akses-dan-tim'
| '/pengaturan/keamanan'
| '/pengaturan/notifikasi'
| '/pengaturan/sinkronisasi'
| '/pengaturan/umum'
| '/profile/edit'
| '/users/$id'
@@ -310,6 +320,7 @@ export interface FileRouteTypes {
| '/pengaturan/akses-dan-tim'
| '/pengaturan/keamanan'
| '/pengaturan/notifikasi'
| '/pengaturan/sinkronisasi'
| '/pengaturan/umum'
| '/profile/edit'
| '/users/$id'
@@ -339,6 +350,7 @@ export interface FileRouteTypes {
| '/pengaturan/akses-dan-tim'
| '/pengaturan/keamanan'
| '/pengaturan/notifikasi'
| '/pengaturan/sinkronisasi'
| '/pengaturan/umum'
| '/profile/edit'
| '/users/$id'
@@ -516,6 +528,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof PengaturanUmumRouteImport
parentRoute: typeof PengaturanRouteRoute
}
'/pengaturan/sinkronisasi': {
id: '/pengaturan/sinkronisasi'
path: '/sinkronisasi'
fullPath: '/pengaturan/sinkronisasi'
preLoaderRoute: typeof PengaturanSinkronisasiRouteImport
parentRoute: typeof PengaturanRouteRoute
}
'/pengaturan/notifikasi': {
id: '/pengaturan/notifikasi'
path: '/notifikasi'
@@ -583,6 +602,7 @@ interface PengaturanRouteRouteChildren {
PengaturanAksesDanTimRoute: typeof PengaturanAksesDanTimRoute
PengaturanKeamananRoute: typeof PengaturanKeamananRoute
PengaturanNotifikasiRoute: typeof PengaturanNotifikasiRoute
PengaturanSinkronisasiRoute: typeof PengaturanSinkronisasiRoute
PengaturanUmumRoute: typeof PengaturanUmumRoute
}
@@ -590,6 +610,7 @@ const PengaturanRouteRouteChildren: PengaturanRouteRouteChildren = {
PengaturanAksesDanTimRoute: PengaturanAksesDanTimRoute,
PengaturanKeamananRoute: PengaturanKeamananRoute,
PengaturanNotifikasiRoute: PengaturanNotifikasiRoute,
PengaturanSinkronisasiRoute: PengaturanSinkronisasiRoute,
PengaturanUmumRoute: PengaturanUmumRoute,
}

View File

@@ -0,0 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import SinkronisasiSettings from "@/components/pengaturan/sinkronisasi";
export const Route = createFileRoute("/pengaturan/sinkronisasi")({
component: SinkronisasiSettings,
});

View File

@@ -38,25 +38,24 @@ function ProfileLayout() {
paddingRight: "1rem",
}}
>
<Group h="100%" justify="space-between">
<Group>
<Button
variant="subtle"
color="gray"
leftSection={<IconChevronLeft size={16} />}
onClick={() => navigate({ to: "/" })}
>
Kembali ke Dashboard
</Button>
</Group>
<Text fw={700} size="lg" c="orange.6">
PENGATURAN AKUN
</Text>
<Box w={150} />
<Group h="100%" justify="space-between">
<Group>
<Button
variant="subtle"
color="gray"
leftSection={<IconChevronLeft size={16} />}
onClick={() => navigate({ to: "/" })}
>
Kembali ke Dashboard
</Button>
</Group>
<Text fw={700} size="lg" c="orange.6">
PENGATURAN AKUN
</Text>
<Box w={150} />
</Group>
</AppShell.Header>
<AppShell.Main>

View File

@@ -0,0 +1,19 @@
import createClient from "openapi-fetch";
import type { paths } from "../../generated/noc-external";
import { getEnv } from "./env";
/**
* NOC External Client
* Digunakan khusus untuk menarik data dari server NOC darmasaba.muku.id
*/
const externalBaseUrl = getEnv(
"NOC_API_URL",
"https://darmasaba.muku.id/api/noc",
);
// Hilangkan suffix /docs/json jika ada di URL
const cleanBaseUrl = externalBaseUrl.replace("/docs/json", "");
export const nocExternalClient = createClient<paths>({
baseUrl: cleanBaseUrl,
});