diff --git a/__tests__/api/noc.test.ts b/__tests__/api/noc.test.ts index 01a7cd4..283ab07 100644 --- a/__tests__/api/noc.test.ts +++ b/__tests__/api/noc.test.ts @@ -3,7 +3,7 @@ import api from "@/api"; import { prisma } from "@/utils/db"; describe("NOC API Module", () => { - const idDesa = "darmasaba"; + const idDesa = "desa1"; it("should return last sync timestamp", async () => { const response = await api.handle( diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e92ee40..e47612e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -43,7 +43,7 @@ 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 + villageId String? @default("desa1") // ID Desa dari sistem NOC name String @unique description String? color String @default("#1E3A5F") @@ -63,7 +63,7 @@ model Division { model Activity { id String @id @default(cuid()) externalId String? @unique // ID asli dari server NOC - villageId String? @default("darmasaba") + villageId String? @default("desa1") title String description String? divisionId String @@ -88,7 +88,7 @@ model Activity { model Document { id String @id @default(cuid()) externalId String? @unique // ID asli dari server NOC - villageId String? @default("darmasaba") + villageId String? @default("desa1") title String category DocumentCategory type String // "Gambar", "Dokumen", "PDF", etc @@ -109,7 +109,7 @@ model Document { model Discussion { id String @id @default(cuid()) externalId String? @unique // ID asli dari server NOC - villageId String? @default("darmasaba") + villageId String? @default("desa1") message String senderId String parentId String? // For threaded discussions @@ -131,7 +131,7 @@ model Discussion { model Event { id String @id @default(cuid()) externalId String? @unique // ID asli dari server NOC - villageId String? @default("darmasaba") + villageId String? @default("desa1") title String description String? eventType EventType diff --git a/scripts/inspect-noc-data.ts b/scripts/inspect-noc-data.ts new file mode 100644 index 0000000..069e281 --- /dev/null +++ b/scripts/inspect-noc-data.ts @@ -0,0 +1,38 @@ +import { nocExternalClient } from "../src/utils/noc-external-client"; + +async function inspect() { + const ID_DESA = "desa1"; + console.log("Checking NOC API Data structure..."); + + const endpoints = [ + "/api/noc/active-divisions", + "/api/noc/latest-projects", + "/api/noc/upcoming-events", + "/api/noc/latest-discussion" + ]; + + for (const endpoint of endpoints) { + console.log(`\n--- Endpoint: ${endpoint} ---`); + try { + const { data, error } = await (nocExternalClient as any).GET(endpoint, { + params: { query: { idDesa: ID_DESA, limit: "1" } } + }); + + if (error) { + console.error(`Error fetching ${endpoint}:`, error); + continue; + } + + if (data && data.data && data.data.length > 0) { + console.log("Sample Data Object Keys:", Object.keys(data.data[0])); + console.log("Sample Data Object Values:", JSON.stringify(data.data[0], null, 2)); + } else { + console.log("No data returned or data is empty."); + } + } catch (err) { + console.error(`Failed to fetch ${endpoint}:`, err); + } + } +} + +inspect(); diff --git a/scripts/reset-noc-data.ts b/scripts/reset-noc-data.ts new file mode 100644 index 0000000..77214b7 --- /dev/null +++ b/scripts/reset-noc-data.ts @@ -0,0 +1,38 @@ +import { prisma } from "../src/utils/db"; +import logger from "../src/utils/logger"; + +async function resetNocData() { + try { + logger.info("Starting NOC Data Reset..."); + + // Delete in order to respect relations + // 1. Delete Activities (though Division cascade might handle it, let's be explicit) + const deletedActivities = await prisma.activity.deleteMany({}); + logger.info(`Deleted ${deletedActivities.count} activities`); + + // 2. Delete Documents + const deletedDocuments = await prisma.document.deleteMany({}); + logger.info(`Deleted ${deletedDocuments.count} documents`); + + // 3. Delete Discussions + const deletedDiscussions = await prisma.discussion.deleteMany({}); + logger.info(`Deleted ${deletedDiscussions.count} discussions`); + + // 4. Delete Events + const deletedEvents = await prisma.event.deleteMany({}); + logger.info(`Deleted ${deletedEvents.count} events`); + + // 5. Delete Divisions + const deletedDivisions = await prisma.division.deleteMany({}); + logger.info(`Deleted ${deletedDivisions.count} divisions`); + + logger.info("NOC Data Reset Completed Successfully"); + } catch (err) { + logger.error({ err }, "Error during NOC data reset"); + process.exit(1); + } finally { + await prisma.$disconnect(); + } +} + +resetNocData(); diff --git a/scripts/sync-noc.ts b/scripts/sync-noc.ts index ff5552a..8653b74 100644 --- a/scripts/sync-noc.ts +++ b/scripts/sync-noc.ts @@ -2,7 +2,7 @@ import { prisma } from "../src/utils/db"; import { nocExternalClient } from "../src/utils/noc-external-client"; import logger from "../src/utils/logger"; -const ID_DESA = "darmasaba"; +const ID_DESA = "desa1"; /** * Helper untuk mendapatkan system user ID untuk relasi @@ -15,7 +15,7 @@ async function getSystemUserId() { // Buat system user jika tidak ada const newUser = await prisma.user.create({ data: { - email: "system@darmasaba.id", + email: "system@desa1.id", name: "System Sync", role: "admin", }, @@ -40,19 +40,29 @@ async function syncActiveDivisions() { } // biome-ignore lint/suspicious/noExplicitAny: External API response is untyped - const divisions = (data as any).data; + const resData = (data as any).data; + const divisions = Array.isArray(resData) ? resData : (resData?.divisi || []); + + if (!Array.isArray(divisions)) { + logger.warn({ data }, "Divisions data from NOC is not an array"); + return; + } + for (const div of divisions) { + const name = div.name || div.division; + const extId = div.id || div.externalId || `div-${name.toLowerCase().replace(/\s+/g, "-")}`; + await prisma.division.upsert({ - where: { externalId: div.id }, + where: { name: name }, update: { - name: div.name, - color: div.color, + externalId: extId, + color: div.color || "#1E3A5F", villageId: ID_DESA, }, create: { - externalId: div.id, - name: div.name, - color: div.color, + externalId: extId, + name: name, + color: div.color || "#1E3A5F", villageId: ID_DESA, }, }); @@ -75,30 +85,39 @@ async function syncLatestProjects() { } // biome-ignore lint/suspicious/noExplicitAny: External API response - const projects = (data as any).data; + const resData = (data as any).data; + const projects = Array.isArray(resData) ? resData : (resData?.projects || []); + + if (!Array.isArray(projects)) { + logger.warn({ data }, "Projects data from NOC is not an array"); + return; + } + 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 extId = proj.id || proj.externalId || `proj-${proj.title.toLowerCase().replace(/\s+/g, "-")}`; + + // Temukan divisi lokal berdasarkan nama atau externalId + const divisionName = proj.divisionName || proj.group; const division = await prisma.division.findFirst({ - where: { name: proj.divisionName }, + where: { name: divisionName }, }); if (!division) continue; await prisma.activity.upsert({ - where: { externalId: proj.id }, + where: { externalId: extId }, update: { title: proj.title, - status: proj.status as any, - progress: proj.progress, + status: (typeof proj.status === 'number' ? (proj.status === 2 ? 'Completed' : 'OnProgress') : proj.status) as any, + progress: proj.progress || (proj.status === 2 ? 100 : 50), divisionId: division.id, villageId: ID_DESA, }, create: { - externalId: proj.id, + externalId: extId, title: proj.title, - status: proj.status as any, - progress: proj.progress, + status: (typeof proj.status === 'number' ? (proj.status === 2 ? 'Completed' : 'OnProgress') : proj.status) as any, + progress: proj.progress || (proj.status === 2 ? 100 : 50), divisionId: division.id, villageId: ID_DESA, }, @@ -123,23 +142,31 @@ async function syncUpcomingEvents() { } // biome-ignore lint/suspicious/noExplicitAny: External API response - const events = (data as any).data; + const resData = (data as any).data; + let events: any[] = []; + if (Array.isArray(resData)) { + events = resData; + } else if (resData?.today || resData?.upcoming) { + events = [...(resData.today || []), ...(resData.upcoming || [])]; + } + for (const event of events) { + const extId = event.id || event.externalId || `event-${event.title.toLowerCase().replace(/\s+/g, "-")}`; await prisma.event.upsert({ - where: { externalId: event.id }, + where: { externalId: extId }, update: { title: event.title, - startDate: new Date(event.startDate), - location: event.location, - eventType: event.eventType as any, + startDate: new Date(event.startDate || event.date), + location: event.location || "N/A", + eventType: (event.eventType || "Meeting") as any, villageId: ID_DESA, }, create: { - externalId: event.id, + externalId: extId, title: event.title, - startDate: new Date(event.startDate), - location: event.location, - eventType: event.eventType as any, + startDate: new Date(event.startDate || event.date), + location: event.location || "N/A", + eventType: (event.eventType || "Meeting") as any, createdBy: systemUserId, villageId: ID_DESA, }, @@ -164,22 +191,29 @@ async function syncLatestDiscussion() { } // biome-ignore lint/suspicious/noExplicitAny: External API response - const discussions = (data as any).data; + const resData = (data as any).data; + const discussions = Array.isArray(resData) ? resData : (resData?.discussions || resData?.data || []); + + if (!Array.isArray(discussions)) { + logger.warn({ data }, "Discussions data from NOC is not an array"); + return; + } + for (const disc of discussions) { const division = await prisma.division.findFirst({ - where: { name: disc.divisionName }, + where: { name: disc.divisionName || disc.group }, }); await prisma.discussion.upsert({ where: { externalId: disc.id }, update: { - message: disc.message, + message: disc.message || disc.desc || disc.title, divisionId: division?.id, villageId: ID_DESA, }, create: { externalId: disc.id, - message: disc.message, + message: disc.message || disc.desc || disc.title, senderId: systemUserId, divisionId: division?.id, villageId: ID_DESA, diff --git a/src/components/pengaturan/sinkronisasi.tsx b/src/components/pengaturan/sinkronisasi.tsx index 1fa3939..0d8064b 100644 --- a/src/components/pengaturan/sinkronisasi.tsx +++ b/src/components/pengaturan/sinkronisasi.tsx @@ -31,7 +31,7 @@ const SinkronisasiSettings = () => { const fetchLastSync = async () => { const { data } = await apiClient.GET("/api/noc/last-sync", { - params: { query: { idDesa: "darmasaba" } }, + params: { query: { idDesa: "desa1" } }, }); if (data?.lastSyncedAt) { setLastSync(data.lastSyncedAt); @@ -158,7 +158,7 @@ const SinkronisasiSettings = () => { ID Desa: - darmasaba + desa1 Model Data: diff --git a/src/utils/noc-external-client.ts b/src/utils/noc-external-client.ts index d892ec2..d5e5d9c 100644 --- a/src/utils/noc-external-client.ts +++ b/src/utils/noc-external-client.ts @@ -6,13 +6,14 @@ 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", -); +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", ""); +// Hilangkan path dokumentasi dan prefix /api/noc jika ada di URL base, +// karena 'paths' di generated/noc-external.ts sudah menyertakan prefix /api/noc +const cleanBaseUrl = externalBaseUrl + .replace("/api/noc/docs/json", "") + .replace("/docs/json", "") + .replace(/\/api\/noc\/?$/, ""); export const nocExternalClient = createClient({ baseUrl: cleanBaseUrl,