import { NextResponse } from "next/server"; import { PrismaClient, Prisma } from "@prisma/client"; import { isWeekendWindow, getWeekendSyncActivity, } from "@/modules/parcel-sync/services/weekend-deep-sync"; const prisma = new PrismaClient(); const g = globalThis as { __parcelSyncRunning?: boolean }; const KV_NAMESPACE = "parcel-sync-weekend"; const KV_KEY = "queue-state"; type StepName = "sync_terenuri" | "sync_cladiri" | "import_nogeom" | "enrich"; type StepStatus = "pending" | "done" | "error"; type CityState = { siruta: string; name: string; county: string; priority: number; steps: Record; lastActivity?: string; errorMessage?: string; }; type WeekendSyncState = { cities: CityState[]; lastSessionDate?: string; totalSessions: number; completedCycles: number; }; const FRESH_STEPS: Record = { sync_terenuri: "pending", sync_cladiri: "pending", import_nogeom: "pending", enrich: "pending", }; const DEFAULT_CITIES: Omit[] = [ { siruta: "54975", name: "Cluj-Napoca", county: "Cluj", priority: 1 }, { siruta: "32394", name: "Bistri\u021Ba", county: "Bistri\u021Ba-N\u0103s\u0103ud", priority: 1 }, { siruta: "114319", name: "T\u00E2rgu Mure\u0219", county: "Mure\u0219", priority: 2 }, { siruta: "139704", name: "Zal\u0103u", county: "S\u0103laj", priority: 2 }, { siruta: "26564", name: "Oradea", county: "Bihor", priority: 2 }, { siruta: "9262", name: "Arad", county: "Arad", priority: 2 }, { siruta: "155243", name: "Timi\u0219oara", county: "Timi\u0219", priority: 2 }, { siruta: "143450", name: "Sibiu", county: "Sibiu", priority: 2 }, { siruta: "40198", name: "Bra\u0219ov", county: "Bra\u0219ov", priority: 2 }, ]; /** Initialize state with default cities if not present in DB */ async function getOrCreateState(): Promise { const row = await prisma.keyValueStore.findUnique({ where: { namespace_key: { namespace: KV_NAMESPACE, key: KV_KEY } }, }); if (row?.value && typeof row.value === "object") { return row.value as unknown as WeekendSyncState; } // First access — initialize with defaults const state: WeekendSyncState = { cities: DEFAULT_CITIES.map((c) => ({ ...c, steps: { ...FRESH_STEPS } })), totalSessions: 0, completedCycles: 0, }; await prisma.keyValueStore.upsert({ where: { namespace_key: { namespace: KV_NAMESPACE, key: KV_KEY } }, update: { value: state as unknown as Prisma.InputJsonValue }, create: { namespace: KV_NAMESPACE, key: KV_KEY, value: state as unknown as Prisma.InputJsonValue, }, }); return state; } /** * GET /api/eterra/weekend-sync * Returns the current queue state. */ export async function GET() { // Auth handled by middleware (route is not excluded) const state = await getOrCreateState(); const sirutas = state.cities.map((c) => c.siruta); const counts = await prisma.gisFeature.groupBy({ by: ["siruta", "layerId"], where: { siruta: { in: sirutas } }, _count: { id: true }, }); const enrichedCounts = await prisma.gisFeature.groupBy({ by: ["siruta"], where: { siruta: { in: sirutas }, enrichedAt: { not: null } }, _count: { id: true }, }); const enrichedMap = new Map(enrichedCounts.map((e) => [e.siruta, e._count.id])); type CityStats = { terenuri: number; cladiri: number; total: number; enriched: number; }; const statsMap = new Map(); for (const c of counts) { const existing = statsMap.get(c.siruta) ?? { terenuri: 0, cladiri: 0, total: 0, enriched: 0 }; existing.total += c._count.id; if (c.layerId === "TERENURI_ACTIVE") existing.terenuri = c._count.id; if (c.layerId === "CLADIRI_ACTIVE") existing.cladiri = c._count.id; existing.enriched = enrichedMap.get(c.siruta) ?? 0; statsMap.set(c.siruta, existing); } const citiesWithStats = state.cities.map((c) => ({ ...c, dbStats: statsMap.get(c.siruta) ?? { terenuri: 0, cladiri: 0, total: 0, enriched: 0 }, })); // Determine live sync status const running = !!g.__parcelSyncRunning; const activity = getWeekendSyncActivity(); const inWindow = isWeekendWindow(); const hasErrors = state.cities.some((c) => (Object.values(c.steps) as StepStatus[]).some((s) => s === "error"), ); type SyncStatus = "running" | "error" | "waiting" | "idle"; let syncStatus: SyncStatus = "idle"; if (running) syncStatus = "running"; else if (hasErrors) syncStatus = "error"; else if (inWindow) syncStatus = "waiting"; return NextResponse.json({ state: { ...state, cities: citiesWithStats }, syncStatus, currentActivity: activity, inWeekendWindow: inWindow, }); } /** * POST /api/eterra/weekend-sync * Modify the queue: add/remove cities, reset steps, change priority. */ export async function POST(request: Request) { // Auth handled by middleware (route is not excluded) const body = (await request.json()) as { action: "add" | "remove" | "reset" | "reset_all" | "set_priority"; siruta?: string; name?: string; county?: string; priority?: number; }; const state = await getOrCreateState(); switch (body.action) { case "add": { if (!body.siruta || !body.name) { return NextResponse.json( { error: "siruta si name sunt obligatorii" }, { status: 400 }, ); } if (state.cities.some((c) => c.siruta === body.siruta)) { return NextResponse.json( { error: `${body.name} (${body.siruta}) e deja in coada` }, { status: 409 }, ); } state.cities.push({ siruta: body.siruta, name: body.name, county: body.county ?? "", priority: body.priority ?? 3, steps: { ...FRESH_STEPS }, }); break; } case "remove": { state.cities = state.cities.filter((c) => c.siruta !== body.siruta); break; } case "reset": { const city = state.cities.find((c) => c.siruta === body.siruta); if (city) { city.steps = { ...FRESH_STEPS }; city.errorMessage = undefined; } break; } case "reset_all": { for (const city of state.cities) { city.steps = { ...FRESH_STEPS }; city.errorMessage = undefined; } state.completedCycles = 0; break; } case "set_priority": { const city = state.cities.find((c) => c.siruta === body.siruta); if (city && body.priority != null) { city.priority = body.priority; } break; } } await prisma.keyValueStore.upsert({ where: { namespace_key: { namespace: KV_NAMESPACE, key: KV_KEY } }, update: { value: state as unknown as Prisma.InputJsonValue }, create: { namespace: KV_NAMESPACE, key: KV_KEY, value: state as unknown as Prisma.InputJsonValue, }, }); return NextResponse.json({ ok: true, cities: state.cities.length }); }