tasks/noc-integration/update-village-id-to-desa1-and-fix-sync-logic/20260330-1522
This commit is contained in:
@@ -3,7 +3,7 @@ import api from "@/api";
|
|||||||
import { prisma } from "@/utils/db";
|
import { prisma } from "@/utils/db";
|
||||||
|
|
||||||
describe("NOC API Module", () => {
|
describe("NOC API Module", () => {
|
||||||
const idDesa = "darmasaba";
|
const idDesa = "desa1";
|
||||||
|
|
||||||
it("should return last sync timestamp", async () => {
|
it("should return last sync timestamp", async () => {
|
||||||
const response = await api.handle(
|
const response = await api.handle(
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ model User {
|
|||||||
model Division {
|
model Division {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
externalId String? @unique // ID asli dari server NOC
|
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
|
name String @unique
|
||||||
description String?
|
description String?
|
||||||
color String @default("#1E3A5F")
|
color String @default("#1E3A5F")
|
||||||
@@ -63,7 +63,7 @@ model Division {
|
|||||||
model Activity {
|
model Activity {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
externalId String? @unique // ID asli dari server NOC
|
externalId String? @unique // ID asli dari server NOC
|
||||||
villageId String? @default("darmasaba")
|
villageId String? @default("desa1")
|
||||||
title String
|
title String
|
||||||
description String?
|
description String?
|
||||||
divisionId String
|
divisionId String
|
||||||
@@ -88,7 +88,7 @@ model Activity {
|
|||||||
model Document {
|
model Document {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
externalId String? @unique // ID asli dari server NOC
|
externalId String? @unique // ID asli dari server NOC
|
||||||
villageId String? @default("darmasaba")
|
villageId String? @default("desa1")
|
||||||
title String
|
title String
|
||||||
category DocumentCategory
|
category DocumentCategory
|
||||||
type String // "Gambar", "Dokumen", "PDF", etc
|
type String // "Gambar", "Dokumen", "PDF", etc
|
||||||
@@ -109,7 +109,7 @@ model Document {
|
|||||||
model Discussion {
|
model Discussion {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
externalId String? @unique // ID asli dari server NOC
|
externalId String? @unique // ID asli dari server NOC
|
||||||
villageId String? @default("darmasaba")
|
villageId String? @default("desa1")
|
||||||
message String
|
message String
|
||||||
senderId String
|
senderId String
|
||||||
parentId String? // For threaded discussions
|
parentId String? // For threaded discussions
|
||||||
@@ -131,7 +131,7 @@ model Discussion {
|
|||||||
model Event {
|
model Event {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
externalId String? @unique // ID asli dari server NOC
|
externalId String? @unique // ID asli dari server NOC
|
||||||
villageId String? @default("darmasaba")
|
villageId String? @default("desa1")
|
||||||
title String
|
title String
|
||||||
description String?
|
description String?
|
||||||
eventType EventType
|
eventType EventType
|
||||||
|
|||||||
38
scripts/inspect-noc-data.ts
Normal file
38
scripts/inspect-noc-data.ts
Normal file
@@ -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();
|
||||||
38
scripts/reset-noc-data.ts
Normal file
38
scripts/reset-noc-data.ts
Normal file
@@ -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();
|
||||||
@@ -2,7 +2,7 @@ import { prisma } from "../src/utils/db";
|
|||||||
import { nocExternalClient } from "../src/utils/noc-external-client";
|
import { nocExternalClient } from "../src/utils/noc-external-client";
|
||||||
import logger from "../src/utils/logger";
|
import logger from "../src/utils/logger";
|
||||||
|
|
||||||
const ID_DESA = "darmasaba";
|
const ID_DESA = "desa1";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper untuk mendapatkan system user ID untuk relasi
|
* Helper untuk mendapatkan system user ID untuk relasi
|
||||||
@@ -15,7 +15,7 @@ async function getSystemUserId() {
|
|||||||
// Buat system user jika tidak ada
|
// Buat system user jika tidak ada
|
||||||
const newUser = await prisma.user.create({
|
const newUser = await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
email: "system@darmasaba.id",
|
email: "system@desa1.id",
|
||||||
name: "System Sync",
|
name: "System Sync",
|
||||||
role: "admin",
|
role: "admin",
|
||||||
},
|
},
|
||||||
@@ -40,19 +40,29 @@ async function syncActiveDivisions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: External API response is untyped
|
// 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) {
|
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({
|
await prisma.division.upsert({
|
||||||
where: { externalId: div.id },
|
where: { name: name },
|
||||||
update: {
|
update: {
|
||||||
name: div.name,
|
externalId: extId,
|
||||||
color: div.color,
|
color: div.color || "#1E3A5F",
|
||||||
villageId: ID_DESA,
|
villageId: ID_DESA,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
externalId: div.id,
|
externalId: extId,
|
||||||
name: div.name,
|
name: name,
|
||||||
color: div.color,
|
color: div.color || "#1E3A5F",
|
||||||
villageId: ID_DESA,
|
villageId: ID_DESA,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -75,30 +85,39 @@ async function syncLatestProjects() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: External API response
|
// 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) {
|
for (const proj of projects) {
|
||||||
// Temukan divisi lokal berdasarkan nama atau externalId (asumsi externalId divisi sinkron)
|
const extId = proj.id || proj.externalId || `proj-${proj.title.toLowerCase().replace(/\s+/g, "-")}`;
|
||||||
// Karena kita sinkron divisi dulu, kita cari berdasarkan nama jika externalId belum pasti
|
|
||||||
|
// Temukan divisi lokal berdasarkan nama atau externalId
|
||||||
|
const divisionName = proj.divisionName || proj.group;
|
||||||
const division = await prisma.division.findFirst({
|
const division = await prisma.division.findFirst({
|
||||||
where: { name: proj.divisionName },
|
where: { name: divisionName },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!division) continue;
|
if (!division) continue;
|
||||||
|
|
||||||
await prisma.activity.upsert({
|
await prisma.activity.upsert({
|
||||||
where: { externalId: proj.id },
|
where: { externalId: extId },
|
||||||
update: {
|
update: {
|
||||||
title: proj.title,
|
title: proj.title,
|
||||||
status: proj.status as any,
|
status: (typeof proj.status === 'number' ? (proj.status === 2 ? 'Completed' : 'OnProgress') : proj.status) as any,
|
||||||
progress: proj.progress,
|
progress: proj.progress || (proj.status === 2 ? 100 : 50),
|
||||||
divisionId: division.id,
|
divisionId: division.id,
|
||||||
villageId: ID_DESA,
|
villageId: ID_DESA,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
externalId: proj.id,
|
externalId: extId,
|
||||||
title: proj.title,
|
title: proj.title,
|
||||||
status: proj.status as any,
|
status: (typeof proj.status === 'number' ? (proj.status === 2 ? 'Completed' : 'OnProgress') : proj.status) as any,
|
||||||
progress: proj.progress,
|
progress: proj.progress || (proj.status === 2 ? 100 : 50),
|
||||||
divisionId: division.id,
|
divisionId: division.id,
|
||||||
villageId: ID_DESA,
|
villageId: ID_DESA,
|
||||||
},
|
},
|
||||||
@@ -123,23 +142,31 @@ async function syncUpcomingEvents() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: External API response
|
// 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) {
|
for (const event of events) {
|
||||||
|
const extId = event.id || event.externalId || `event-${event.title.toLowerCase().replace(/\s+/g, "-")}`;
|
||||||
await prisma.event.upsert({
|
await prisma.event.upsert({
|
||||||
where: { externalId: event.id },
|
where: { externalId: extId },
|
||||||
update: {
|
update: {
|
||||||
title: event.title,
|
title: event.title,
|
||||||
startDate: new Date(event.startDate),
|
startDate: new Date(event.startDate || event.date),
|
||||||
location: event.location,
|
location: event.location || "N/A",
|
||||||
eventType: event.eventType as any,
|
eventType: (event.eventType || "Meeting") as any,
|
||||||
villageId: ID_DESA,
|
villageId: ID_DESA,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
externalId: event.id,
|
externalId: extId,
|
||||||
title: event.title,
|
title: event.title,
|
||||||
startDate: new Date(event.startDate),
|
startDate: new Date(event.startDate || event.date),
|
||||||
location: event.location,
|
location: event.location || "N/A",
|
||||||
eventType: event.eventType as any,
|
eventType: (event.eventType || "Meeting") as any,
|
||||||
createdBy: systemUserId,
|
createdBy: systemUserId,
|
||||||
villageId: ID_DESA,
|
villageId: ID_DESA,
|
||||||
},
|
},
|
||||||
@@ -164,22 +191,29 @@ async function syncLatestDiscussion() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: External API response
|
// 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) {
|
for (const disc of discussions) {
|
||||||
const division = await prisma.division.findFirst({
|
const division = await prisma.division.findFirst({
|
||||||
where: { name: disc.divisionName },
|
where: { name: disc.divisionName || disc.group },
|
||||||
});
|
});
|
||||||
|
|
||||||
await prisma.discussion.upsert({
|
await prisma.discussion.upsert({
|
||||||
where: { externalId: disc.id },
|
where: { externalId: disc.id },
|
||||||
update: {
|
update: {
|
||||||
message: disc.message,
|
message: disc.message || disc.desc || disc.title,
|
||||||
divisionId: division?.id,
|
divisionId: division?.id,
|
||||||
villageId: ID_DESA,
|
villageId: ID_DESA,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
externalId: disc.id,
|
externalId: disc.id,
|
||||||
message: disc.message,
|
message: disc.message || disc.desc || disc.title,
|
||||||
senderId: systemUserId,
|
senderId: systemUserId,
|
||||||
divisionId: division?.id,
|
divisionId: division?.id,
|
||||||
villageId: ID_DESA,
|
villageId: ID_DESA,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const SinkronisasiSettings = () => {
|
|||||||
|
|
||||||
const fetchLastSync = async () => {
|
const fetchLastSync = async () => {
|
||||||
const { data } = await apiClient.GET("/api/noc/last-sync", {
|
const { data } = await apiClient.GET("/api/noc/last-sync", {
|
||||||
params: { query: { idDesa: "darmasaba" } },
|
params: { query: { idDesa: "desa1" } },
|
||||||
});
|
});
|
||||||
if (data?.lastSyncedAt) {
|
if (data?.lastSyncedAt) {
|
||||||
setLastSync(data.lastSyncedAt);
|
setLastSync(data.lastSyncedAt);
|
||||||
@@ -158,7 +158,7 @@ const SinkronisasiSettings = () => {
|
|||||||
</Group>
|
</Group>
|
||||||
<Group>
|
<Group>
|
||||||
<Text fw={600} size="sm" w={100}>ID Desa:</Text>
|
<Text fw={600} size="sm" w={100}>ID Desa:</Text>
|
||||||
<Text size="sm">darmasaba</Text>
|
<Text size="sm">desa1</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Group>
|
<Group>
|
||||||
<Text fw={600} size="sm" w={100}>Model Data:</Text>
|
<Text fw={600} size="sm" w={100}>Model Data:</Text>
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ import { getEnv } from "./env";
|
|||||||
* NOC External Client
|
* NOC External Client
|
||||||
* Digunakan khusus untuk menarik data dari server NOC darmasaba.muku.id
|
* Digunakan khusus untuk menarik data dari server NOC darmasaba.muku.id
|
||||||
*/
|
*/
|
||||||
const externalBaseUrl = getEnv(
|
const externalBaseUrl = getEnv("NOC_API_URL", "https://darmasaba.muku.id/api/noc");
|
||||||
"NOC_API_URL",
|
|
||||||
"https://darmasaba.muku.id/api/noc",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Hilangkan suffix /docs/json jika ada di URL
|
// Hilangkan path dokumentasi dan prefix /api/noc jika ada di URL base,
|
||||||
const cleanBaseUrl = externalBaseUrl.replace("/docs/json", "");
|
// 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<paths>({
|
export const nocExternalClient = createClient<paths>({
|
||||||
baseUrl: cleanBaseUrl,
|
baseUrl: cleanBaseUrl,
|
||||||
|
|||||||
Reference in New Issue
Block a user