feat(frontend): Export-Buttons fuer Voelker, Honigchargen (Modul 21)
CSV-Export in den Listen-Views (Voelker, Honigchargen) und PDF-Rueckverfolgbarkeitsbericht auf der Honigcharge-Detailseite. apiFetchBlob() im API-Client ergaenzt fuer authentifizierte Blob-Downloads (JWT im Authorization-Header statt Cookie).
This commit is contained in:
1 parent
c281d04b16
commit
ed108045af
6 files changed
+110
-5
No files matched your search
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { fetchColonies } from "@/lib/colonies/api";
|
import { fetchColonies } from "@/lib/colonies/api";
|
||||||
import { ColonyStatus } from "@/lib/colonies/types";
|
import { ColonyStatus } from "@/lib/colonies/types";
|
||||||
|
import { exportColoniesCsv } from "@/lib/export/api";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
@@ -42,14 +43,25 @@ export default function ColoniesPage() {
|
|||||||
queryFn: () => fetchColonies({ status: status || undefined }),
|
queryFn: () => fetchColonies({ status: status || undefined }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const exportMutation = useMutation({ mutationFn: exportColoniesCsv });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col gap-4 p-4 sm:p-6">
|
<main className="flex flex-col gap-4 p-4 sm:p-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-xl font-semibold">Völker</h1>
|
<h1 className="text-xl font-semibold">Völker</h1>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
disabled={exportMutation.isPending}
|
||||||
|
onClick={() => exportMutation.mutate()}
|
||||||
|
>
|
||||||
|
CSV exportieren
|
||||||
|
</Button>
|
||||||
<Button nativeButton={false} render={<Link href="/colonies/new" />}>
|
<Button nativeButton={false} render={<Link href="/colonies/new" />}>
|
||||||
+ Neues Volk
|
+ Neues Volk
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={status || "ALL"}
|
value={status || "ALL"}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { fetchHoneyBatch, fetchHoneyBatchTraceability } from "@/lib/honey-batches/api";
|
import { fetchHoneyBatch, fetchHoneyBatchTraceability } from "@/lib/honey-batches/api";
|
||||||
|
import { exportHoneyBatchTraceabilityPdf } from "@/lib/export/api";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { AddMeasurementDialog } from "./add-measurement-dialog";
|
import { AddMeasurementDialog } from "./add-measurement-dialog";
|
||||||
|
|
||||||
@@ -18,6 +20,10 @@ export default function HoneyBatchDetailPage() {
|
|||||||
queryFn: () => fetchHoneyBatchTraceability(params.id),
|
queryFn: () => fetchHoneyBatchTraceability(params.id),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const exportPdfMutation = useMutation({
|
||||||
|
mutationFn: () => exportHoneyBatchTraceabilityPdf(params.id),
|
||||||
|
});
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <p className="p-4 text-sm text-muted-foreground sm:p-6">Lädt…</p>;
|
return <p className="p-4 text-sm text-muted-foreground sm:p-6">Lädt…</p>;
|
||||||
}
|
}
|
||||||
@@ -64,8 +70,16 @@ export default function HoneyBatchDetailPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="max-w-lg">
|
<Card className="max-w-lg">
|
||||||
<CardHeader>
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
<CardTitle>Rückverfolgbarkeit</CardTitle>
|
<CardTitle>Rückverfolgbarkeit</CardTitle>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={exportPdfMutation.isPending}
|
||||||
|
onClick={() => exportPdfMutation.mutate()}
|
||||||
|
>
|
||||||
|
PDF-Bericht
|
||||||
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-3 text-sm">
|
<CardContent className="flex flex-col gap-3 text-sm">
|
||||||
{!traceability ? <p className="text-muted-foreground">Lädt…</p> : null}
|
{!traceability ? <p className="text-muted-foreground">Lädt…</p> : null}
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { fetchHoneyBatches } from "@/lib/honey-batches/api";
|
import { fetchHoneyBatches } from "@/lib/honey-batches/api";
|
||||||
import { HoneyBatchStatus } from "@/lib/honey-batches/types";
|
import { HoneyBatchStatus } from "@/lib/honey-batches/types";
|
||||||
|
import { exportHoneyBatchesCsv } from "@/lib/export/api";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -42,12 +44,23 @@ export default function HoneyBatchesPage() {
|
|||||||
queryFn: () => fetchHoneyBatches({ status: status || undefined }),
|
queryFn: () => fetchHoneyBatches({ status: status || undefined }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const exportMutation = useMutation({ mutationFn: exportHoneyBatchesCsv });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col gap-4 p-4 sm:p-6">
|
<main className="flex flex-col gap-4 p-4 sm:p-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-xl font-semibold">Honigchargen</h1>
|
<h1 className="text-xl font-semibold">Honigchargen</h1>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
disabled={exportMutation.isPending}
|
||||||
|
onClick={() => exportMutation.mutate()}
|
||||||
|
>
|
||||||
|
CSV exportieren
|
||||||
|
</Button>
|
||||||
<CreateHoneyBatchDialog />
|
<CreateHoneyBatchDialog />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={status || "ALL"}
|
value={status || "ALL"}
|
||||||
|
|||||||
@@ -25,6 +25,39 @@ async function rawFetch(path: string, init: RequestInit): Promise<Response> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BlobDownload {
|
||||||
|
blob: Blob;
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filenameFromContentDisposition(header: string | null, fallback: string): string {
|
||||||
|
const match = header ? /filename="([^"]+)"/.exec(header) : null;
|
||||||
|
return match?.[1] ?? fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function apiFetchBlob(path: string, fallbackFilename: string): Promise<BlobDownload> {
|
||||||
|
let response = await rawFetch(path, {});
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
const refreshed = await refreshRequest();
|
||||||
|
if (refreshed) {
|
||||||
|
setAccessToken(refreshed.accessToken);
|
||||||
|
response = await rawFetch(path, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new ApiError(response.status, 'Export fehlgeschlagen');
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const filename = filenameFromContentDisposition(
|
||||||
|
response.headers.get('Content-Disposition'),
|
||||||
|
fallbackFilename,
|
||||||
|
);
|
||||||
|
return { blob, filename };
|
||||||
|
}
|
||||||
|
|
||||||
export async function apiFetch<T>(path: string, init: RequestInit = {}): Promise<T> {
|
export async function apiFetch<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||||
let response = await rawFetch(path, init);
|
let response = await rawFetch(path, init);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { apiFetchBlob } from "@/lib/auth/api-client";
|
||||||
|
|
||||||
|
export function downloadBlob(blob: Blob, filename: string): void {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportColoniesCsv(): Promise<void> {
|
||||||
|
const { blob, filename } = await apiFetchBlob("/export/colonies.csv", "colonies.csv");
|
||||||
|
downloadBlob(blob, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportHoneyBatchesCsv(): Promise<void> {
|
||||||
|
const { blob, filename } = await apiFetchBlob(
|
||||||
|
"/export/honey-batches.csv",
|
||||||
|
"honey-batches.csv",
|
||||||
|
);
|
||||||
|
downloadBlob(blob, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportHoneyBatchTraceabilityPdf(honeyBatchId: string): Promise<void> {
|
||||||
|
const { blob, filename } = await apiFetchBlob(
|
||||||
|
`/export/honey-batch-traceability.pdf?honeyBatchId=${honeyBatchId}`,
|
||||||
|
"honey-batch-traceability.pdf",
|
||||||
|
);
|
||||||
|
downloadBlob(blob, filename);
|
||||||
|
}
|
||||||
@@ -154,7 +154,7 @@ Abgeleitet aus [`docs/lastenheft.md`](docs/lastenheft.md) Abschnitt 26. Reihenfo
|
|||||||
- Endpunkte: `GET /api/v1/export/colonies.csv`, `GET /api/v1/export/honey-batches.csv`, `GET /api/v1/export/:report.pdf`, `GET /api/v1/export/:entity.json`
|
- Endpunkte: `GET /api/v1/export/colonies.csv`, `GET /api/v1/export/honey-batches.csv`, `GET /api/v1/export/:report.pdf`, `GET /api/v1/export/:entity.json`
|
||||||
- Frontend: Export-Buttons in den jeweiligen Listen-Views
|
- Frontend: Export-Buttons in den jeweiligen Listen-Views
|
||||||
- [x] Backend-Modul fertig, getestet, grün, committet
|
- [x] Backend-Modul fertig, getestet, grün, committet
|
||||||
- [ ] Frontend-Views fertig, getestet, grün, committet
|
- [x] Frontend-Views fertig, getestet, grün, committet
|
||||||
|
|
||||||
### MVP-Abschluss
|
### MVP-Abschluss
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user