feat: county sync on monitor page + in-app notification system

- GET /api/eterra/counties — distinct county list from GisUat
- POST /api/eterra/sync-county — background sync all UATs in a county
  (TERENURI + CLADIRI + INTRAVILAN), magic mode for enriched UATs,
  concurrency guard, creates notification on completion
- In-app notification service (KeyValueStore, CRUD, unread count)
- GET/PATCH /api/notifications/app — list and mark-read endpoints
- NotificationBell component in header with popover + polling
- Monitor page: county select dropdown + SyncTestButton with customBody

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude VM
2026-04-07 22:56:59 +03:00
parent 8222be2f0e
commit f44d57629f
8 changed files with 742 additions and 3 deletions
+56
View File
@@ -0,0 +1,56 @@
/**
* GET /api/notifications/app — list recent + unread count
* PATCH /api/notifications/app — mark read / mark all read
*
* Body for PATCH:
* { action: "mark-read", id: string }
* { action: "mark-all-read" }
*/
import { NextResponse } from "next/server";
import {
getAppNotifications,
getUnreadCount,
markAsRead,
markAllAsRead,
} from "@/core/notifications/app-notifications";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export async function GET(req: Request) {
try {
const url = new URL(req.url);
const limit = Math.min(parseInt(url.searchParams.get("limit") ?? "30", 10), 100);
const [notifications, unreadCount] = await Promise.all([
getAppNotifications(limit),
getUnreadCount(),
]);
return NextResponse.json({ notifications, unreadCount });
} catch (error) {
const msg = error instanceof Error ? error.message : "Eroare notificari";
return NextResponse.json({ error: msg }, { status: 500 });
}
}
export async function PATCH(req: Request) {
try {
const body = (await req.json()) as { action: string; id?: string };
if (body.action === "mark-read" && body.id) {
await markAsRead(body.id);
return NextResponse.json({ ok: true });
}
if (body.action === "mark-all-read") {
await markAllAsRead();
return NextResponse.json({ ok: true });
}
return NextResponse.json({ error: "Actiune necunoscuta" }, { status: 400 });
} catch (error) {
const msg = error instanceof Error ? error.message : "Eroare notificari";
return NextResponse.json({ error: msg }, { status: 500 });
}
}