feat(noc): implement sync management UI and backend integration

This commit is contained in:
2026-03-30 14:48:47 +08:00
parent 3125bc1002
commit 65844bac7e
28 changed files with 2558 additions and 1339 deletions

View File

@@ -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");

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "division" ADD COLUMN "lastSyncedAt" TIMESTAMP(3);

View File

@@ -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

View File

@@ -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);
}

View File

@@ -1,4 +1,4 @@
import { PrismaClient, Gender, Religion } from "../../generated/prisma";
import { Gender, PrismaClient, Religion } from "../../generated/prisma";
const prisma = new PrismaClient();

View File

@@ -1,8 +1,4 @@
import {
ActivityStatus,
Priority,
PrismaClient,
} from "../../generated/prisma";
import { ActivityStatus, Priority, PrismaClient } from "../../generated/prisma";
const prisma = new PrismaClient();

View File

@@ -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) {