feat: expose the remaining fields the edit forms were withholding
The backend accepted every field of these records already; the forms simply did not offer them, so from the app they were still frozen. - Inventar: only the condition could be changed. A mistyped inventory number, a wrong purchase or maintenance date, a renamed item - all stuck for the life of the record. New edit dialog covering every field; the inline condition select stays as a shortcut. - Produkt: the status was shown as a badge but could not be set anywhere, so a product could never be discontinued or brought back. - Aufgabe: the status could only ever move to DONE through the "Erledigt" button. IN_PROGRESS and CANCELLED were unreachable, and an accidentally completed task could not be reopened. - Aufgabenvorlage: priority, assignee and the active flag were missing. Deactivating was one-way, since only a deactivate endpoint was wired up - the active checkbox now reactivates through PATCH. - Königinnen-Bewertung: notes and the evaluated colony were missing. Verified by extracting the field set each form submits and comparing it with the matching update DTO: every form now covers its DTO. The one gap left is re-linking a document to a different record, which still has no UI. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
5a5bd4a138
commit
786d929df9
6 files changed
+421
-4
No files matched your search
@@ -0,0 +1,211 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FormEvent, useState } from "react";
|
||||||
|
import { Pencil } from "lucide-react";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { updateEquipment } from "@/lib/equipment/api";
|
||||||
|
import { Equipment, EquipmentCondition } from "@/lib/equipment/types";
|
||||||
|
import {
|
||||||
|
EQUIPMENT_CONDITIONS,
|
||||||
|
EQUIPMENT_CONDITION_LABELS,
|
||||||
|
} from "@/lib/equipment/labels";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Corrects every field of an inventory item. Only the condition used to be
|
||||||
|
* changeable, so a mistyped inventory number or a wrong purchase date stayed
|
||||||
|
* wrong for the life of the record.
|
||||||
|
*/
|
||||||
|
function EquipmentEditForm({
|
||||||
|
equipment,
|
||||||
|
onSaved,
|
||||||
|
}: {
|
||||||
|
equipment: Equipment;
|
||||||
|
onSaved: () => void;
|
||||||
|
}) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [inventoryNumber, setInventoryNumber] = useState(
|
||||||
|
equipment.inventoryNumber,
|
||||||
|
);
|
||||||
|
const [name, setName] = useState(equipment.name);
|
||||||
|
const [manufacturer, setManufacturer] = useState(equipment.manufacturer ?? "");
|
||||||
|
const [location, setLocation] = useState(equipment.location ?? "");
|
||||||
|
const [purchaseDate, setPurchaseDate] = useState(
|
||||||
|
equipment.purchaseDate?.slice(0, 10) ?? "",
|
||||||
|
);
|
||||||
|
const [maintenanceDate, setMaintenanceDate] = useState(
|
||||||
|
equipment.maintenanceDate?.slice(0, 10) ?? "",
|
||||||
|
);
|
||||||
|
const [condition, setCondition] = useState<EquipmentCondition>(
|
||||||
|
equipment.condition,
|
||||||
|
);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: () =>
|
||||||
|
updateEquipment(equipment.id, {
|
||||||
|
inventoryNumber,
|
||||||
|
name,
|
||||||
|
manufacturer: manufacturer || undefined,
|
||||||
|
location: location || undefined,
|
||||||
|
purchaseDate: purchaseDate
|
||||||
|
? new Date(purchaseDate).toISOString()
|
||||||
|
: undefined,
|
||||||
|
maintenanceDate: maintenanceDate
|
||||||
|
? new Date(maintenanceDate).toISOString()
|
||||||
|
: undefined,
|
||||||
|
condition,
|
||||||
|
}),
|
||||||
|
onSuccess: () => {
|
||||||
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: ["equipment", equipment.id],
|
||||||
|
});
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ["equipment"] });
|
||||||
|
onSaved();
|
||||||
|
},
|
||||||
|
onError: (mutationError: Error) => {
|
||||||
|
const message =
|
||||||
|
mutationError.message || "Gegenstand konnte nicht gespeichert werden";
|
||||||
|
setError(message);
|
||||||
|
toast.error(message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
setError(null);
|
||||||
|
mutation.mutate();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="equipment-edit-inventory-number">Inventarnummer</Label>
|
||||||
|
<Input
|
||||||
|
id="equipment-edit-inventory-number"
|
||||||
|
required
|
||||||
|
value={inventoryNumber}
|
||||||
|
onChange={(event) => setInventoryNumber(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="equipment-edit-name">Bezeichnung</Label>
|
||||||
|
<Input
|
||||||
|
id="equipment-edit-name"
|
||||||
|
required
|
||||||
|
value={name}
|
||||||
|
onChange={(event) => setName(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="equipment-edit-manufacturer">Hersteller</Label>
|
||||||
|
<Input
|
||||||
|
id="equipment-edit-manufacturer"
|
||||||
|
value={manufacturer}
|
||||||
|
onChange={(event) => setManufacturer(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="equipment-edit-location">Standort</Label>
|
||||||
|
<Input
|
||||||
|
id="equipment-edit-location"
|
||||||
|
value={location}
|
||||||
|
onChange={(event) => setLocation(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<div className="flex flex-1 flex-col gap-2">
|
||||||
|
<Label htmlFor="equipment-edit-purchase-date">Kaufdatum</Label>
|
||||||
|
<Input
|
||||||
|
id="equipment-edit-purchase-date"
|
||||||
|
type="date"
|
||||||
|
value={purchaseDate}
|
||||||
|
onChange={(event) => setPurchaseDate(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 flex-col gap-2">
|
||||||
|
<Label htmlFor="equipment-edit-maintenance-date">Wartungsdatum</Label>
|
||||||
|
<Input
|
||||||
|
id="equipment-edit-maintenance-date"
|
||||||
|
type="date"
|
||||||
|
value={maintenanceDate}
|
||||||
|
onChange={(event) => setMaintenanceDate(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="equipment-edit-condition">Zustand</Label>
|
||||||
|
<Select
|
||||||
|
value={condition}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setCondition((value ?? condition) as EquipmentCondition)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="equipment-edit-condition" className="w-full">
|
||||||
|
<SelectValue>{EQUIPMENT_CONDITION_LABELS[condition]}</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{EQUIPMENT_CONDITIONS.map((option) => (
|
||||||
|
<SelectItem key={option} value={option}>
|
||||||
|
{EQUIPMENT_CONDITION_LABELS[option]}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
{error ? (
|
||||||
|
<p role="alert" className="text-sm text-destructive">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<Button type="submit" disabled={mutation.isPending}>
|
||||||
|
{mutation.isPending ? "Speichert…" : "Speichern"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EquipmentEditDialog({ equipment }: { equipment: Equipment }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger
|
||||||
|
render={
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<Pencil className="size-3.5" />
|
||||||
|
Bearbeiten
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Gegenstand bearbeiten</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
{open ? (
|
||||||
|
<EquipmentEditForm
|
||||||
|
equipment={equipment}
|
||||||
|
onSaved={() => setOpen(false)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { Wrench } from "lucide-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 { fetchEquipment, updateEquipment } from "@/lib/equipment/api";
|
import { fetchEquipment, updateEquipment } from "@/lib/equipment/api";
|
||||||
|
import { EquipmentEditDialog } from "./equipment-edit-dialog";
|
||||||
import { EquipmentCondition } from "@/lib/equipment/types";
|
import { EquipmentCondition } from "@/lib/equipment/types";
|
||||||
import { EQUIPMENT_CONDITIONS, EQUIPMENT_CONDITION_LABELS } from "@/lib/equipment/labels";
|
import { EQUIPMENT_CONDITIONS, EQUIPMENT_CONDITION_LABELS } from "@/lib/equipment/labels";
|
||||||
import { statusTone } from "@/lib/status-tone";
|
import { statusTone } from "@/lib/status-tone";
|
||||||
@@ -60,9 +61,12 @@ export default function EquipmentDetailPage() {
|
|||||||
</span>
|
</span>
|
||||||
<CardTitle>{equipment.name}</CardTitle>
|
<CardTitle>{equipment.name}</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<Badge variant={statusTone(equipment.condition)}>
|
<Badge variant={statusTone(equipment.condition)}>
|
||||||
{EQUIPMENT_CONDITION_LABELS[equipment.condition]}
|
{EQUIPMENT_CONDITION_LABELS[equipment.condition]}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
<EquipmentEditDialog equipment={equipment} />
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-3 text-sm">
|
<CardContent className="flex flex-col gap-3 text-sm">
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -5,11 +5,19 @@ import { Pencil } 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 { updateProduct } from "@/lib/products/api";
|
import { updateProduct } from "@/lib/products/api";
|
||||||
import { Product } from "@/lib/products/types";
|
import { Product, ProductStatus } from "@/lib/products/types";
|
||||||
|
import { PRODUCT_STATUS_LABELS } from "@/lib/products/labels";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -28,6 +36,7 @@ export function EditProductDialog({ product }: { product: Product }) {
|
|||||||
const [packagingSize, setPackagingSize] = useState(product.packagingSize?.toString() ?? "");
|
const [packagingSize, setPackagingSize] = useState(product.packagingSize?.toString() ?? "");
|
||||||
const [weight, setWeight] = useState(product.weight?.toString() ?? "");
|
const [weight, setWeight] = useState(product.weight?.toString() ?? "");
|
||||||
const [salesUnit, setSalesUnit] = useState(product.salesUnit ?? "");
|
const [salesUnit, setSalesUnit] = useState(product.salesUnit ?? "");
|
||||||
|
const [status, setStatus] = useState<ProductStatus>(product.status);
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: () =>
|
mutationFn: () =>
|
||||||
@@ -38,6 +47,7 @@ export function EditProductDialog({ product }: { product: Product }) {
|
|||||||
packagingSize: packagingSize ? Number(packagingSize) : undefined,
|
packagingSize: packagingSize ? Number(packagingSize) : undefined,
|
||||||
weight: weight ? Number(weight) : undefined,
|
weight: weight ? Number(weight) : undefined,
|
||||||
salesUnit: salesUnit || undefined,
|
salesUnit: salesUnit || undefined,
|
||||||
|
status,
|
||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
void queryClient.invalidateQueries({ queryKey: ["products"] });
|
void queryClient.invalidateQueries({ queryKey: ["products"] });
|
||||||
@@ -111,6 +121,28 @@ export function EditProductDialog({ product }: { product: Product }) {
|
|||||||
onChange={(event) => setWeight(event.target.value)}
|
onChange={(event) => setWeight(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="product-edit-status">Status</Label>
|
||||||
|
<Select
|
||||||
|
value={status}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setStatus((value ?? status) as ProductStatus)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="product-edit-status" className="w-full">
|
||||||
|
<SelectValue>{PRODUCT_STATUS_LABELS[status]}</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{(Object.keys(PRODUCT_STATUS_LABELS) as ProductStatus[]).map(
|
||||||
|
(option) => (
|
||||||
|
<SelectItem key={option} value={option}>
|
||||||
|
{PRODUCT_STATUS_LABELS[option]}
|
||||||
|
</SelectItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="product-edit-sales-unit">Verkaufseinheit</Label>
|
<Label htmlFor="product-edit-sales-unit">Verkaufseinheit</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ import { CreateQueenEvaluationInput, QueenEvaluation } from "@/lib/queens/types"
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { fetchColonies } from "@/lib/colonies/api";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
@@ -32,6 +41,10 @@ const CRITERIA: { key: keyof Omit<CreateQueenEvaluationInput, "date" | "colonyId
|
|||||||
{ key: "honeyYield", label: "Honigertrag" },
|
{ key: "honeyYield", label: "Honigertrag" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** Sentinel for "no colony": the rating may stand on its own, and an empty
|
||||||
|
* select value would render the placeholder rather than a choosable entry. */
|
||||||
|
const NO_COLONY = "__none__";
|
||||||
|
|
||||||
function todayIsoDate(): string {
|
function todayIsoDate(): string {
|
||||||
return new Date().toISOString().slice(0, 10);
|
return new Date().toISOString().slice(0, 10);
|
||||||
}
|
}
|
||||||
@@ -58,8 +71,15 @@ function EditEvaluationDialog({
|
|||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
const [colonyId, setColonyId] = useState(evaluation.colonyId ?? NO_COLONY);
|
||||||
|
const [notes, setNotes] = useState(evaluation.notes ?? "");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { data: colonies } = useQuery({
|
||||||
|
queryKey: ["colonies"],
|
||||||
|
queryFn: () => fetchColonies(),
|
||||||
|
});
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: () =>
|
mutationFn: () =>
|
||||||
updateQueenEvaluation(evaluation.id, {
|
updateQueenEvaluation(evaluation.id, {
|
||||||
@@ -70,6 +90,8 @@ function EditEvaluationDialog({
|
|||||||
swarmingTendency: Number(scores.swarmingTendency),
|
swarmingTendency: Number(scores.swarmingTendency),
|
||||||
broodPattern: Number(scores.broodPattern),
|
broodPattern: Number(scores.broodPattern),
|
||||||
honeyYield: Number(scores.honeyYield),
|
honeyYield: Number(scores.honeyYield),
|
||||||
|
colonyId: colonyId === NO_COLONY ? undefined : colonyId,
|
||||||
|
notes: notes || undefined,
|
||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
void queryClient.invalidateQueries({
|
void queryClient.invalidateQueries({
|
||||||
@@ -136,6 +158,38 @@ function EditEvaluationDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor={`evaluation-edit-colony-${evaluation.id}`}>Volk</Label>
|
||||||
|
<Select
|
||||||
|
value={colonyId}
|
||||||
|
onValueChange={(value) => setColonyId(value ?? NO_COLONY)}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id={`evaluation-edit-colony-${evaluation.id}`}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Kein Volk" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={NO_COLONY}>Kein Volk</SelectItem>
|
||||||
|
{(colonies ?? []).map((colony) => (
|
||||||
|
<SelectItem key={colony.id} value={colony.id}>
|
||||||
|
{colony.code}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor={`evaluation-edit-notes-${evaluation.id}`}>
|
||||||
|
Notizen
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id={`evaluation-edit-notes-${evaluation.id}`}
|
||||||
|
value={notes}
|
||||||
|
onChange={(event) => setNotes(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{error ? (
|
{error ? (
|
||||||
<p role="alert" className="text-sm text-destructive">
|
<p role="alert" className="text-sm text-destructive">
|
||||||
{error}
|
{error}
|
||||||
|
|||||||
@@ -2,9 +2,12 @@
|
|||||||
|
|
||||||
import { FormEvent, useState } from "react";
|
import { FormEvent, useState } from "react";
|
||||||
import { Pencil } from "lucide-react";
|
import { Pencil } from "lucide-react";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { updateTaskTemplate } from "@/lib/task-templates/api";
|
import { updateTaskTemplate } from "@/lib/task-templates/api";
|
||||||
import { TaskTemplate, TaskTemplateRecurrenceType } from "@/lib/task-templates/types";
|
import { TaskTemplate, TaskTemplateRecurrenceType } from "@/lib/task-templates/types";
|
||||||
|
import { TaskPriority } from "@/lib/tasks/types";
|
||||||
|
import { TASK_PRIORITIES, TASK_PRIORITY_LABELS } from "@/lib/tasks/labels";
|
||||||
|
import { fetchUsers } from "@/lib/users/api";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -25,6 +28,10 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
/** Sentinel for "nobody assigned" - an empty select value renders as the
|
||||||
|
* placeholder instead of a choosable entry. */
|
||||||
|
const NO_ASSIGNEE = "__none__";
|
||||||
|
|
||||||
const RECURRENCE_LABELS: Record<TaskTemplateRecurrenceType, string> = {
|
const RECURRENCE_LABELS: Record<TaskTemplateRecurrenceType, string> = {
|
||||||
YEARLY: "Jährlich",
|
YEARLY: "Jährlich",
|
||||||
MONTHLY: "Monatlich",
|
MONTHLY: "Monatlich",
|
||||||
@@ -61,8 +68,23 @@ export function EditTaskTemplateDialog({ template }: { template: TaskTemplate })
|
|||||||
template.recurrenceDayOfMonth?.toString() ?? "1",
|
template.recurrenceDayOfMonth?.toString() ?? "1",
|
||||||
);
|
);
|
||||||
const [intervalDays, setIntervalDays] = useState(template.intervalDays?.toString() ?? "14");
|
const [intervalDays, setIntervalDays] = useState(template.intervalDays?.toString() ?? "14");
|
||||||
|
const [priority, setPriority] = useState<TaskPriority>(
|
||||||
|
template.priority ?? "NORMAL",
|
||||||
|
);
|
||||||
|
const [assignedUserId, setAssignedUserId] = useState(
|
||||||
|
template.assignedUserId ?? NO_ASSIGNEE,
|
||||||
|
);
|
||||||
|
const [active, setActive] = useState(template.active);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Assigning needs users:read, which not every role has - if the request
|
||||||
|
// fails the field is simply omitted rather than breaking the dialog.
|
||||||
|
const { data: users } = useQuery({
|
||||||
|
queryKey: ["users"],
|
||||||
|
queryFn: () => fetchUsers(),
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: () =>
|
mutationFn: () =>
|
||||||
updateTaskTemplate(template.id, {
|
updateTaskTemplate(template.id, {
|
||||||
@@ -75,6 +97,10 @@ export function EditTaskTemplateDialog({ template }: { template: TaskTemplate })
|
|||||||
? Number(recurrenceDayOfMonth)
|
? Number(recurrenceDayOfMonth)
|
||||||
: undefined,
|
: undefined,
|
||||||
intervalDays: recurrenceType === "INTERVAL_DAYS" ? Number(intervalDays) : undefined,
|
intervalDays: recurrenceType === "INTERVAL_DAYS" ? Number(intervalDays) : undefined,
|
||||||
|
priority,
|
||||||
|
assignedUserId:
|
||||||
|
assignedUserId === NO_ASSIGNEE ? undefined : assignedUserId,
|
||||||
|
active,
|
||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
void queryClient.invalidateQueries({ queryKey: ["task-templates"] });
|
void queryClient.invalidateQueries({ queryKey: ["task-templates"] });
|
||||||
@@ -190,6 +216,60 @@ export function EditTaskTemplateDialog({ template }: { template: TaskTemplate })
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="template-edit-priority">Priorität</Label>
|
||||||
|
<Select
|
||||||
|
value={priority}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setPriority((value ?? priority) as TaskPriority)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="template-edit-priority" className="w-full">
|
||||||
|
<SelectValue>{TASK_PRIORITY_LABELS[priority]}</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{TASK_PRIORITIES.map((option) => (
|
||||||
|
<SelectItem key={option} value={option}>
|
||||||
|
{TASK_PRIORITY_LABELS[option]}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
{users ? (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="template-edit-assignee">Zugewiesen an</Label>
|
||||||
|
<Select
|
||||||
|
value={assignedUserId}
|
||||||
|
onValueChange={(value) => setAssignedUserId(value ?? NO_ASSIGNEE)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="template-edit-assignee" className="w-full">
|
||||||
|
<SelectValue placeholder="Niemand" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={NO_ASSIGNEE}>Niemand</SelectItem>
|
||||||
|
{users.map((user) => (
|
||||||
|
<SelectItem key={user.id} value={user.id}>
|
||||||
|
{user.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<label
|
||||||
|
className="flex h-9 items-center gap-3 text-sm"
|
||||||
|
htmlFor="template-edit-active"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="template-edit-active"
|
||||||
|
type="checkbox"
|
||||||
|
className="size-4.5"
|
||||||
|
checked={active}
|
||||||
|
onChange={(event) => setActive(event.target.checked)}
|
||||||
|
/>
|
||||||
|
Aktiv – eine deaktivierte Vorlage erzeugt keine Aufgaben mehr
|
||||||
|
</label>
|
||||||
{error ? (
|
{error ? (
|
||||||
<p role="alert" className="text-sm text-destructive">
|
<p role="alert" className="text-sm text-destructive">
|
||||||
{error}
|
{error}
|
||||||
|
|||||||
@@ -5,8 +5,18 @@ import { Pencil } from "lucide-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 { updateTask } from "@/lib/tasks/api";
|
import { updateTask } from "@/lib/tasks/api";
|
||||||
import { Task, TaskPriority, TaskRelatedType } from "@/lib/tasks/types";
|
import {
|
||||||
import { TASK_PRIORITIES, TASK_PRIORITY_LABELS } from "@/lib/tasks/labels";
|
Task,
|
||||||
|
TaskPriority,
|
||||||
|
TaskRelatedType,
|
||||||
|
TaskStatus,
|
||||||
|
} from "@/lib/tasks/types";
|
||||||
|
import {
|
||||||
|
TASK_PRIORITIES,
|
||||||
|
TASK_PRIORITY_LABELS,
|
||||||
|
TASK_STATUSES,
|
||||||
|
TASK_STATUS_LABELS,
|
||||||
|
} from "@/lib/tasks/labels";
|
||||||
import { fetchUsers } from "@/lib/users/api";
|
import { fetchUsers } from "@/lib/users/api";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -38,6 +48,7 @@ export function TaskEditDialog({ task }: { task: Task }) {
|
|||||||
const [assignedUserId, setAssignedUserId] = useState(task.assignedUserId ?? "");
|
const [assignedUserId, setAssignedUserId] = useState(task.assignedUserId ?? "");
|
||||||
const [relatedType, setRelatedType] = useState<TaskRelatedType>(task.relatedType);
|
const [relatedType, setRelatedType] = useState<TaskRelatedType>(task.relatedType);
|
||||||
const [relatedId, setRelatedId] = useState(task.relatedId ?? "");
|
const [relatedId, setRelatedId] = useState(task.relatedId ?? "");
|
||||||
|
const [status, setStatus] = useState<TaskStatus>(task.status);
|
||||||
|
|
||||||
// Assigning a task requires users:read, which not every role has - if the
|
// Assigning a task requires users:read, which not every role has - if the
|
||||||
// request fails, the assignee field below is simply omitted rather than
|
// request fails, the assignee field below is simply omitted rather than
|
||||||
@@ -58,6 +69,7 @@ export function TaskEditDialog({ task }: { task: Task }) {
|
|||||||
assignedUserId: assignedUserId || undefined,
|
assignedUserId: assignedUserId || undefined,
|
||||||
relatedType,
|
relatedType,
|
||||||
relatedId: relatedType === "GENERAL" ? undefined : relatedId || undefined,
|
relatedId: relatedType === "GENERAL" ? undefined : relatedId || undefined,
|
||||||
|
status,
|
||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
void queryClient.invalidateQueries({ queryKey: ["tasks"] });
|
void queryClient.invalidateQueries({ queryKey: ["tasks"] });
|
||||||
@@ -138,6 +150,30 @@ export function TaskEditDialog({ task }: { task: Task }) {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="task-edit-status">Status</Label>
|
||||||
|
<Select
|
||||||
|
value={status}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setStatus((value ?? status) as TaskStatus)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="task-edit-status" className="w-full">
|
||||||
|
<SelectValue>{TASK_STATUS_LABELS[status]}</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{TASK_STATUSES.map((option) => (
|
||||||
|
<SelectItem key={option} value={option}>
|
||||||
|
{TASK_STATUS_LABELS[option]}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Hier lässt sich eine versehentlich erledigte oder abgebrochene
|
||||||
|
Aufgabe wieder öffnen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<TaskRelatedField
|
<TaskRelatedField
|
||||||
idPrefix="task-edit-related"
|
idPrefix="task-edit-related"
|
||||||
relatedType={relatedType}
|
relatedType={relatedType}
|
||||||
|
|||||||
Reference in new issue
Block a user