262 lines
6.9 KiB
TypeScript
262 lines
6.9 KiB
TypeScript
import { prisma } from "../src/utils/db";
|
|
import { nocExternalClient } from "../src/utils/noc-external-client";
|
|
import logger from "../src/utils/logger";
|
|
|
|
const ID_DESA = "desa1";
|
|
|
|
/**
|
|
* 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@desa1.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 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: { name: name },
|
|
update: {
|
|
externalId: extId,
|
|
color: div.color || "#1E3A5F",
|
|
villageId: ID_DESA,
|
|
externalActivityCount: div.totalKegiatan || 0,
|
|
},
|
|
create: {
|
|
externalId: extId,
|
|
name: name,
|
|
color: div.color || "#1E3A5F",
|
|
villageId: ID_DESA,
|
|
externalActivityCount: div.totalKegiatan || 0,
|
|
},
|
|
});
|
|
}
|
|
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 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) {
|
|
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: divisionName },
|
|
});
|
|
|
|
if (!division) continue;
|
|
|
|
await prisma.activity.upsert({
|
|
where: { externalId: extId },
|
|
update: {
|
|
title: proj.title,
|
|
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: extId,
|
|
title: proj.title,
|
|
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,
|
|
},
|
|
});
|
|
}
|
|
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 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: extId },
|
|
update: {
|
|
title: event.title,
|
|
startDate: new Date(event.startDate || event.date),
|
|
location: event.location || "N/A",
|
|
eventType: (event.eventType || "Meeting") as any,
|
|
villageId: ID_DESA,
|
|
},
|
|
create: {
|
|
externalId: extId,
|
|
title: event.title,
|
|
startDate: new Date(event.startDate || event.date),
|
|
location: event.location || "N/A",
|
|
eventType: (event.eventType || "Meeting") 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 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 || disc.group },
|
|
});
|
|
|
|
await prisma.discussion.upsert({
|
|
where: { externalId: disc.id },
|
|
update: {
|
|
message: disc.message || disc.desc || disc.title,
|
|
divisionId: division?.id,
|
|
villageId: ID_DESA,
|
|
},
|
|
create: {
|
|
externalId: disc.id,
|
|
message: disc.message || disc.desc || disc.title,
|
|
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();
|