fix(estimates): allow approved estimate revocation
This commit is contained in:
parent
31bdc1047d
commit
8d37eb29b3
14 changed files with 295 additions and 8 deletions
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { AlertTriangle, FileCheck2, PackageSearch, Plus, Send, Trash2, XCircle } from "lucide-react";
|
||||
import { AlertTriangle, FileCheck2, PackageSearch, Plus, Send, Trash2, Undo2, XCircle } from "lucide-react";
|
||||
|
||||
import ConfirmDialog from "@/components/common/ConfirmDialog";
|
||||
import { useToast } from "@/components/common/ToastProvider";
|
||||
|
|
@ -42,6 +42,7 @@ const statusLabels: Record<string, string> = {
|
|||
declined: "Abgelehnt",
|
||||
expired: "Abgelaufen",
|
||||
cancelled: "Storniert",
|
||||
revoked: "Zurückgenommen",
|
||||
};
|
||||
|
||||
function humanizeValidationDetail(detail: unknown): string | null {
|
||||
|
|
@ -162,6 +163,7 @@ type Props = {
|
|||
canUpdate: boolean;
|
||||
canDelete: boolean;
|
||||
canSend: boolean;
|
||||
canRevoke: boolean;
|
||||
};
|
||||
|
||||
export default function RepairEstimatesSection({
|
||||
|
|
@ -172,6 +174,7 @@ export default function RepairEstimatesSection({
|
|||
canUpdate,
|
||||
canDelete,
|
||||
canSend,
|
||||
canRevoke,
|
||||
}: Props) {
|
||||
const { showToast } = useToast();
|
||||
const [estimates, setEstimates] = useState<RepairEstimate[]>([]);
|
||||
|
|
@ -184,6 +187,7 @@ export default function RepairEstimatesSection({
|
|||
const [pendingId, setPendingId] = useState<number | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<RepairEstimate | null>(null);
|
||||
const [cancelTarget, setCancelTarget] = useState<RepairEstimate | null>(null);
|
||||
const [revokeTarget, setRevokeTarget] = useState<RepairEstimate | null>(null);
|
||||
const [inventoryDialogOpen, setInventoryDialogOpen] = useState(false);
|
||||
const [inventoryItems, setInventoryItems] = useState<InventoryItem[]>([]);
|
||||
const [inventoryCategories, setInventoryCategories] = useState<InventoryCategory[]>([]);
|
||||
|
|
@ -388,6 +392,21 @@ export default function RepairEstimatesSection({
|
|||
}
|
||||
}
|
||||
|
||||
async function revokeEstimate() {
|
||||
if (!revokeTarget) return;
|
||||
setPendingId(revokeTarget.id);
|
||||
try {
|
||||
await api.post<RepairEstimate>(`/repairs/${repairId}/estimates/${revokeTarget.id}/revoke`);
|
||||
await loadEstimates();
|
||||
setRevokeTarget(null);
|
||||
showToast({ type: "success", title: "Freigabe zurückgenommen", description: "Die Reservierungen wurden freigegeben." });
|
||||
} catch (err) {
|
||||
showToast({ type: "error", title: "Freigabe konnte nicht zurückgenommen werden", description: getErrorMessage(err) });
|
||||
} finally {
|
||||
setPendingId(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteEstimate() {
|
||||
if (!deleteTarget) return;
|
||||
setPendingId(deleteTarget.id);
|
||||
|
|
@ -476,6 +495,7 @@ export default function RepairEstimatesSection({
|
|||
{canUpdate && ["draft", "sent"].includes(estimate.status) && <Button type="button" variant="outline" size="sm" onClick={() => openEditDialog(estimate)}>Bearbeiten</Button>}
|
||||
{canSend && ["draft", "sent"].includes(estimate.status) && <Button type="button" size="sm" onClick={() => void sendEstimate(estimate)} disabled={pendingId === estimate.id || !customerEmail}><Send />Senden</Button>}
|
||||
{(canUpdate || canSend) && ["draft", "sent"].includes(estimate.status) && <Button type="button" variant="outline" size="sm" onClick={() => setCancelTarget(estimate)} disabled={pendingId === estimate.id}><XCircle />Stornieren</Button>}
|
||||
{canRevoke && estimate.status === "approved" && <Button type="button" variant="destructive" size="sm" onClick={() => setRevokeTarget(estimate)} disabled={pendingId === estimate.id}><Undo2 />Freigabe zurücknehmen</Button>}
|
||||
{canDelete && ["draft", "cancelled"].includes(estimate.status) && <Button type="button" variant="destructive" size="sm" onClick={() => setDeleteTarget(estimate)} disabled={pendingId === estimate.id}><Trash2 />Löschen</Button>}
|
||||
</div>
|
||||
</article>
|
||||
|
|
@ -621,6 +641,18 @@ export default function RepairEstimatesSection({
|
|||
{cancelTarget && <p className="text-sm text-slate-600">{cancelTarget.estimate_number} · {cancelTarget.title}</p>}
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
open={Boolean(revokeTarget)}
|
||||
title="Freigabe zurücknehmen?"
|
||||
description="Möchten Sie den bereits freigegebenen Kostenvoranschlag wirklich zurücknehmen? Die Lagerreservierungen werden freigegeben und der Kunde erhält eine Benachrichtigung."
|
||||
confirmLabel="Freigabe zurücknehmen"
|
||||
pending={pendingId === revokeTarget?.id}
|
||||
onOpenChange={(open) => !open && setRevokeTarget(null)}
|
||||
onConfirm={() => void revokeEstimate()}
|
||||
>
|
||||
{revokeTarget && <p className="text-sm text-slate-600">{revokeTarget.estimate_number} · {revokeTarget.title}</p>}
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
open={Boolean(deleteTarget)}
|
||||
title="Kostenvoranschlag löschen?"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue