feat(parcel-sync): sync button on empty Harta tab + intravilan in base sync

Map tab: when UAT has no local data, shows a "Sincronizează terenuri,
clădiri și intravilan" button that triggers background base sync.

Sync background (base mode): now also syncs LIMITE_INTRAV_DYNAMIC layer
(intravilan boundaries) alongside TERENURI_ACTIVE + CLADIRI_ACTIVE.
Non-critical — if intravilan fails, the rest continues.

Also fixed remaining \u2192 unicode escapes in export/layers/epay tabs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-24 18:04:09 +02:00
parent b1fc7c84a7
commit 3da45a4cab
6 changed files with 101 additions and 11 deletions
@@ -221,6 +221,21 @@ async function runBackground(params: {
throw new Error(r.error ?? "Sync clădiri failed");
}
// Sync intravilan limits (always, lightweight layer)
phase = "Sincronizare limite intravilan";
push({});
try {
await syncLayer(username, password, siruta, "LIMITE_INTRAV_DYNAMIC", {
forceFullSync: forceSync,
jobId,
isSubStep: true,
});
} catch {
// Non-critical — don't fail the whole job
note = "Avertisment: limite intravilan nu s-au sincronizat";
push({});
}
if (!terenuriNeedsSync && !cladiriNeedsSync) {
note = "Date proaspete — sync skip";
}
@@ -60,7 +60,7 @@ type GisUatResult = {
/* ------------------------------------------------------------------ */
function formatDate(iso?: string | null) {
if (!iso) return "\u2014";
if (!iso) return "";
return new Date(iso).toLocaleDateString("ro-RO", {
day: "2-digit",
month: "2-digit",
@@ -71,7 +71,7 @@ function formatDate(iso?: string | null) {
}
function formatShortDate(iso?: string | null) {
if (!iso) return "\u2014";
if (!iso) return "";
return new Date(iso).toLocaleDateString("ro-RO", {
day: "2-digit",
month: "2-digit",
@@ -715,7 +715,7 @@ export function EpayTab() {
{formatShortDate(order.expiresAt)}
</span>
) : (
<span className="text-muted-foreground">{"\u2014"}</span>
<span className="text-muted-foreground">{""}</span>
)}
</td>
@@ -445,7 +445,13 @@ export function ParcelSyncModule() {
</TabsContent>
<TabsContent value="map" className="space-y-4">
<MapTab siruta={siruta} sirutaValid={sirutaValid} />
<MapTab
siruta={siruta}
sirutaValid={sirutaValid}
sessionConnected={session.connected}
syncLocalCount={Object.values(syncLocalCounts).reduce((s, c) => s + c, 0)}
onSyncRefresh={() => void fetchSyncStatus()}
/>
</TabsContent>
</Tabs>
);
@@ -1189,7 +1189,7 @@ export function ExportTab({
Sync fundal Bază
</div>
<div className="text-[10px] opacity-60 font-normal">
Terenuri + clădiri \u2192 salvează în DB
Terenuri + clădiri salvează în DB
</div>
</div>
</Button>
@@ -1215,7 +1215,7 @@ export function ExportTab({
Sync fundal Magic
</div>
<div className="text-[10px] opacity-60 font-normal">
Sync + îmbogățire \u2192 salvează în DB
Sync + îmbogățire salvează în DB
</div>
</div>
</Button>
@@ -1311,7 +1311,7 @@ export function ExportTab({
<div className="flex items-center gap-2 flex-wrap text-xs text-muted-foreground">
{bgPhaseTrail.map((p, i) => (
<span key={i} className="flex items-center gap-1">
{i > 0 && <span className="opacity-40">\u2192</span>}
{i > 0 && <span className="opacity-40"></span>}
<span
className={cn(
i === bgPhaseTrail.length - 1
@@ -1436,7 +1436,7 @@ export function ExportTab({
<div className="flex items-center gap-2 flex-wrap text-xs text-muted-foreground">
{phaseTrail.map((p, i) => (
<span key={i} className="flex items-center gap-1">
{i > 0 && <span className="opacity-40">\u2192</span>}
{i > 0 && <span className="opacity-40"></span>}
<span
className={cn(
i === phaseTrail.length - 1
@@ -880,7 +880,7 @@ export function LayersTab({
</p>
<ol className="list-decimal list-inside space-y-1 text-muted-foreground">
<li>
QGIS \u2192 Layer \u2192 Add Layer \u2192 Add PostGIS Layers
QGIS Layer Add Layer Add PostGIS Layers
</li>
<li>New connection:</li>
</ol>
@@ -2,7 +2,8 @@
import { useState, useRef, useCallback, useEffect } from "react";
import dynamic from "next/dynamic";
import { Map as MapIcon, Loader2, AlertTriangle } from "lucide-react";
import { Map as MapIcon, Loader2, AlertTriangle, RefreshCw } from "lucide-react";
import { Button } from "@/shared/components/ui/button";
import { Card, CardContent } from "@/shared/components/ui/card";
import { Badge } from "@/shared/components/ui/badge";
import { BasemapSwitcher } from "@/modules/geoportal/components/basemap-switcher";
@@ -54,6 +55,9 @@ const BASE_LAYERS = [
type MapTabProps = {
siruta: string;
sirutaValid: boolean;
sessionConnected: boolean;
syncLocalCount: number; // total local features for this UAT
onSyncRefresh: () => void;
};
/* ------------------------------------------------------------------ */
@@ -84,7 +88,7 @@ function asMap(handle: MapViewerHandle | null): MapLike | null {
/* Component */
/* ------------------------------------------------------------------ */
export function MapTab({ siruta, sirutaValid }: MapTabProps) {
export function MapTab({ siruta, sirutaValid, sessionConnected, syncLocalCount, onSyncRefresh }: MapTabProps) {
const mapHandleRef = useRef<MapViewerHandle>(null);
const [basemap, setBasemap] = useState<BasemapId>("liberty");
const [clickedFeature, setClickedFeature] =
@@ -104,6 +108,10 @@ export function MapTab({ siruta, sirutaValid }: MapTabProps) {
edge: number;
} | null>(null);
/* Quick sync state */
const [syncing, setSyncing] = useState(false);
const [syncMsg, setSyncMsg] = useState("");
/* Layer visibility: show terenuri + cladiri, hide admin */
const [layerVisibility] = useState<LayerVisibility>({
terenuri: true,
@@ -419,6 +427,30 @@ export function MapTab({ siruta, sirutaValid }: MapTabProps) {
setSelectionMode(mode);
}, []);
/* ── Quick sync (terenuri + cladiri + intravilan) ──────────── */
const handleQuickSync = useCallback(async () => {
if (!siruta || syncing || !sessionConnected) return;
setSyncing(true);
setSyncMsg("Se sincronizează...");
try {
const res = await fetch("/api/eterra/sync-background", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ siruta, mode: "base" }),
});
const data = (await res.json()) as { jobId?: string; error?: string };
if (data.error) {
setSyncMsg(`Eroare: ${data.error}`);
} else {
setSyncMsg("Sincronizare pornită în fundal. Revino în câteva minute.");
onSyncRefresh();
}
} catch {
setSyncMsg("Eroare rețea");
}
setSyncing(false);
}, [siruta, syncing, sessionConnected, onSyncRefresh]);
/* ── Render ─────────────────────────────────────────────────── */
if (!sirutaValid) {
@@ -432,6 +464,43 @@ export function MapTab({ siruta, sirutaValid }: MapTabProps) {
);
}
/* No data for this UAT — show sync button */
if (syncLocalCount === 0) {
return (
<Card>
<CardContent className="py-12 text-center space-y-4">
<MapIcon className="h-10 w-10 mx-auto mb-1 text-muted-foreground opacity-30" />
<p className="text-muted-foreground">
Nu există date locale pentru acest UAT.
</p>
{sessionConnected ? (
<div className="space-y-2">
<Button
onClick={() => void handleQuickSync()}
disabled={syncing}
className="bg-zinc-900 hover:bg-zinc-800 text-white dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
>
{syncing ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<RefreshCw className="mr-2 h-4 w-4" />
)}
Sincronizează terenuri, clădiri și intravilan
</Button>
{syncMsg && (
<p className="text-xs text-muted-foreground">{syncMsg}</p>
)}
</div>
) : (
<p className="text-xs text-muted-foreground">
Conectează-te la eTerra pentru a sincroniza date.
</p>
)}
</CardContent>
</Card>
);
}
return (
<div className="space-y-2">
{/* Boundary mismatch alert */}