feat(geoportal-v2): export toolbar + Semnez ca picker + CF intern/Extras split
V2 panel toolbar replaces the single "Comandă CF" button with two rows:
[Încadrare] [Pl. situație] [Coord.] [DXF] ← 4 exports
[CF intern] [Extras CF] ← 2 CF flows
Each export button pops an inline modal:
- PIZ / PAD: SignAsPicker (PFA / PJA radio list, manual-add inline,
co-signer slot on PIZ) + basemap toggle (google / orto for PIZ).
- Coord / DXF: no picker — single-click download via JWT proxy.
"CF intern" is the free copycf flow from eTerra (proxied via gis-api);
"Extras CF" keeps the existing CfOrderModal (1 credit ePay). The two
modes are now visually balanced as a 2-button row.
Sign-as picker rows merge user-owned Signatory table entries with the
SIGN_AS_DEFAULT_OPTIONS env-driven fallback (org-wide hardcoded options;
defaults seed two Studii de teren entries — Tiurbe PFA + SRL PJA). New
rows added via the picker's "Adaugă autorizație" inline form write to
the Signatory table; ENV rows are read-only.
Architots side ships fully:
- prisma Signatory model + ALTER TABLE applied (per the schema-drift
feedback memory).
- /api/sign-as-options (GET, POST) + /api/sign-as-options/[id]
(PATCH, DELETE).
- /api/cf-intern/order and /api/gis/parcel/[id]/{piz,pad,coords,dxf}
proxy routes — auth check + JWT forward, stream binary back.
- gis-api thin client extended with the matching exports.* namespace.
Until the gis-api endpoints ship (next session — full spec in
docs/plans/005-gis-api-export-endpoints.md), each export proxy returns
501 "…urmează" with a Romanian message so the modal shows what's
coming instead of a hard error.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,9 +7,11 @@ import {
|
||||
Home, Building, Building2, MapPin, ChevronRight, Users,
|
||||
Sparkles, ShieldCheck, AlertTriangle, HelpCircle,
|
||||
Factory, Warehouse,
|
||||
Map as MapIcon, FileSpreadsheet, FileBox, Receipt, ScrollText,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import { CfOrderModal } from "./cf-order-modal";
|
||||
import { ExportModal, type ExportKind } from "./exports/export-modal";
|
||||
import { useUatName } from "./uat-lookup";
|
||||
|
||||
const AUTH_RETRY_KEY = "gis_panel_auth_retry";
|
||||
@@ -406,14 +408,63 @@ export function FeatureInfoPanel({ feature, onClose, onSelectFeature, basic = fa
|
||||
const [condoOwners, setCondoOwners] = useState<CondoOwner[] | null>(null);
|
||||
const [condoLoading, setCondoLoading] = useState(false);
|
||||
const [cfModalOpen, setCfModalOpen] = useState(false);
|
||||
const [cfInternBusy, setCfInternBusy] = useState(false);
|
||||
const [cfInternError, setCfInternError] = useState<string | null>(null);
|
||||
const [exportKind, setExportKind] = useState<ExportKind | null>(null);
|
||||
|
||||
// Close the CF modal whenever the user switches to a different
|
||||
// parcel — keeps the modal scoped to a single decision instead of
|
||||
// silently re-targeting mid-flight.
|
||||
// Close the modals whenever the user switches to a different parcel —
|
||||
// keeps the modal scoped to a single decision instead of silently
|
||||
// re-targeting mid-flight.
|
||||
useEffect(() => {
|
||||
setCfModalOpen(false);
|
||||
setExportKind(null);
|
||||
setCfInternError(null);
|
||||
}, [feature.cadastralRef, feature.siruta, feature.layerId]);
|
||||
|
||||
const handleCfIntern = useCallback(async () => {
|
||||
if (!feature.cadastralRef || !feature.siruta) return;
|
||||
setCfInternBusy(true);
|
||||
setCfInternError(null);
|
||||
try {
|
||||
const res = await fetch("/api/cf-intern/order", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
nrCadastral: feature.cadastralRef,
|
||||
siruta: feature.siruta,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
let msg = `Eroare HTTP ${res.status}`;
|
||||
const text = await res.text();
|
||||
try {
|
||||
const j = JSON.parse(text);
|
||||
if (typeof j.error === "string") msg = j.error;
|
||||
} catch {
|
||||
if (text) msg += `: ${text.slice(0, 200)}`;
|
||||
}
|
||||
setCfInternError(msg);
|
||||
return;
|
||||
}
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const cd = res.headers.get("content-disposition") ?? "";
|
||||
const m = /filename\*?=(?:UTF-8''|"?)([^";]+)/i.exec(cd);
|
||||
const filename = m?.[1]
|
||||
? decodeURIComponent(m[1])
|
||||
: `cf_intern_${feature.cadastralRef}.pdf`;
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 30_000);
|
||||
} catch (err) {
|
||||
setCfInternError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setCfInternBusy(false);
|
||||
}
|
||||
}, [feature.cadastralRef, feature.siruta]);
|
||||
|
||||
const authRetriedRef = useRef<boolean>(
|
||||
typeof window !== "undefined" &&
|
||||
sessionStorage.getItem(AUTH_RETRY_KEY) === "1",
|
||||
@@ -1297,18 +1348,85 @@ export function FeatureInfoPanel({ feature, onClose, onSelectFeature, basic = fa
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions toolbar */}
|
||||
<div className="flex flex-wrap gap-1 border-t bg-muted/30 p-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCfModalOpen(true)}
|
||||
disabled={!feature.cadastralRef || !feature.siruta}
|
||||
className="inline-flex items-center gap-1 rounded bg-background px-2 py-1 text-[11px] font-medium hover:bg-muted disabled:opacity-50"
|
||||
title="Comandă extras Carte Funciară (1 credit ePay)"
|
||||
>
|
||||
<FileText className="h-3 w-3" />
|
||||
Comandă CF
|
||||
</button>
|
||||
{/* Actions toolbar — exports + CF split */}
|
||||
<div className="space-y-1 border-t bg-muted/30 p-1.5">
|
||||
{/* 4 export buttons row: PIZ / Plan situație / Coord / DXF */}
|
||||
<div className="grid grid-cols-4 gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExportKind("piz")}
|
||||
disabled={!feature.cadastralRef}
|
||||
className="inline-flex flex-col items-center gap-0.5 rounded bg-background px-1 py-1.5 text-[10px] font-medium hover:bg-muted disabled:opacity-50"
|
||||
title="Plan de încadrare în zonă (PDF, 1:5000)"
|
||||
>
|
||||
<MapIcon className="h-3.5 w-3.5" />
|
||||
<span>Încadrare</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExportKind("pad")}
|
||||
disabled={!feature.cadastralRef}
|
||||
className="inline-flex flex-col items-center gap-0.5 rounded bg-background px-1 py-1.5 text-[10px] font-medium hover:bg-muted disabled:opacity-50"
|
||||
title="Plan de situație / amplasament (PDF)"
|
||||
>
|
||||
<ScrollText className="h-3.5 w-3.5" />
|
||||
<span>Pl. situație</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExportKind("coords")}
|
||||
disabled={!feature.cadastralRef}
|
||||
className="inline-flex flex-col items-center gap-0.5 rounded bg-background px-1 py-1.5 text-[10px] font-medium hover:bg-muted disabled:opacity-50"
|
||||
title="Coordonate Stereo70 (XLSX)"
|
||||
>
|
||||
<FileSpreadsheet className="h-3.5 w-3.5" />
|
||||
<span>Coord.</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExportKind("dxf")}
|
||||
disabled={!feature.cadastralRef}
|
||||
className="inline-flex flex-col items-center gap-0.5 rounded bg-background px-1 py-1.5 text-[10px] font-medium hover:bg-muted disabled:opacity-50"
|
||||
title="Export DXF (parcela + vecini)"
|
||||
>
|
||||
<FileBox className="h-3.5 w-3.5" />
|
||||
<span>DXF</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* CF row: intern (free, ~2-3s) vs ANCPI extract (1 credit ePay) */}
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCfIntern}
|
||||
disabled={cfInternBusy || !feature.cadastralRef || !feature.siruta}
|
||||
className="inline-flex items-center justify-center gap-1 rounded bg-background px-2 py-1.5 text-[11px] font-medium hover:bg-muted disabled:opacity-50"
|
||||
title="Descarcă extras CF intern din eTerra (gratuit)"
|
||||
>
|
||||
{cfInternBusy ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<FileText className="h-3 w-3" />
|
||||
)}
|
||||
CF intern
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCfModalOpen(true)}
|
||||
disabled={!feature.cadastralRef || !feature.siruta}
|
||||
className="inline-flex items-center justify-center gap-1 rounded bg-background px-2 py-1.5 text-[11px] font-medium hover:bg-muted disabled:opacity-50"
|
||||
title="Comandă extras de Carte Funciară prin ePay (1 credit)"
|
||||
>
|
||||
<Receipt className="h-3 w-3" />
|
||||
Extras CF
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{cfInternError && (
|
||||
<div className="rounded border border-destructive/40 bg-destructive/10 px-2 py-1 text-[10px] text-destructive">
|
||||
{cfInternError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* CF order modal — confirmation + animated multi-step progress */}
|
||||
@@ -1318,6 +1436,19 @@ export function FeatureInfoPanel({ feature, onClose, onSelectFeature, basic = fa
|
||||
siruta={feature.siruta}
|
||||
onClose={() => setCfModalOpen(false)}
|
||||
/>
|
||||
|
||||
{/* Export modal — used by all 4 of PIZ/PAD/Coord/DXF buttons */}
|
||||
{exportKind && (
|
||||
<ExportModal
|
||||
open
|
||||
kind={exportKind}
|
||||
parcelId={detail?.id ? String(detail.id) : feature.id}
|
||||
cadastralRef={feature.cadastralRef}
|
||||
uatName={uatName ?? null}
|
||||
layerId={feature.layerId}
|
||||
onClose={() => setExportKind(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user