feat(noc): implement sync management UI and backend integration
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(bun *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
"permissions": {
|
||||
"allow": ["Bash(bun *)"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
855
generated/api.ts
855
generated/api.ts
@@ -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
269
generated/noc-external.ts
Normal 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
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "division" ADD COLUMN "lastSyncedAt" TIMESTAMP(3);
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PrismaClient, Gender, Religion } from "../../generated/prisma";
|
||||
import { Gender, PrismaClient, Religion } from "../../generated/prisma";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
ActivityStatus,
|
||||
Priority,
|
||||
PrismaClient,
|
||||
} from "../../generated/prisma";
|
||||
import { ActivityStatus, Priority, PrismaClient } from "../../generated/prisma";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
|
||||
@@ -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
225
scripts/sync-noc.ts
Normal 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();
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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" })
|
||||
),
|
||||
}),
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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),
|
||||
}));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
176
src/components/pengaturan/sinkronisasi.tsx
Normal file
176
src/components/pengaturan/sinkronisasi.tsx
Normal 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;
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
6
src/routes/pengaturan/sinkronisasi.tsx
Normal file
6
src/routes/pengaturan/sinkronisasi.tsx
Normal 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,
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
19
src/utils/noc-external-client.ts
Normal file
19
src/utils/noc-external-client.ts
Normal 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,
|
||||
});
|
||||
Reference in New Issue
Block a user