fix: surface specific business-rule error messages to beekeepers
Backend business-rule guards (inactive apiary blocking colony create/move, active colonies blocking apiary deactivation, negative quantity on IN/OUT stock movements, unavailable queen blocking colony assignment, inactive colony blocking split/merge/move) threw English, UUID-laden exception text that the frontend either discarded in favor of a generic message or - in colony-actions.tsx's move/split/merge/ assign-queen dialogs - didn't handle at all, so the guard fired with zero visible feedback. Translated the messages to plain German and wired the affected mutations to display them, so a beekeeper sees the actual reason a save was rejected instead of silence or "failed". Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
de5df72491
commit
997dadfaaa
8 files changed
+37
-11
No files matched your search
@@ -86,7 +86,7 @@ export class ApiariesService {
|
|||||||
});
|
});
|
||||||
if (activeColonyCount > 0) {
|
if (activeColonyCount > 0) {
|
||||||
throw new ConflictException(
|
throw new ConflictException(
|
||||||
`Apiary ${id} still has ${activeColonyCount} active colonies - move or dissolve them first`,
|
`Der Bienenstand hat noch ${activeColonyCount} aktive(s) Volk/Völker - zuerst verlegen oder auflösen`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const apiary = await this.prisma.apiary.update({
|
const apiary = await this.prisma.apiary.update({
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export class ColoniesService {
|
|||||||
private assertActive(colony: Colony): void {
|
private assertActive(colony: Colony): void {
|
||||||
if (colony.status !== 'ACTIVE') {
|
if (colony.status !== 'ACTIVE') {
|
||||||
throw new ConflictException(
|
throw new ConflictException(
|
||||||
`Colony ${colony.id} is not active (status: ${colony.status})`,
|
`Das Volk ist nicht aktiv (Status: ${colony.status})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,11 +61,11 @@ export class ColoniesService {
|
|||||||
where: { id: apiaryId },
|
where: { id: apiaryId },
|
||||||
});
|
});
|
||||||
if (!apiary) {
|
if (!apiary) {
|
||||||
throw new NotFoundException(`Apiary ${apiaryId} not found`);
|
throw new NotFoundException('Bienenstand nicht gefunden');
|
||||||
}
|
}
|
||||||
if (apiary.status !== 'ACTIVE') {
|
if (apiary.status !== 'ACTIVE') {
|
||||||
throw new ConflictException(
|
throw new ConflictException(
|
||||||
`Apiary ${apiaryId} is not active (status: ${apiary.status})`,
|
'Der Bienenstand ist nicht aktiv und kann daher keine Völker aufnehmen',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,7 +190,9 @@ export class ColoniesService {
|
|||||||
where: { id: mergedColonyId },
|
where: { id: mergedColonyId },
|
||||||
});
|
});
|
||||||
if (!merged) {
|
if (!merged) {
|
||||||
throw new NotFoundException(`Colony ${mergedColonyId} not found`);
|
throw new NotFoundException(
|
||||||
|
'Zu vereinigendes Volk wurde nicht gefunden',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.assertActive(merged);
|
this.assertActive(merged);
|
||||||
// currentQueenId is unique across colonies - if it stayed set on
|
// currentQueenId is unique across colonies - if it stayed set on
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ export class QueensService {
|
|||||||
const queen = await this.findById(id);
|
const queen = await this.findById(id);
|
||||||
if (QUEEN_NO_LONGER_AVAILABLE_STATUSES.includes(queen.status)) {
|
if (QUEEN_NO_LONGER_AVAILABLE_STATUSES.includes(queen.status)) {
|
||||||
throw new ConflictException(
|
throw new ConflictException(
|
||||||
`Queen ${id} is not available for assignment (status: ${queen.status}) - correct her status first if this was a mistake`,
|
'Die Königin ist nicht mehr verfügbar für eine Zuweisung - korrigiere zuerst ihren Status, falls dies ein Versehen war',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const colony = await this.prisma.colony.findUnique({
|
const colony = await this.prisma.colony.findUnique({
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export class StorageService {
|
|||||||
// that is the only way to correct a stock count downward.
|
// that is the only way to correct a stock count downward.
|
||||||
if (dto.type !== 'ADJUSTMENT' && dto.quantity <= 0) {
|
if (dto.type !== 'ADJUSTMENT' && dto.quantity <= 0) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
'quantity must be positive for IN/OUT movements - the direction is determined by "type"',
|
'Menge muss bei Eingang/Ausgang positiv sein - die Richtung wird durch den Bewegungstyp bestimmt',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const delta =
|
const delta =
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { MapPin } from "lucide-react";
|
|||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { deactivateApiary, reactivateApiary, updateApiary } from "@/lib/apiaries/api";
|
import { deactivateApiary, reactivateApiary, updateApiary } from "@/lib/apiaries/api";
|
||||||
|
import { ApiError } from "@/lib/auth/api-client";
|
||||||
import { Apiary } from "@/lib/apiaries/types";
|
import { Apiary } from "@/lib/apiaries/types";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -54,7 +55,10 @@ export function ApiaryEditCard({ apiary }: { apiary: Apiary }) {
|
|||||||
description: "Er kann jederzeit über „Reaktivieren“ wiederhergestellt werden.",
|
description: "Er kann jederzeit über „Reaktivieren“ wiederhergestellt werden.",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: () => toast.error("Bienenstand konnte nicht deaktiviert werden"),
|
onError: (error) =>
|
||||||
|
toast.error(
|
||||||
|
error instanceof ApiError ? error.message : "Bienenstand konnte nicht deaktiviert werden",
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const reactivateMutation = useMutation({
|
const reactivateMutation = useMutation({
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { FormEvent, useState } from "react";
|
import { FormEvent, useState } from "react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { ApiError } from "@/lib/auth/api-client";
|
||||||
import {
|
import {
|
||||||
dissolveColony,
|
dissolveColony,
|
||||||
mergeColony,
|
mergeColony,
|
||||||
@@ -35,6 +36,10 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
|
function errorMessage(error: unknown, fallback: string): string {
|
||||||
|
return error instanceof ApiError ? error.message : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
function invalidateColony(
|
function invalidateColony(
|
||||||
queryClient: ReturnType<typeof useQueryClient>,
|
queryClient: ReturnType<typeof useQueryClient>,
|
||||||
colonyId: string,
|
colonyId: string,
|
||||||
@@ -59,6 +64,8 @@ function MoveDialog({ colony }: { colony: Colony }) {
|
|||||||
invalidateColony(queryClient, colony.id);
|
invalidateColony(queryClient, colony.id);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
},
|
},
|
||||||
|
onError: (error) =>
|
||||||
|
toast.error(errorMessage(error, "Volk konnte nicht umgesetzt werden")),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -122,6 +129,8 @@ function SplitDialog({ colony }: { colony: Colony }) {
|
|||||||
void queryClient.invalidateQueries({ queryKey: ["colonies"] });
|
void queryClient.invalidateQueries({ queryKey: ["colonies"] });
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
},
|
},
|
||||||
|
onError: (error) =>
|
||||||
|
toast.error(errorMessage(error, "Ableger konnte nicht angelegt werden")),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -196,6 +205,8 @@ function AssignQueenDialog({ colony }: { colony: Colony }) {
|
|||||||
void queryClient.invalidateQueries({ queryKey: ["queens"] });
|
void queryClient.invalidateQueries({ queryKey: ["queens"] });
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
},
|
},
|
||||||
|
onError: (error) =>
|
||||||
|
toast.error(errorMessage(error, "Königin konnte nicht zugewiesen werden")),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -259,6 +270,8 @@ function MergeDialog({ colony }: { colony: Colony }) {
|
|||||||
void queryClient.invalidateQueries({ queryKey: ["colonies"] });
|
void queryClient.invalidateQueries({ queryKey: ["colonies"] });
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
},
|
},
|
||||||
|
onError: (error) =>
|
||||||
|
toast.error(errorMessage(error, "Völker konnten nicht vereinigt werden")),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { FormEvent, useState } from "react";
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { assignQueenColony } from "@/lib/queens/api";
|
import { assignQueenColony } from "@/lib/queens/api";
|
||||||
|
import { ApiError } from "@/lib/auth/api-client";
|
||||||
import { QUEEN_UNAVAILABLE_STATUSES } from "@/lib/queens/labels";
|
import { QUEEN_UNAVAILABLE_STATUSES } from "@/lib/queens/labels";
|
||||||
import { QueenStatus } from "@/lib/queens/types";
|
import { QueenStatus } from "@/lib/queens/types";
|
||||||
import { fetchColonies } from "@/lib/colonies/api";
|
import { fetchColonies } from "@/lib/colonies/api";
|
||||||
@@ -45,7 +46,10 @@ export function AssignColonyDialog({
|
|||||||
void queryClient.invalidateQueries({ queryKey: ["queens", queenId, "history"] });
|
void queryClient.invalidateQueries({ queryKey: ["queens", queenId, "history"] });
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
},
|
},
|
||||||
onError: () => toast.error("Volk konnte nicht zugewiesen werden"),
|
onError: (error) =>
|
||||||
|
toast.error(
|
||||||
|
error instanceof ApiError ? error.message : "Volk konnte nicht zugewiesen werden",
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { FormEvent, useState } from "react";
|
import { FormEvent, useState } from "react";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { addStockMovement } from "@/lib/storage/api";
|
import { addStockMovement } from "@/lib/storage/api";
|
||||||
|
import { ApiError } from "@/lib/auth/api-client";
|
||||||
import { StockMovementType } from "@/lib/storage/types";
|
import { StockMovementType } from "@/lib/storage/types";
|
||||||
import { STOCK_MOVEMENT_TYPES, STOCK_MOVEMENT_TYPE_LABELS } from "@/lib/storage/labels";
|
import { STOCK_MOVEMENT_TYPES, STOCK_MOVEMENT_TYPE_LABELS } from "@/lib/storage/labels";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -40,8 +41,10 @@ export function RecordMovementDialog({ stockItemId }: { stockItemId: string }) {
|
|||||||
setQuantity("");
|
setQuantity("");
|
||||||
setError(null);
|
setError(null);
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (error) => {
|
||||||
setError("Bewegung konnte nicht gespeichert werden.");
|
setError(
|
||||||
|
error instanceof ApiError ? error.message : "Bewegung konnte nicht gespeichert werden.",
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user