diff --git a/.qwen/settings.json b/.qwen/settings.json index 9cb3001..d3df730 100644 --- a/.qwen/settings.json +++ b/.qwen/settings.json @@ -1,7 +1,5 @@ { - "permissions": { - "allow": [ - "Bash(bun *)" - ] - } -} \ No newline at end of file + "permissions": { + "allow": ["Bash(bun *)"] + } +} diff --git a/__tests__/api/noc.test.ts b/__tests__/api/noc.test.ts index 0291e35..8b4f67d 100644 --- a/__tests__/api/noc.test.ts +++ b/__tests__/api/noc.test.ts @@ -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(); diff --git a/generated/api.ts b/generated/api.ts index 3909eed..7327ee1 100644 --- a/generated/api.ts +++ b/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; 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; - }[]; - }; - }; - }; - }; - }; } diff --git a/generated/noc-external.ts b/generated/noc-external.ts new file mode 100644 index 0000000..a4ce390 --- /dev/null +++ b/generated/noc-external.ts @@ -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; +export interface components { + schemas: never; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +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; + }; + }; + }; +} diff --git a/generated/schema.json b/generated/schema.json index 4b24031..47a21be 100644 --- a/generated/schema.json +++ b/generated/schema.json @@ -100,6 +100,1096 @@ "operationId": "getApiSession" } }, + "/api/noc/sync": { + "post": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "error": { + "type": "string" + }, + "lastSyncedAt": { + "type": "string" + } + }, + "required": [ + "success" + ] + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "error": { + "type": "string" + }, + "lastSyncedAt": { + "type": "string" + } + }, + "required": [ + "success" + ] + } + }, + "text/plain": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "error": { + "type": "string" + }, + "lastSyncedAt": { + "type": "string" + } + }, + "required": [ + "success" + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + }, + "text/plain": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + } + } + } + }, + "operationId": "postApiNocSync" + } + }, + "/api/noc/last-sync": { + "get": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "idDesa", + "required": true + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "lastSyncedAt": { + "nullable": true, + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "lastSyncedAt" + ] + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "lastSyncedAt": { + "nullable": true, + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "lastSyncedAt" + ] + } + }, + "text/plain": { + "schema": { + "type": "object", + "properties": { + "lastSyncedAt": { + "nullable": true, + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "lastSyncedAt" + ] + } + } + } + } + }, + "operationId": "getApiNocLast-sync" + } + }, + "/api/noc/active-divisions": { + "get": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "idDesa", + "required": true + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "limit", + "required": false + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "name", + "activityCount", + "color" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "activityCount": { + "type": "number" + }, + "color": { + "type": "string" + } + } + } + } + }, + "required": [ + "data" + ] + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "name", + "activityCount", + "color" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "activityCount": { + "type": "number" + }, + "color": { + "type": "string" + } + } + } + } + }, + "required": [ + "data" + ] + } + }, + "text/plain": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "name", + "activityCount", + "color" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "activityCount": { + "type": "number" + }, + "color": { + "type": "string" + } + } + } + } + }, + "required": [ + "data" + ] + } + } + } + } + }, + "operationId": "getApiNocActive-divisions" + } + }, + "/api/noc/latest-projects": { + "get": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "idDesa", + "required": true + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "limit", + "required": false + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "title", + "status", + "progress", + "divisionName", + "createdAt" + ], + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "status": { + "type": "string" + }, + "progress": { + "type": "number" + }, + "divisionName": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + } + } + } + }, + "required": [ + "data" + ] + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "title", + "status", + "progress", + "divisionName", + "createdAt" + ], + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "status": { + "type": "string" + }, + "progress": { + "type": "number" + }, + "divisionName": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + } + } + } + }, + "required": [ + "data" + ] + } + }, + "text/plain": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "title", + "status", + "progress", + "divisionName", + "createdAt" + ], + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "status": { + "type": "string" + }, + "progress": { + "type": "number" + }, + "divisionName": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + } + } + } + }, + "required": [ + "data" + ] + } + } + } + } + }, + "operationId": "getApiNocLatest-projects" + } + }, + "/api/noc/upcoming-events": { + "get": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "idDesa", + "required": true + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "limit", + "required": false + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "filter", + "required": false + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "title", + "startDate", + "location", + "eventType" + ], + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "startDate": { + "type": "string" + }, + "location": { + "nullable": true, + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "eventType": { + "type": "string" + } + } + } + } + }, + "required": [ + "data" + ] + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "title", + "startDate", + "location", + "eventType" + ], + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "startDate": { + "type": "string" + }, + "location": { + "nullable": true, + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "eventType": { + "type": "string" + } + } + } + } + }, + "required": [ + "data" + ] + } + }, + "text/plain": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "title", + "startDate", + "location", + "eventType" + ], + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "startDate": { + "type": "string" + }, + "location": { + "nullable": true, + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "eventType": { + "type": "string" + } + } + } + } + }, + "required": [ + "data" + ] + } + } + } + } + }, + "operationId": "getApiNocUpcoming-events" + } + }, + "/api/noc/diagram-jumlah-document": { + "get": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "idDesa", + "required": true + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "category", + "count" + ], + "properties": { + "category": { + "type": "string" + }, + "count": { + "type": "number" + } + } + } + } + }, + "required": [ + "data" + ] + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "category", + "count" + ], + "properties": { + "category": { + "type": "string" + }, + "count": { + "type": "number" + } + } + } + } + }, + "required": [ + "data" + ] + } + }, + "text/plain": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "category", + "count" + ], + "properties": { + "category": { + "type": "string" + }, + "count": { + "type": "number" + } + } + } + } + }, + "required": [ + "data" + ] + } + } + } + } + }, + "operationId": "getApiNocDiagram-jumlah-document" + } + }, + "/api/noc/diagram-progres-kegiatan": { + "get": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "idDesa", + "required": true + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "status", + "avgProgress", + "count" + ], + "properties": { + "status": { + "type": "string" + }, + "avgProgress": { + "type": "number" + }, + "count": { + "type": "number" + } + } + } + } + }, + "required": [ + "data" + ] + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "status", + "avgProgress", + "count" + ], + "properties": { + "status": { + "type": "string" + }, + "avgProgress": { + "type": "number" + }, + "count": { + "type": "number" + } + } + } + } + }, + "required": [ + "data" + ] + } + }, + "text/plain": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "status", + "avgProgress", + "count" + ], + "properties": { + "status": { + "type": "string" + }, + "avgProgress": { + "type": "number" + }, + "count": { + "type": "number" + } + } + } + } + }, + "required": [ + "data" + ] + } + } + } + } + }, + "operationId": "getApiNocDiagram-progres-kegiatan" + } + }, + "/api/noc/latest-discussion": { + "get": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "idDesa", + "required": true + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "limit", + "required": false + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "message", + "senderName", + "senderImage", + "divisionName", + "createdAt" + ], + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + }, + "senderName": { + "type": "string" + }, + "senderImage": { + "nullable": true, + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "divisionName": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + } + } + } + }, + "required": [ + "data" + ] + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "message", + "senderName", + "senderImage", + "divisionName", + "createdAt" + ], + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + }, + "senderName": { + "type": "string" + }, + "senderImage": { + "nullable": true, + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "divisionName": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + } + } + } + }, + "required": [ + "data" + ] + } + }, + "text/plain": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "message", + "senderName", + "senderImage", + "divisionName", + "createdAt" + ], + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + }, + "senderName": { + "type": "string" + }, + "senderImage": { + "nullable": true, + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "divisionName": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + } + } + } + }, + "required": [ + "data" + ] + } + } + } + } + }, + "operationId": "getApiNocLatest-discussion" + } + }, "/api/apikey/": { "get": { "responses": { @@ -4022,892 +5112,6 @@ }, "operationId": "getApiDashboardSatisfaction" } - }, - "/api/noc/active-divisions": { - "get": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "in": "query", - "name": "idDesa", - "required": true - }, - { - "schema": { - "type": "string" - }, - "in": "query", - "name": "limit", - "required": false - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "name", - "activityCount", - "color" - ], - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "activityCount": { - "type": "number" - }, - "color": { - "type": "string" - } - } - } - } - }, - "required": [ - "data" - ] - } - }, - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "name", - "activityCount", - "color" - ], - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "activityCount": { - "type": "number" - }, - "color": { - "type": "string" - } - } - } - } - }, - "required": [ - "data" - ] - } - }, - "text/plain": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "name", - "activityCount", - "color" - ], - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "activityCount": { - "type": "number" - }, - "color": { - "type": "string" - } - } - } - } - }, - "required": [ - "data" - ] - } - } - } - } - }, - "operationId": "getApiNocActive-divisions" - } - }, - "/api/noc/latest-projects": { - "get": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "in": "query", - "name": "idDesa", - "required": true - }, - { - "schema": { - "type": "string" - }, - "in": "query", - "name": "limit", - "required": false - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "title", - "status", - "progress", - "divisionName", - "createdAt" - ], - "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "status": { - "type": "string" - }, - "progress": { - "type": "number" - }, - "divisionName": { - "type": "string" - }, - "createdAt": { - "type": "string" - } - } - } - } - }, - "required": [ - "data" - ] - } - }, - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "title", - "status", - "progress", - "divisionName", - "createdAt" - ], - "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "status": { - "type": "string" - }, - "progress": { - "type": "number" - }, - "divisionName": { - "type": "string" - }, - "createdAt": { - "type": "string" - } - } - } - } - }, - "required": [ - "data" - ] - } - }, - "text/plain": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "title", - "status", - "progress", - "divisionName", - "createdAt" - ], - "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "status": { - "type": "string" - }, - "progress": { - "type": "number" - }, - "divisionName": { - "type": "string" - }, - "createdAt": { - "type": "string" - } - } - } - } - }, - "required": [ - "data" - ] - } - } - } - } - }, - "operationId": "getApiNocLatest-projects" - } - }, - "/api/noc/upcoming-events": { - "get": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "in": "query", - "name": "idDesa", - "required": true - }, - { - "schema": { - "type": "string" - }, - "in": "query", - "name": "limit", - "required": false - }, - { - "schema": { - "type": "string" - }, - "in": "query", - "name": "filter", - "required": false - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "title", - "startDate", - "location", - "eventType" - ], - "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "startDate": { - "type": "string" - }, - "location": { - "nullable": true, - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "eventType": { - "type": "string" - } - } - } - } - }, - "required": [ - "data" - ] - } - }, - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "title", - "startDate", - "location", - "eventType" - ], - "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "startDate": { - "type": "string" - }, - "location": { - "nullable": true, - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "eventType": { - "type": "string" - } - } - } - } - }, - "required": [ - "data" - ] - } - }, - "text/plain": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "title", - "startDate", - "location", - "eventType" - ], - "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "startDate": { - "type": "string" - }, - "location": { - "nullable": true, - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "eventType": { - "type": "string" - } - } - } - } - }, - "required": [ - "data" - ] - } - } - } - } - }, - "operationId": "getApiNocUpcoming-events" - } - }, - "/api/noc/diagram-jumlah-document": { - "get": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "in": "query", - "name": "idDesa", - "required": true - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "required": [ - "category", - "count" - ], - "properties": { - "category": { - "type": "string" - }, - "count": { - "type": "number" - } - } - } - } - }, - "required": [ - "data" - ] - } - }, - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "required": [ - "category", - "count" - ], - "properties": { - "category": { - "type": "string" - }, - "count": { - "type": "number" - } - } - } - } - }, - "required": [ - "data" - ] - } - }, - "text/plain": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "required": [ - "category", - "count" - ], - "properties": { - "category": { - "type": "string" - }, - "count": { - "type": "number" - } - } - } - } - }, - "required": [ - "data" - ] - } - } - } - } - }, - "operationId": "getApiNocDiagram-jumlah-document" - } - }, - "/api/noc/diagram-progres-kegiatan": { - "get": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "in": "query", - "name": "idDesa", - "required": true - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "required": [ - "status", - "avgProgress", - "count" - ], - "properties": { - "status": { - "type": "string" - }, - "avgProgress": { - "type": "number" - }, - "count": { - "type": "number" - } - } - } - } - }, - "required": [ - "data" - ] - } - }, - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "required": [ - "status", - "avgProgress", - "count" - ], - "properties": { - "status": { - "type": "string" - }, - "avgProgress": { - "type": "number" - }, - "count": { - "type": "number" - } - } - } - } - }, - "required": [ - "data" - ] - } - }, - "text/plain": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "required": [ - "status", - "avgProgress", - "count" - ], - "properties": { - "status": { - "type": "string" - }, - "avgProgress": { - "type": "number" - }, - "count": { - "type": "number" - } - } - } - } - }, - "required": [ - "data" - ] - } - } - } - } - }, - "operationId": "getApiNocDiagram-progres-kegiatan" - } - }, - "/api/noc/latest-discussion": { - "get": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "in": "query", - "name": "idDesa", - "required": true - }, - { - "schema": { - "type": "string" - }, - "in": "query", - "name": "limit", - "required": false - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "message", - "senderName", - "senderImage", - "divisionName", - "createdAt" - ], - "properties": { - "id": { - "type": "string" - }, - "message": { - "type": "string" - }, - "senderName": { - "type": "string" - }, - "senderImage": { - "nullable": true, - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "divisionName": { - "type": "string" - }, - "createdAt": { - "type": "string" - } - } - } - } - }, - "required": [ - "data" - ] - } - }, - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "message", - "senderName", - "senderImage", - "divisionName", - "createdAt" - ], - "properties": { - "id": { - "type": "string" - }, - "message": { - "type": "string" - }, - "senderName": { - "type": "string" - }, - "senderImage": { - "nullable": true, - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "divisionName": { - "type": "string" - }, - "createdAt": { - "type": "string" - } - } - } - } - }, - "required": [ - "data" - ] - } - }, - "text/plain": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "message", - "senderName", - "senderImage", - "divisionName", - "createdAt" - ], - "properties": { - "id": { - "type": "string" - }, - "message": { - "type": "string" - }, - "senderName": { - "type": "string" - }, - "senderImage": { - "nullable": true, - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "divisionName": { - "type": "string" - }, - "createdAt": { - "type": "string" - } - } - } - } - }, - "required": [ - "data" - ] - } - } - } - } - }, - "operationId": "getApiNocLatest-discussion" - } } }, "components": { diff --git a/package.json b/package.json index 91dd8a2..6b5446b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/prisma/migrations/20260330063333_add_noc_sync_fields/migration.sql b/prisma/migrations/20260330063333_add_noc_sync_fields/migration.sql new file mode 100644 index 0000000..600376a --- /dev/null +++ b/prisma/migrations/20260330063333_add_noc_sync_fields/migration.sql @@ -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"); diff --git a/prisma/migrations/20260330064557_add_last_synced_at_to_division/migration.sql b/prisma/migrations/20260330064557_add_last_synced_at_to_division/migration.sql new file mode 100644 index 0000000..f4af6a1 --- /dev/null +++ b/prisma/migrations/20260330064557_add_last_synced_at_to_division/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "division" ADD COLUMN "lastSyncedAt" TIMESTAMP(3); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b03d65c..e92ee40 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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 diff --git a/prisma/seed.ts b/prisma/seed.ts index 6e0195f..cee4c87 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -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); } diff --git a/prisma/seeders/seed-demographics.ts b/prisma/seeders/seed-demographics.ts index d20ca34..533c2d8 100644 --- a/prisma/seeders/seed-demographics.ts +++ b/prisma/seeders/seed-demographics.ts @@ -1,4 +1,4 @@ -import { PrismaClient, Gender, Religion } from "../../generated/prisma"; +import { Gender, PrismaClient, Religion } from "../../generated/prisma"; const prisma = new PrismaClient(); diff --git a/prisma/seeders/seed-division-performance.ts b/prisma/seeders/seed-division-performance.ts index 1163d58..8993760 100644 --- a/prisma/seeders/seed-division-performance.ts +++ b/prisma/seeders/seed-division-performance.ts @@ -1,8 +1,4 @@ -import { - ActivityStatus, - Priority, - PrismaClient, -} from "../../generated/prisma"; +import { ActivityStatus, Priority, PrismaClient } from "../../generated/prisma"; const prisma = new PrismaClient(); diff --git a/prisma/seeders/seed-public-services.ts b/prisma/seeders/seed-public-services.ts index 2fa21c9..161dac7 100644 --- a/prisma/seeders/seed-public-services.ts +++ b/prisma/seeders/seed-public-services.ts @@ -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) { diff --git a/scripts/sync-noc.ts b/scripts/sync-noc.ts new file mode 100644 index 0000000..ff5552a --- /dev/null +++ b/scripts/sync-noc.ts @@ -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(); diff --git a/src/api/index.tsx b/src/api/index.tsx index c5d999b..bd0b76a 100644 --- a/src/api/index.tsx +++ b/src/api/index.tsx @@ -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"; diff --git a/src/api/noc.ts b/src/api/noc.ts index 0c258c9..9ecf4e5 100644 --- a/src/api/noc.ts +++ b/src/api/noc.ts @@ -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" }) ), }), }, - }); + }, + ); diff --git a/src/components/dashboard-content.tsx b/src/components/dashboard-content.tsx index 5665640..41b075b 100644 --- a/src/components/dashboard-content.tsx +++ b/src/components/dashboard-content.tsx @@ -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) => ( : null} + image={ + sdg.image ? {sdg.title} : null + } title={sdg.title} score={sdg.score} /> diff --git a/src/components/dashboard/chart-apbdes.tsx b/src/components/dashboard/chart-apbdes.tsx index 318ab5b..15ea36f 100644 --- a/src/components/dashboard/chart-apbdes.tsx +++ b/src/components/dashboard/chart-apbdes.tsx @@ -96,7 +96,13 @@ export function ChartAPBDes() { - + {item.value}% diff --git a/src/components/dashboard/chart-surat.tsx b/src/components/dashboard/chart-surat.tsx index 3402a32..d033b5d 100644 --- a/src/components/dashboard/chart-surat.tsx +++ b/src/components/dashboard/chart-surat.tsx @@ -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), })); diff --git a/src/components/kinerja-divisi/discussion-panel.tsx b/src/components/kinerja-divisi/discussion-panel.tsx index 894634f..ae83cc6 100644 --- a/src/components/kinerja-divisi/discussion-panel.tsx +++ b/src/components/kinerja-divisi/discussion-panel.tsx @@ -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; diff --git a/src/components/kinerja-divisi/document-chart.tsx b/src/components/kinerja-divisi/document-chart.tsx index c281509..33efd73 100644 --- a/src/components/kinerja-divisi/document-chart.tsx +++ b/src/components/kinerja-divisi/document-chart.tsx @@ -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, diff --git a/src/components/kinerja-divisi/progress-chart.tsx b/src/components/kinerja-divisi/progress-chart.tsx index afa113a..28f4a37 100644 --- a/src/components/kinerja-divisi/progress-chart.tsx +++ b/src/components/kinerja-divisi/progress-chart.tsx @@ -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); } diff --git a/src/components/pengaturan/sinkronisasi.tsx b/src/components/pengaturan/sinkronisasi.tsx new file mode 100644 index 0000000..1fa3939 --- /dev/null +++ b/src/components/pengaturan/sinkronisasi.tsx @@ -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(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 ( + + + Sinkronisasi Data NOC + + + + Gunakan fitur ini untuk memperbarui data dashboard dengan data terbaru dari + server Network Operation Center (NOC) darmasaba.muku.id. + + + + + + + + Status Terakhir + + + {lastSync ? "Terkoneksi" : "Belum Pernah Sinkron"} + + + + + + + + Waktu Sinkronisasi Terakhir: + + + {lastSync + ? dayjs(lastSync).format("DD MMMM YYYY, HH:mm:ss") + : "Belum pernah dilakukan"} + + {lastSync && ( + + ({dayjs(lastSync).fromNow()}) + + )} + + + {status.type && ( + + ) : ( + + ) + } + title={status.type === "success" ? "Berhasil" : "Kesalahan"} + color={status.type === "success" ? "green" : "red"} + onClose={() => setStatus({ type: null, message: "" })} + withCloseButton + > + {status.message} + + )} + + + + + + + Informasi API + + + + + + URL Sumber: + https://darmasaba.muku.id/api/noc/ + + + ID Desa: + darmasaba + + + Model Data: + Divisi + Kegiatan + Event + Diskusi + + + + + ); +}; + +export default SinkronisasiSettings; diff --git a/src/components/sidebar.tsx b/src/components/sidebar.tsx index dfdd80f..1ca139c 100644 --- a/src/components/sidebar.tsx +++ b/src/components/sidebar.tsx @@ -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 diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 898d718..056cd45 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -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, } diff --git a/src/routes/pengaturan/sinkronisasi.tsx b/src/routes/pengaturan/sinkronisasi.tsx new file mode 100644 index 0000000..f9169b7 --- /dev/null +++ b/src/routes/pengaturan/sinkronisasi.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from "@tanstack/react-router"; +import SinkronisasiSettings from "@/components/pengaturan/sinkronisasi"; + +export const Route = createFileRoute("/pengaturan/sinkronisasi")({ + component: SinkronisasiSettings, +}); diff --git a/src/routes/profile/route.tsx b/src/routes/profile/route.tsx index fbece82..303326a 100644 --- a/src/routes/profile/route.tsx +++ b/src/routes/profile/route.tsx @@ -38,25 +38,24 @@ function ProfileLayout() { paddingRight: "1rem", }} > - - - - - - - - PENGATURAN AKUN - - - + + + + + + PENGATURAN AKUN + + + + diff --git a/src/utils/noc-external-client.ts b/src/utils/noc-external-client.ts new file mode 100644 index 0000000..d892ec2 --- /dev/null +++ b/src/utils/noc-external-client.ts @@ -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({ + baseUrl: cleanBaseUrl, +});