feat(frontend): QR-Code-Dialog auf Objekt-Detailseiten (Modul 22)
Wiederverwendbarer QrCodeDialog (authentifizierter Blob-Fetch) auf Volk-, Bienenstand-, Koenigin-, Honigcharge-Detailseiten und je Lagerbehaelter-Zeile eingebunden.
This commit is contained in:
1 parent
dd88b6a92c
commit
8b7fd61c1c
8 files changed
+101
-2
No files matched your search
@@ -4,6 +4,7 @@ import Link from "next/link";
|
|||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { fetchApiary } from "@/lib/apiaries/api";
|
import { fetchApiary } from "@/lib/apiaries/api";
|
||||||
|
import { QrCodeDialog } from "@/components/qr-code-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ApiaryEditCard } from "./apiary-edit-card";
|
import { ApiaryEditCard } from "./apiary-edit-card";
|
||||||
|
|
||||||
@@ -27,6 +28,9 @@ export default function ApiaryDetailPage() {
|
|||||||
|
|
||||||
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 justify-end">
|
||||||
|
<QrCodeDialog entityType="apiaries" id={apiary.id} />
|
||||||
|
</div>
|
||||||
<ApiaryEditCard key={apiary.id} apiary={apiary} />
|
<ApiaryEditCard key={apiary.id} apiary={apiary} />
|
||||||
<Button
|
<Button
|
||||||
className="w-fit"
|
className="w-fit"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { fetchColony } from "@/lib/colonies/api";
|
import { fetchColony } from "@/lib/colonies/api";
|
||||||
|
import { QrCodeDialog } from "@/components/qr-code-dialog";
|
||||||
import { ColonyEditCard } from "./colony-edit-card";
|
import { ColonyEditCard } from "./colony-edit-card";
|
||||||
import { ColonyActions } from "./colony-actions";
|
import { ColonyActions } from "./colony-actions";
|
||||||
import { ColonyHistoryCard } from "./colony-history";
|
import { ColonyHistoryCard } from "./colony-history";
|
||||||
@@ -31,6 +32,9 @@ export default function ColonyDetailPage() {
|
|||||||
|
|
||||||
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 justify-end">
|
||||||
|
<QrCodeDialog entityType="colonies" id={colony.id} label="QR-Code für Durchsicht" />
|
||||||
|
</div>
|
||||||
<ColonyEditCard key={colony.id} colony={colony} />
|
<ColonyEditCard key={colony.id} colony={colony} />
|
||||||
<ColonyActions colony={colony} />
|
<ColonyActions colony={colony} />
|
||||||
<ColonyInspectionsCard colonyId={colony.id} />
|
<ColonyInspectionsCard colonyId={colony.id} />
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useParams } from "next/navigation";
|
|||||||
import { useMutation, 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 { exportHoneyBatchTraceabilityPdf } from "@/lib/export/api";
|
||||||
|
import { QrCodeDialog } from "@/components/qr-code-dialog";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
@@ -40,7 +41,10 @@ export default function HoneyBatchDetailPage() {
|
|||||||
<Card className="max-w-lg">
|
<Card className="max-w-lg">
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
<CardTitle>{honeyBatch.batchNumber}</CardTitle>
|
<CardTitle>{honeyBatch.batchNumber}</CardTitle>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<Badge>{honeyBatch.status}</Badge>
|
<Badge>{honeyBatch.status}</Badge>
|
||||||
|
<QrCodeDialog entityType="honey-batches" id={honeyBatch.id} />
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-3 text-sm">
|
<CardContent className="flex flex-col gap-3 text-sm">
|
||||||
{honeyBatch.honeyType ? <p>Sorte: {honeyBatch.honeyType}</p> : null}
|
{honeyBatch.honeyType ? <p>Sorte: {honeyBatch.honeyType}</p> : null}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { fetchQueen } from "@/lib/queens/api";
|
import { fetchQueen } from "@/lib/queens/api";
|
||||||
|
import { QrCodeDialog } from "@/components/qr-code-dialog";
|
||||||
import { QueenEditCard } from "./queen-edit-card";
|
import { QueenEditCard } from "./queen-edit-card";
|
||||||
import { AssignColonyDialog } from "./assign-colony-dialog";
|
import { AssignColonyDialog } from "./assign-colony-dialog";
|
||||||
import { QueenHistoryCard } from "./queen-history";
|
import { QueenHistoryCard } from "./queen-history";
|
||||||
@@ -27,6 +28,9 @@ export default function QueenDetailPage() {
|
|||||||
|
|
||||||
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 justify-end">
|
||||||
|
<QrCodeDialog entityType="queens" id={queen.id} />
|
||||||
|
</div>
|
||||||
<QueenEditCard key={queen.id} queen={queen} />
|
<QueenEditCard key={queen.id} queen={queen} />
|
||||||
<AssignColonyDialog queenId={queen.id} />
|
<AssignColonyDialog queenId={queen.id} />
|
||||||
<QueenHistoryCard queenId={queen.id} />
|
<QueenHistoryCard queenId={queen.id} />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { fetchStockItems, fetchStorageContainers } from "@/lib/storage/api";
|
import { fetchStockItems, fetchStorageContainers } from "@/lib/storage/api";
|
||||||
|
import { QrCodeDialog } from "@/components/qr-code-dialog";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@@ -41,12 +42,13 @@ export default function StoragePage() {
|
|||||||
<TableHead>Typ</TableHead>
|
<TableHead>Typ</TableHead>
|
||||||
<TableHead>Standort</TableHead>
|
<TableHead>Standort</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead className="text-right">QR-Code</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{containers.map((container) => (
|
{containers.map((container) => (
|
||||||
<TableRow key={container.id}>
|
<TableRow key={container.id}>
|
||||||
<TableCell>{container.code}</TableCell>
|
<TableCell className="font-mono font-medium">{container.code}</TableCell>
|
||||||
<TableCell>{container.containerType ?? "—"}</TableCell>
|
<TableCell>{container.containerType ?? "—"}</TableCell>
|
||||||
<TableCell>{container.location ?? "—"}</TableCell>
|
<TableCell>{container.location ?? "—"}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -54,6 +56,9 @@ export default function StoragePage() {
|
|||||||
{container.status}
|
{container.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<QrCodeDialog entityType="storage-containers" id={container.id} label="QR" />
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { QrCode } from "lucide-react";
|
||||||
|
import { fetchQrCodeSvg, QrEntityType } from "@/lib/qr/api";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
interface QrCodeDialogProps {
|
||||||
|
entityType: QrEntityType;
|
||||||
|
id: string;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function QrCodeDialog({ entityType, id, label = "QR-Code" }: QrCodeDialogProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const { data: blob, isLoading, isError } = useQuery({
|
||||||
|
queryKey: ["qr", entityType, id],
|
||||||
|
queryFn: () => fetchQrCodeSvg(entityType, id),
|
||||||
|
enabled: open,
|
||||||
|
});
|
||||||
|
|
||||||
|
const objectUrl = useMemo(() => (blob ? URL.createObjectURL(blob) : null), [blob]);
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
||||||
|
};
|
||||||
|
}, [objectUrl]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger
|
||||||
|
render={
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<QrCode className="size-4" />
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DialogContent className="flex flex-col items-center gap-4">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{label}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
{isLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||||
|
{isError ? (
|
||||||
|
<p role="alert" className="text-sm text-destructive">
|
||||||
|
QR-Code konnte nicht geladen werden.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{objectUrl ? (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element -- blob: object URL, next/image cannot handle this
|
||||||
|
<img src={objectUrl} alt="QR-Code" className="size-64 rounded-lg border bg-white p-3" />
|
||||||
|
) : null}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { apiFetchBlob } from "@/lib/auth/api-client";
|
||||||
|
|
||||||
|
export type QrEntityType =
|
||||||
|
| "colonies"
|
||||||
|
| "apiaries"
|
||||||
|
| "queens"
|
||||||
|
| "honey-batches"
|
||||||
|
| "storage-containers";
|
||||||
|
|
||||||
|
export async function fetchQrCodeSvg(entityType: QrEntityType, id: string): Promise<Blob> {
|
||||||
|
const { blob } = await apiFetchBlob(`/qr/${entityType}/${id}.svg`, `${entityType}.svg`);
|
||||||
|
return blob;
|
||||||
|
}
|
||||||
@@ -180,7 +180,7 @@ Reihenfolge = Bau-Reihenfolge. Gleiche Haken-Regel wie MVP: erst setzen, wenn Li
|
|||||||
- Endpunkte: `GET /api/v1/qr/:entityType/:id.svg` für `entityType` ∈ `colonies|apiaries|queens|honey-batches|storage-containers`
|
- Endpunkte: `GET /api/v1/qr/:entityType/:id.svg` für `entityType` ∈ `colonies|apiaries|queens|honey-batches|storage-containers`
|
||||||
- Frontend: "QR-Code"-Button/Dialog auf den jeweiligen Detailseiten, druckfreundliche Ansicht
|
- Frontend: "QR-Code"-Button/Dialog auf den jeweiligen Detailseiten, druckfreundliche Ansicht
|
||||||
- [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
|
||||||
|
|
||||||
### 23. Abfüllungen und Produkte
|
### 23. Abfüllungen und Produkte
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user