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";
|
||||
|
||||
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<string, number> = {};
|
||||
for (const d of docs) typeCounts[d.docType || "Document"] = (typeCounts[d.docType || "Document"] || 0) + 1;
|
||||
const typeIdx: Record<string, number> = {};
|
||||
@@ -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() {
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<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>Status: {app.statusName || app.stateCode}</p>
|
||||
<p>Rezolutie: {app.resolutionName || "-"}</p>
|
||||
|
||||
@@ -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<string, number> = {};
|
||||
for (const d of docs) typeCounts[d.docType || "Document"] = (typeCounts[d.docType || "Document"] || 0) + 1;
|
||||
const typeIdx: Record<string, number> = {};
|
||||
@@ -402,18 +404,23 @@ function IssuedDocsPanel({
|
||||
continue;
|
||||
}
|
||||
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");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = filename;
|
||||
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);
|
||||
downloaded++;
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
} catch {
|
||||
blocked++;
|
||||
}
|
||||
}
|
||||
|
||||
setDownloadProgress(
|
||||
@@ -625,6 +632,7 @@ function RgiContent() {
|
||||
return;
|
||||
}
|
||||
|
||||
const zip = new JSZip();
|
||||
const typeCounts: Record<string, number> = {};
|
||||
for (const d of docs) typeCounts[d.docType || "Document"] = (typeCounts[d.docType || "Document"] || 0) + 1;
|
||||
const typeIdx: Record<string, number> = {};
|
||||
@@ -650,17 +658,21 @@ function RgiContent() {
|
||||
const ct = r.headers.get("content-type") || "";
|
||||
if (ct.includes("application/json")) continue;
|
||||
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");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = filename;
|
||||
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);
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
} catch {
|
||||
// skip
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// silent
|
||||
@@ -920,7 +932,7 @@ function RgiContent() {
|
||||
<button
|
||||
type="button"
|
||||
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); }}
|
||||
disabled={downloadingAppPk === pk}
|
||||
>
|
||||
@@ -1025,7 +1037,7 @@ function RgiContent() {
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<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>Status: {app.statusName || app.stateCode}</p>
|
||||
<p>Rezolutie: {app.resolutionName || "-"}</p>
|
||||
@@ -1189,6 +1201,7 @@ type MapLike = {
|
||||
bounds: [number, number, number, number],
|
||||
opts?: Record<string, unknown>,
|
||||
): 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() {
|
||||
</div>
|
||||
|
||||
{/* 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} />
|
||||
{clickedFeature && selectionMode === "off" && (
|
||||
<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>
|
||||
|
||||
{/* 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
|
||||
selectedFeatures={selectedFeatures}
|
||||
selectionMode={selectionMode}
|
||||
|
||||
Reference in New Issue
Block a user