fix: Bug fixed für Rollen
Backend Image / build-and-push (push) Failing after 1m12s
Frontend Image / build-and-push (push) Successful in 1m14s

This commit is contained in:
Nick Szaunig committed 2026-09-01 16:54:42 +02:00
1 parent 8432313fa9
commit 5f4e1a7ba9
9 files changed
+311 -341

No files matched your search

@@ -1,130 +0,0 @@
"use client";
import { FormEvent, useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { createLabelTemplate } from "@/lib/label-templates/api";
import { LABEL_PLACEHOLDERS } from "@/lib/label-templates/types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
export function CreateLabelTemplateDialog() {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const [content, setContent] = useState("");
const [widthMm, setWidthMm] = useState("90");
const [heightMm, setHeightMm] = useState("35");
const [error, setError] = useState<string | null>(null);
function reset() {
setName("");
setContent("");
setWidthMm("90");
setHeightMm("35");
setError(null);
}
const mutation = useMutation({
mutationFn: createLabelTemplate,
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["label-templates"] });
setOpen(false);
reset();
},
onError: () => setError("Vorlage konnte nicht angelegt werden."),
});
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setError(null);
mutation.mutate({
name,
content,
widthMm: widthMm ? Number(widthMm) : undefined,
heightMm: heightMm ? Number(heightMm) : undefined,
});
}
return (
<Dialog
open={open}
onOpenChange={(next) => {
setOpen(next);
if (!next) reset();
}}
>
<Button onClick={() => setOpen(true)}>+ Neue Etikettenvorlage</Button>
<DialogContent>
<DialogHeader>
<DialogTitle>Neue Etikettenvorlage</DialogTitle>
</DialogHeader>
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
<div className="flex flex-col gap-2">
<Label htmlFor="label-template-name">Name</Label>
<Input
id="label-template-name"
required
value={name}
onChange={(event) => setName(event.target.value)}
/>
</div>
<div className="flex gap-3">
<div className="flex flex-1 flex-col gap-2">
<Label htmlFor="label-template-width">Breite (mm)</Label>
<Input
id="label-template-width"
type="number"
inputMode="decimal"
value={widthMm}
onChange={(event) => setWidthMm(event.target.value)}
/>
</div>
<div className="flex flex-1 flex-col gap-2">
<Label htmlFor="label-template-height">Höhe (mm)</Label>
<Input
id="label-template-height"
type="number"
inputMode="decimal"
value={heightMm}
onChange={(event) => setHeightMm(event.target.value)}
/>
</div>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="label-template-content">Etikettentext</Label>
<Textarea
id="label-template-content"
required
rows={6}
value={content}
onChange={(event) => setContent(event.target.value)}
/>
<p className="text-xs text-muted-foreground">
Platzhalter:{" "}
{LABEL_PLACEHOLDERS.map((placeholder) => `{{${placeholder.key}}}`).join(", ")}
</p>
</div>
{error ? (
<p role="alert" className="text-sm text-destructive">
{error}
</p>
) : null}
<DialogFooter>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? "Wird angelegt…" : "Anlegen"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
@@ -1,128 +0,0 @@
"use client";
import { FormEvent, useState } from "react";
import { Pencil } from "lucide-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { updateLabelTemplate } from "@/lib/label-templates/api";
import { LABEL_PLACEHOLDERS, LabelTemplate } from "@/lib/label-templates/types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
export function EditLabelTemplateDialog({ template }: { template: LabelTemplate }) {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const [name, setName] = useState(template.name);
const [content, setContent] = useState(template.content);
const [widthMm, setWidthMm] = useState(template.widthMm.toString());
const [heightMm, setHeightMm] = useState(template.heightMm.toString());
const [isDefault, setIsDefault] = useState(template.isDefault);
const mutation = useMutation({
mutationFn: () =>
updateLabelTemplate(template.id, {
name,
content,
widthMm: widthMm ? Number(widthMm) : undefined,
heightMm: heightMm ? Number(heightMm) : undefined,
isDefault,
}),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["label-templates"] });
setOpen(false);
},
});
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
mutation.mutate();
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger
render={
<Button variant="ghost" size="sm">
<Pencil className="size-3.5" />
Bearbeiten
</Button>
}
/>
<DialogContent>
<DialogHeader>
<DialogTitle>Etikettenvorlage bearbeiten</DialogTitle>
</DialogHeader>
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
<div className="flex flex-col gap-2">
<Label htmlFor="label-template-edit-name">Name</Label>
<Input
id="label-template-edit-name"
required
value={name}
onChange={(event) => setName(event.target.value)}
/>
</div>
<div className="flex gap-3">
<div className="flex flex-1 flex-col gap-2">
<Label htmlFor="label-template-edit-width">Breite (mm)</Label>
<Input
id="label-template-edit-width"
type="number"
inputMode="decimal"
value={widthMm}
onChange={(event) => setWidthMm(event.target.value)}
/>
</div>
<div className="flex flex-1 flex-col gap-2">
<Label htmlFor="label-template-edit-height">Höhe (mm)</Label>
<Input
id="label-template-edit-height"
type="number"
inputMode="decimal"
value={heightMm}
onChange={(event) => setHeightMm(event.target.value)}
/>
</div>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="label-template-edit-content">Etikettentext</Label>
<Textarea
id="label-template-edit-content"
required
rows={6}
value={content}
onChange={(event) => setContent(event.target.value)}
/>
<p className="text-xs text-muted-foreground">
Platzhalter:{" "}
{LABEL_PLACEHOLDERS.map((placeholder) => `{{${placeholder.key}}}`).join(", ")}
</p>
</div>
<label className="flex items-center gap-2 text-sm" htmlFor="label-template-edit-default">
<input
id="label-template-edit-default"
type="checkbox"
checked={isDefault}
onChange={(event) => setIsDefault(event.target.checked)}
/>
Als Standardvorlage verwenden
</label>
<DialogFooter>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? "Speichert…" : "Speichern"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
@@ -1,6 +1,7 @@
"use client";
import { Tag } from "lucide-react";
import Link from "next/link";
import { Pencil, Plus, Tag } from "lucide-react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { deactivateLabelTemplate, fetchLabelTemplates } from "@/lib/label-templates/api";
@@ -9,8 +10,6 @@ import { Button } from "@/components/ui/button";
import { PageHeader } from "@/components/page-header";
import { EmptyState } from "@/components/empty-state";
import { EntityCard } from "@/components/entity-card";
import { CreateLabelTemplateDialog } from "./create-label-template-dialog";
import { EditLabelTemplateDialog } from "./edit-label-template-dialog";
export default function LabelTemplatesPage() {
const queryClient = useQueryClient();
@@ -37,7 +36,12 @@ export default function LabelTemplatesPage() {
title="Etikettenvorlagen"
description="Vorlagen mit Platzhaltern für den Etikettendruck verwalten"
gradient="var(--gradient-sky)"
actions={<CreateLabelTemplateDialog />}
actions={
<Button render={<Link href="/settings/label-templates/new" />}>
<Plus className="size-4" />
Neue Etikettenvorlage
</Button>
}
/>
{isLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
@@ -69,7 +73,14 @@ export default function LabelTemplatesPage() {
]}
actions={
<>
<EditLabelTemplateDialog template={template} />
<Button
variant="ghost"
size="sm"
render={<Link href={`/settings/label-templates/${template.id}`} />}
>
<Pencil className="size-3.5" />
Bearbeiten
</Button>
<Button
type="button"
variant="ghost"
@@ -1,7 +1,78 @@
export type LabelFontFamily = "sans" | "serif" | "mono";
export type LabelTextAlign = "left" | "center" | "right";
export type LabelLineOrientation = "horizontal" | "vertical";
export const PLACEHOLDER_KEYS = [
"batchNumber",
"honeyType",
"lotNumber",
"bottlingDate",
"bestBeforeDate",
"jarSize",
"productName",
] as const;
export type PlaceholderKey = (typeof PLACEHOLDER_KEYS)[number];
export const PLACEHOLDER_LABELS: Record<PlaceholderKey, string> = {
batchNumber: "Chargennummer",
honeyType: "Honigsorte",
lotNumber: "Los-/Chargennummer der Abfüllung",
bottlingDate: "Abfülldatum",
bestBeforeDate: "Mindesthaltbarkeitsdatum (MHD)",
jarSize: "Glasgröße",
productName: "Produktname",
};
interface LabelElementBase {
id: string;
x: number;
y: number;
}
export interface LabelTextElement extends LabelElementBase {
type: "text";
width: number;
height: number;
text: string;
placeholderKey?: PlaceholderKey | null;
fontSize: number;
fontFamily: LabelFontFamily;
bold: boolean;
italic: boolean;
color: string;
align: LabelTextAlign;
}
export interface LabelQrCodeElement extends LabelElementBase {
type: "qrCode";
size: number;
}
export interface LabelImageElement extends LabelElementBase {
type: "image";
width: number;
height: number;
src: string;
}
export interface LabelLineElement extends LabelElementBase {
type: "line";
length: number;
thickness: number;
color: string;
orientation: LabelLineOrientation;
}
export type LabelElement =
| LabelTextElement
| LabelQrCodeElement
| LabelImageElement
| LabelLineElement;
export interface LabelTemplate {
id: string;
name: string;
content: string;
elements: LabelElement[];
widthMm: number;
heightMm: number;
isDefault: boolean;
@@ -12,7 +83,7 @@ export interface LabelTemplate {
export interface CreateLabelTemplateInput {
name: string;
content: string;
elements: LabelElement[];
widthMm?: number;
heightMm?: number;
isDefault?: boolean;
@@ -20,22 +91,8 @@ export interface CreateLabelTemplateInput {
export interface UpdateLabelTemplateInput {
name?: string;
content?: string;
elements?: LabelElement[];
widthMm?: number;
heightMm?: number;
isDefault?: boolean;
}
export const LABEL_PLACEHOLDERS: { key: string; description: string }[] = [
{ key: "batchNumber", description: "Chargennummer der Honigcharge" },
{ key: "honeyType", description: "Honigsorte" },
{ key: "lotNumber", description: "Los-/Chargennummer der Abfüllung" },
{ key: "bottlingDate", description: "Abfülldatum" },
{ key: "bestBeforeDate", description: "Mindesthaltbarkeitsdatum (MHD)" },
{ key: "jarSize", description: "Glasgröße" },
{ key: "productName", description: "Produktname" },
{
key: "qrCode",
description: "QR-Code zur öffentlichen Herkunftsseite der Charge (Honigsorte, Daten, Rückverfolgbarkeit)",
},
];