From 12ff629fbfa29f8c5717e089e28c8e9207f58598 Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Wed, 25 Mar 2026 09:17:29 +0200 Subject: [PATCH] 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) --- src/app/(modules)/rgi-test/page.tsx | 36 +++++++++--------- src/app/(portal)/portal/page.tsx | 59 +++++++++++++++++++---------- 2 files changed, 58 insertions(+), 37 deletions(-) diff --git a/src/app/(modules)/rgi-test/page.tsx b/src/app/(modules)/rgi-test/page.tsx index 967b4ee..9aa5417 100644 --- a/src/app/(modules)/rgi-test/page.tsx +++ b/src/app/(modules)/rgi-test/page.tsx @@ -1,6 +1,7 @@ "use client"; import React, { useState, useCallback, useMemo, useEffect, useRef } from "react"; +import JSZip from "jszip"; import { Button } from "@/shared/components/ui/button"; import { Input } from "@/shared/components/ui/input"; import { Label } from "@/shared/components/ui/label"; @@ -354,10 +355,10 @@ function IssuedDocsPanel({ const handleDownloadAll = useCallback(async () => { if (!docs || docs.length === 0 || downloadingAll) return; setDownloadingAll(true); + const zip = new JSZip(); let downloaded = 0; let blocked = 0; - // Count duplicates by docType for naming (e.g. Receptie_tehnica_66903_2.pdf) const typeCounts: Record = {}; for (const d of docs) typeCounts[d.docType || "Document"] = (typeCounts[d.docType || "Document"] || 0) + 1; const typeIdx: Record = {}; @@ -382,30 +383,31 @@ function IssuedDocsPanel({ try { const res = await fetch(url); const ct = res.headers.get("content-type") || ""; - if (ct.includes("application/json")) { - blocked++; - continue; - } + if (ct.includes("application/json")) { blocked++; continue; } const blob = await res.blob(); - const a = document.createElement("a"); - a.href = URL.createObjectURL(blob); - a.download = filename; - document.body.appendChild(a); - a.click(); - URL.revokeObjectURL(a.href); - document.body.removeChild(a); + zip.file(filename, blob); downloaded++; - // Small delay between downloads so browser doesn't block them - await new Promise((r) => setTimeout(r, 300)); } catch { 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( blocked > 0 - ? `${downloaded} descarcat${downloaded !== 1 ? "e" : ""}, ${blocked} indisponibil${blocked !== 1 ? "e" : ""}` - : `${downloaded} document${downloaded !== 1 ? "e" : ""} descarcat${downloaded !== 1 ? "e" : ""}`, + ? `${downloaded} in ZIP, ${blocked} indisponibil${blocked !== 1 ? "e" : ""}` + : `ZIP descarcat cu ${downloaded} document${downloaded !== 1 ? "e" : ""}`, ); setDownloadingAll(false); setTimeout(() => setDownloadProgress(""), 5000); @@ -982,7 +984,7 @@ export default function RgiTestPage() { -

Nr. {app.appNo} — click descarca toate

+

Descarca arhiva ZIP cu documentele cererii {app.appNo}

{app.applicationObject || "-"}

Status: {app.statusName || app.stateCode}

Rezolutie: {app.resolutionName || "-"}

diff --git a/src/app/(portal)/portal/page.tsx b/src/app/(portal)/portal/page.tsx index 32e793c..60f9452 100644 --- a/src/app/(portal)/portal/page.tsx +++ b/src/app/(portal)/portal/page.tsx @@ -41,6 +41,7 @@ import { Satellite, RefreshCw, } from "lucide-react"; +import JSZip from "jszip"; import { cn } from "@/shared/lib/utils"; import { SelectionToolbar, type SelectionMode } from "@/modules/geoportal/components/selection-toolbar"; // Simple inline feature panel — no enrichment, no CF extract @@ -373,6 +374,7 @@ function IssuedDocsPanel({ let downloaded = 0; let blocked = 0; + const zip = new JSZip(); const typeCounts: Record = {}; for (const d of docs) typeCounts[d.docType || "Document"] = (typeCounts[d.docType || "Document"] || 0) + 1; const typeIdx: Record = {}; @@ -402,20 +404,25 @@ function IssuedDocsPanel({ continue; } const blob = await res.blob(); - const a = document.createElement("a"); - a.href = URL.createObjectURL(blob); - a.download = filename; - document.body.appendChild(a); - a.click(); - URL.revokeObjectURL(a.href); - document.body.removeChild(a); + zip.file(filename, blob); downloaded++; - await new Promise((r) => setTimeout(r, 300)); } catch { blocked++; } } + if (downloaded > 0) { + setDownloadProgress("Se genereaza 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( blocked > 0 ? `${downloaded} descarcat${downloaded !== 1 ? "e" : ""}, ${blocked} indisponibil${blocked !== 1 ? "e" : ""}` @@ -625,6 +632,7 @@ function RgiContent() { return; } + const zip = new JSZip(); const typeCounts: Record = {}; for (const d of docs) typeCounts[d.docType || "Document"] = (typeCounts[d.docType || "Document"] || 0) + 1; const typeIdx: Record = {}; @@ -650,18 +658,22 @@ function RgiContent() { const ct = r.headers.get("content-type") || ""; if (ct.includes("application/json")) continue; const blob = await r.blob(); - const a = document.createElement("a"); - a.href = URL.createObjectURL(blob); - a.download = filename; - document.body.appendChild(a); - a.click(); - URL.revokeObjectURL(a.href); - document.body.removeChild(a); - await new Promise((resolve) => setTimeout(resolve, 300)); + 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"); + a.href = URL.createObjectURL(zipBlob); + a.download = `Documente_eliberate_${app.appNo}.zip`; + document.body.appendChild(a); + a.click(); + URL.revokeObjectURL(a.href); + document.body.removeChild(a); + } } catch { // silent } @@ -920,7 +932,7 @@ function RgiContent() { -

Nr. {app.appNo} — click descarca toate

+

Descarca arhiva ZIP cu toate documentele cererii {app.appNo}

{app.applicationObject || "-"}

Status: {app.statusName || app.stateCode}

Rezolutie: {app.resolutionName || "-"}

@@ -1189,6 +1201,7 @@ type MapLike = { bounds: [number, number, number, number], opts?: Record, ): void; + flyTo(opts: { center: [number, number]; duration?: number; zoom?: number }): void; isStyleLoaded(): boolean; }; @@ -1365,6 +1378,11 @@ function HartaContent() { return; } 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) => { @@ -1539,7 +1557,8 @@ function HartaContent() { {/* Top-right: basemap switcher + simple feature info (offset to avoid zoom controls) */} -
+ {/* On mobile (< sm), drops below the UAT card via top-14 */} +
{clickedFeature && selectionMode === "off" && (
@@ -1598,7 +1617,7 @@ function HartaContent() {
{/* Bottom: selection toolbar — centered, above attribution */} -
+