feat: ZIP download, mobile fixes, click centering, tooltip
ZIP download:
- Both portal and RGI test page now create a single ZIP archive
(Documente_eliberate_{appNo}.zip) instead of sequential downloads
- Uses JSZip (already in project dependencies)
Portal mobile:
- Basemap switcher drops below UAT card on mobile (top-14 sm:top-2)
- Selection toolbar at bottom-3 with z-30 (always visible)
- Click on feature centers map on that parcel (flyTo)
Tooltips:
- Green download icon: "Descarca arhiva ZIP cu documentele cererii X"
- Updated on both portal and RGI test page
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useCallback, useMemo, useEffect, useRef } from "react";
|
import React, { useState, useCallback, useMemo, useEffect, useRef } from "react";
|
||||||
|
import JSZip from "jszip";
|
||||||
import { Button } from "@/shared/components/ui/button";
|
import { Button } from "@/shared/components/ui/button";
|
||||||
import { Input } from "@/shared/components/ui/input";
|
import { Input } from "@/shared/components/ui/input";
|
||||||
import { Label } from "@/shared/components/ui/label";
|
import { Label } from "@/shared/components/ui/label";
|
||||||
@@ -354,10 +355,10 @@ function IssuedDocsPanel({
|
|||||||
const handleDownloadAll = useCallback(async () => {
|
const handleDownloadAll = useCallback(async () => {
|
||||||
if (!docs || docs.length === 0 || downloadingAll) return;
|
if (!docs || docs.length === 0 || downloadingAll) return;
|
||||||
setDownloadingAll(true);
|
setDownloadingAll(true);
|
||||||
|
const zip = new JSZip();
|
||||||
let downloaded = 0;
|
let downloaded = 0;
|
||||||
let blocked = 0;
|
let blocked = 0;
|
||||||
|
|
||||||
// Count duplicates by docType for naming (e.g. Receptie_tehnica_66903_2.pdf)
|
|
||||||
const typeCounts: Record<string, number> = {};
|
const typeCounts: Record<string, number> = {};
|
||||||
for (const d of docs) typeCounts[d.docType || "Document"] = (typeCounts[d.docType || "Document"] || 0) + 1;
|
for (const d of docs) typeCounts[d.docType || "Document"] = (typeCounts[d.docType || "Document"] || 0) + 1;
|
||||||
const typeIdx: Record<string, number> = {};
|
const typeIdx: Record<string, number> = {};
|
||||||
@@ -382,30 +383,31 @@ function IssuedDocsPanel({
|
|||||||
try {
|
try {
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
const ct = res.headers.get("content-type") || "";
|
const ct = res.headers.get("content-type") || "";
|
||||||
if (ct.includes("application/json")) {
|
if (ct.includes("application/json")) { blocked++; continue; }
|
||||||
blocked++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const blob = await res.blob();
|
const blob = await res.blob();
|
||||||
const a = document.createElement("a");
|
zip.file(filename, blob);
|
||||||
a.href = URL.createObjectURL(blob);
|
|
||||||
a.download = filename;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(a.href);
|
|
||||||
document.body.removeChild(a);
|
|
||||||
downloaded++;
|
downloaded++;
|
||||||
// Small delay between downloads so browser doesn't block them
|
|
||||||
await new Promise((r) => setTimeout(r, 300));
|
|
||||||
} catch {
|
} catch {
|
||||||
blocked++;
|
blocked++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (downloaded > 0) {
|
||||||
|
setDownloadProgress("Se creeaza arhiva ZIP...");
|
||||||
|
const zipBlob = await zip.generateAsync({ type: "blob" });
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = URL.createObjectURL(zipBlob);
|
||||||
|
a.download = `Documente_eliberate_${appNo}.zip`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(a.href);
|
||||||
|
document.body.removeChild(a);
|
||||||
|
}
|
||||||
|
|
||||||
setDownloadProgress(
|
setDownloadProgress(
|
||||||
blocked > 0
|
blocked > 0
|
||||||
? `${downloaded} descarcat${downloaded !== 1 ? "e" : ""}, ${blocked} indisponibil${blocked !== 1 ? "e" : ""}`
|
? `${downloaded} in ZIP, ${blocked} indisponibil${blocked !== 1 ? "e" : ""}`
|
||||||
: `${downloaded} document${downloaded !== 1 ? "e" : ""} descarcat${downloaded !== 1 ? "e" : ""}`,
|
: `ZIP descarcat cu ${downloaded} document${downloaded !== 1 ? "e" : ""}`,
|
||||||
);
|
);
|
||||||
setDownloadingAll(false);
|
setDownloadingAll(false);
|
||||||
setTimeout(() => setDownloadProgress(""), 5000);
|
setTimeout(() => setDownloadProgress(""), 5000);
|
||||||
@@ -982,7 +984,7 @@ export default function RgiTestPage() {
|
|||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right" className="text-xs max-w-xs">
|
<TooltipContent side="right" className="text-xs max-w-xs">
|
||||||
<p className="font-semibold">Nr. {app.appNo} — click descarca toate</p>
|
<p className="font-semibold">Descarca arhiva ZIP cu documentele cererii {app.appNo}</p>
|
||||||
<p>{app.applicationObject || "-"}</p>
|
<p>{app.applicationObject || "-"}</p>
|
||||||
<p>Status: {app.statusName || app.stateCode}</p>
|
<p>Status: {app.statusName || app.stateCode}</p>
|
||||||
<p>Rezolutie: {app.resolutionName || "-"}</p>
|
<p>Rezolutie: {app.resolutionName || "-"}</p>
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
Satellite,
|
Satellite,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import JSZip from "jszip";
|
||||||
import { cn } from "@/shared/lib/utils";
|
import { cn } from "@/shared/lib/utils";
|
||||||
import { SelectionToolbar, type SelectionMode } from "@/modules/geoportal/components/selection-toolbar";
|
import { SelectionToolbar, type SelectionMode } from "@/modules/geoportal/components/selection-toolbar";
|
||||||
// Simple inline feature panel — no enrichment, no CF extract
|
// Simple inline feature panel — no enrichment, no CF extract
|
||||||
@@ -373,6 +374,7 @@ function IssuedDocsPanel({
|
|||||||
let downloaded = 0;
|
let downloaded = 0;
|
||||||
let blocked = 0;
|
let blocked = 0;
|
||||||
|
|
||||||
|
const zip = new JSZip();
|
||||||
const typeCounts: Record<string, number> = {};
|
const typeCounts: Record<string, number> = {};
|
||||||
for (const d of docs) typeCounts[d.docType || "Document"] = (typeCounts[d.docType || "Document"] || 0) + 1;
|
for (const d of docs) typeCounts[d.docType || "Document"] = (typeCounts[d.docType || "Document"] || 0) + 1;
|
||||||
const typeIdx: Record<string, number> = {};
|
const typeIdx: Record<string, number> = {};
|
||||||
@@ -402,18 +404,23 @@ function IssuedDocsPanel({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const blob = await res.blob();
|
const blob = await res.blob();
|
||||||
|
zip.file(filename, blob);
|
||||||
|
downloaded++;
|
||||||
|
} catch {
|
||||||
|
blocked++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloaded > 0) {
|
||||||
|
setDownloadProgress("Se genereaza arhiva ZIP...");
|
||||||
|
const zipBlob = await zip.generateAsync({ type: "blob" });
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = URL.createObjectURL(blob);
|
a.href = URL.createObjectURL(zipBlob);
|
||||||
a.download = filename;
|
a.download = `Documente_eliberate_${appNo}.zip`;
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(a.href);
|
URL.revokeObjectURL(a.href);
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
downloaded++;
|
|
||||||
await new Promise((r) => setTimeout(r, 300));
|
|
||||||
} catch {
|
|
||||||
blocked++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setDownloadProgress(
|
setDownloadProgress(
|
||||||
@@ -625,6 +632,7 @@ function RgiContent() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const zip = new JSZip();
|
||||||
const typeCounts: Record<string, number> = {};
|
const typeCounts: Record<string, number> = {};
|
||||||
for (const d of docs) typeCounts[d.docType || "Document"] = (typeCounts[d.docType || "Document"] || 0) + 1;
|
for (const d of docs) typeCounts[d.docType || "Document"] = (typeCounts[d.docType || "Document"] || 0) + 1;
|
||||||
const typeIdx: Record<string, number> = {};
|
const typeIdx: Record<string, number> = {};
|
||||||
@@ -650,17 +658,21 @@ function RgiContent() {
|
|||||||
const ct = r.headers.get("content-type") || "";
|
const ct = r.headers.get("content-type") || "";
|
||||||
if (ct.includes("application/json")) continue;
|
if (ct.includes("application/json")) continue;
|
||||||
const blob = await r.blob();
|
const blob = await r.blob();
|
||||||
|
zip.file(filename, blob);
|
||||||
|
} catch {
|
||||||
|
// skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(zip.files).length > 0) {
|
||||||
|
const zipBlob = await zip.generateAsync({ type: "blob" });
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = URL.createObjectURL(blob);
|
a.href = URL.createObjectURL(zipBlob);
|
||||||
a.download = filename;
|
a.download = `Documente_eliberate_${app.appNo}.zip`;
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(a.href);
|
URL.revokeObjectURL(a.href);
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
||||||
} catch {
|
|
||||||
// skip
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// silent
|
// silent
|
||||||
@@ -920,7 +932,7 @@ function RgiContent() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="mt-0.5 shrink-0"
|
className="mt-0.5 shrink-0"
|
||||||
title={`Descarca toate documentele cererii ${app.appNo}`}
|
title={`Descarca arhiva ZIP cu toate documentele cererii ${app.appNo}`}
|
||||||
onClick={(e) => { e.stopPropagation(); void downloadAllForApp(app); }}
|
onClick={(e) => { e.stopPropagation(); void downloadAllForApp(app); }}
|
||||||
disabled={downloadingAppPk === pk}
|
disabled={downloadingAppPk === pk}
|
||||||
>
|
>
|
||||||
@@ -1025,7 +1037,7 @@ function RgiContent() {
|
|||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right" className="text-xs max-w-xs">
|
<TooltipContent side="right" className="text-xs max-w-xs">
|
||||||
<p className="font-semibold">Nr. {app.appNo} — click descarca toate</p>
|
<p className="font-semibold">Descarca arhiva ZIP cu toate documentele cererii {app.appNo}</p>
|
||||||
<p>{app.applicationObject || "-"}</p>
|
<p>{app.applicationObject || "-"}</p>
|
||||||
<p>Status: {app.statusName || app.stateCode}</p>
|
<p>Status: {app.statusName || app.stateCode}</p>
|
||||||
<p>Rezolutie: {app.resolutionName || "-"}</p>
|
<p>Rezolutie: {app.resolutionName || "-"}</p>
|
||||||
@@ -1189,6 +1201,7 @@ type MapLike = {
|
|||||||
bounds: [number, number, number, number],
|
bounds: [number, number, number, number],
|
||||||
opts?: Record<string, unknown>,
|
opts?: Record<string, unknown>,
|
||||||
): void;
|
): void;
|
||||||
|
flyTo(opts: { center: [number, number]; duration?: number; zoom?: number }): void;
|
||||||
isStyleLoaded(): boolean;
|
isStyleLoaded(): boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1365,6 +1378,11 @@ function HartaContent() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setClickedFeature(feature);
|
setClickedFeature(feature);
|
||||||
|
// Center map on clicked feature
|
||||||
|
const map = asMap(mapHandleRef.current);
|
||||||
|
if (map && feature.coordinates) {
|
||||||
|
map.flyTo({ center: feature.coordinates, duration: 500 });
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSelectionModeChange = useCallback((mode: SelectionMode) => {
|
const handleSelectionModeChange = useCallback((mode: SelectionMode) => {
|
||||||
@@ -1539,7 +1557,8 @@ function HartaContent() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Top-right: basemap switcher + simple feature info (offset to avoid zoom controls) */}
|
{/* Top-right: basemap switcher + simple feature info (offset to avoid zoom controls) */}
|
||||||
<div className="absolute top-2 right-12 z-10 flex flex-col items-end gap-2">
|
{/* On mobile (< sm), drops below the UAT card via top-14 */}
|
||||||
|
<div className="absolute top-14 sm:top-2 right-2 sm:right-12 z-10 flex flex-col items-end gap-2">
|
||||||
<PortalBasemapSwitcher value={basemap} onChange={setBasemap} />
|
<PortalBasemapSwitcher value={basemap} onChange={setBasemap} />
|
||||||
{clickedFeature && selectionMode === "off" && (
|
{clickedFeature && selectionMode === "off" && (
|
||||||
<div className="bg-background/95 backdrop-blur-sm border rounded-lg shadow-lg w-56 sm:w-64 overflow-hidden">
|
<div className="bg-background/95 backdrop-blur-sm border rounded-lg shadow-lg w-56 sm:w-64 overflow-hidden">
|
||||||
@@ -1598,7 +1617,7 @@ function HartaContent() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom: selection toolbar — centered, above attribution */}
|
{/* Bottom: selection toolbar — centered, above attribution */}
|
||||||
<div className="absolute bottom-12 sm:bottom-8 left-1/2 -translate-x-1/2 sm:left-3 sm:translate-x-0 z-20">
|
<div className="absolute bottom-3 sm:bottom-8 left-1/2 -translate-x-1/2 sm:left-3 sm:translate-x-0 z-30">
|
||||||
<SelectionToolbar
|
<SelectionToolbar
|
||||||
selectedFeatures={selectedFeatures}
|
selectedFeatures={selectedFeatures}
|
||||||
selectionMode={selectionMode}
|
selectionMode={selectionMode}
|
||||||
|
|||||||
Reference in New Issue
Block a user