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:
@@ -16,6 +16,8 @@ export default function MonitorPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [actionLoading, setActionLoading] = useState("");
|
||||
const [logs, setLogs] = useState<{ time: string; type: "info" | "ok" | "error" | "wait"; msg: string }[]>([]);
|
||||
const [counties, setCounties] = useState<string[]>([]);
|
||||
const [selectedCounty, setSelectedCounty] = useState("");
|
||||
const rebuildPrevRef = useRef<string | null>(null);
|
||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
@@ -40,6 +42,14 @@ export default function MonitorPage() {
|
||||
setLogs((prev) => [{ time: new Date().toLocaleTimeString("ro-RO"), type, msg }, ...prev.slice(0, 49)]);
|
||||
}, []);
|
||||
|
||||
// Fetch counties for sync selector
|
||||
useEffect(() => {
|
||||
fetch("/api/eterra/counties")
|
||||
.then((r) => (r.ok ? r.json() : Promise.reject()))
|
||||
.then((d: { counties: string[] }) => setCounties(d.counties ?? []))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Cleanup poll on unmount
|
||||
useEffect(() => {
|
||||
return () => { if (pollRef.current) clearInterval(pollRef.current); };
|
||||
@@ -273,6 +283,38 @@ export default function MonitorPage() {
|
||||
pollRef={pollRef}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* County sync */}
|
||||
<div className="flex items-end gap-3 mt-2 pt-3 border-t border-border/50">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs text-muted-foreground">Sync pe judet</span>
|
||||
<select
|
||||
value={selectedCounty}
|
||||
onChange={(e) => setSelectedCounty(e.target.value)}
|
||||
className="h-9 w-52 rounded-md border border-border bg-background px-3 text-sm"
|
||||
>
|
||||
<option value="">Alege judet...</option>
|
||||
{counties.map((c) => (
|
||||
<option key={c} value={c}>{c}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<SyncTestButton
|
||||
label={selectedCounty ? `Sync ${selectedCounty}` : "Sync Judet"}
|
||||
description="TERENURI + CLADIRI + INTRAVILAN pentru tot judetul"
|
||||
siruta=""
|
||||
mode="base"
|
||||
includeNoGeometry={false}
|
||||
actionKey="sync-county"
|
||||
actionLoading={actionLoading}
|
||||
setActionLoading={setActionLoading}
|
||||
addLog={addLog}
|
||||
pollRef={pollRef}
|
||||
customEndpoint="/api/eterra/sync-county"
|
||||
customBody={{ county: selectedCounty }}
|
||||
disabled={!selectedCounty}
|
||||
/>
|
||||
</div>
|
||||
{logs.length > 0 && (
|
||||
<div className="border border-border rounded-lg overflow-hidden">
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-muted/50 border-b border-border">
|
||||
@@ -366,13 +408,15 @@ function ActionButton({ label, description, loading, onClick }: {
|
||||
);
|
||||
}
|
||||
|
||||
function SyncTestButton({ label, description, siruta, mode, includeNoGeometry, actionKey, actionLoading, setActionLoading, addLog, pollRef, customEndpoint }: {
|
||||
function SyncTestButton({ label, description, siruta, mode, includeNoGeometry, actionKey, actionLoading, setActionLoading, addLog, pollRef, customEndpoint, customBody, disabled }: {
|
||||
label: string; description: string; siruta: string; mode: "base" | "magic";
|
||||
includeNoGeometry: boolean; actionKey: string; actionLoading: string;
|
||||
setActionLoading: (v: string) => void;
|
||||
addLog: (type: "info" | "ok" | "error" | "wait", msg: string) => void;
|
||||
pollRef: React.MutableRefObject<ReturnType<typeof setInterval> | null>;
|
||||
customEndpoint?: string;
|
||||
customBody?: Record<string, unknown>;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const startTimeRef = useRef<number>(0);
|
||||
const formatElapsed = () => {
|
||||
@@ -389,7 +433,7 @@ function SyncTestButton({ label, description, siruta, mode, includeNoGeometry, a
|
||||
addLog("info", `[${label}] Pornire...`);
|
||||
try {
|
||||
const endpoint = customEndpoint ?? "/api/eterra/sync-background";
|
||||
const body = customEndpoint ? {} : { siruta, mode, includeNoGeometry };
|
||||
const body = customEndpoint ? (customBody ?? {}) : { siruta, mode, includeNoGeometry };
|
||||
const res = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -439,7 +483,7 @@ function SyncTestButton({ label, description, siruta, mode, includeNoGeometry, a
|
||||
}, 3 * 60 * 60_000);
|
||||
} catch { addLog("error", `[${label}] Eroare retea`); setActionLoading(""); }
|
||||
}}
|
||||
disabled={!!actionLoading}
|
||||
disabled={!!actionLoading || !!disabled}
|
||||
className="flex flex-col items-start px-4 py-3 rounded-lg border border-border hover:border-primary/50 hover:bg-primary/5 transition-colors disabled:opacity-50 text-left"
|
||||
>
|
||||
<span className="font-medium text-sm">{actionLoading === actionKey ? "Se ruleaza..." : label}</span>
|
||||
|
||||
Reference in New Issue
Block a user