fix(estimates): add cancel confirmation and permissions
This commit is contained in:
parent
663bdc41b1
commit
31bdc1047d
2 changed files with 21 additions and 6 deletions
|
|
@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
|
|
||||||
from app.core.rbac import require_permission
|
from app.core.rbac import require_any_permission, require_permission
|
||||||
from app.db.database import get_db
|
from app.db.database import get_db
|
||||||
from app.models.repair import Repair
|
from app.models.repair import Repair
|
||||||
from app.models.repair_estimate import RepairEstimate
|
from app.models.repair_estimate import RepairEstimate
|
||||||
|
|
@ -117,7 +117,7 @@ def cancel_estimate(
|
||||||
estimate_id: int,
|
estimate_id: int,
|
||||||
request: Request,
|
request: Request,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(require_permission("repair_estimates.update")),
|
current_user: User = Depends(require_any_permission(["repair_estimates.update", "repair_estimates.send"])),
|
||||||
):
|
):
|
||||||
repair = get_repair_or_404(db, repair_id)
|
repair = get_repair_or_404(db, repair_id)
|
||||||
estimate = get_estimate_or_404(db, repair_id, estimate_id)
|
estimate = get_estimate_or_404(db, repair_id, estimate_id)
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,7 @@ export default function RepairEstimatesSection({
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [pendingId, setPendingId] = useState<number | null>(null);
|
const [pendingId, setPendingId] = useState<number | null>(null);
|
||||||
const [deleteTarget, setDeleteTarget] = useState<RepairEstimate | null>(null);
|
const [deleteTarget, setDeleteTarget] = useState<RepairEstimate | null>(null);
|
||||||
|
const [cancelTarget, setCancelTarget] = useState<RepairEstimate | null>(null);
|
||||||
const [inventoryDialogOpen, setInventoryDialogOpen] = useState(false);
|
const [inventoryDialogOpen, setInventoryDialogOpen] = useState(false);
|
||||||
const [inventoryItems, setInventoryItems] = useState<InventoryItem[]>([]);
|
const [inventoryItems, setInventoryItems] = useState<InventoryItem[]>([]);
|
||||||
const [inventoryCategories, setInventoryCategories] = useState<InventoryCategory[]>([]);
|
const [inventoryCategories, setInventoryCategories] = useState<InventoryCategory[]>([]);
|
||||||
|
|
@ -372,11 +373,13 @@ export default function RepairEstimatesSection({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cancelEstimate(estimate: RepairEstimate) {
|
async function cancelEstimate() {
|
||||||
setPendingId(estimate.id);
|
if (!cancelTarget) return;
|
||||||
|
setPendingId(cancelTarget.id);
|
||||||
try {
|
try {
|
||||||
await api.post<RepairEstimate>(`/repairs/${repairId}/estimates/${estimate.id}/cancel`);
|
await api.post<RepairEstimate>(`/repairs/${repairId}/estimates/${cancelTarget.id}/cancel`);
|
||||||
await loadEstimates();
|
await loadEstimates();
|
||||||
|
setCancelTarget(null);
|
||||||
showToast({ type: "success", title: "Kostenvoranschlag storniert" });
|
showToast({ type: "success", title: "Kostenvoranschlag storniert" });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast({ type: "error", title: "Stornierung fehlgeschlagen", description: getErrorMessage(err) });
|
showToast({ type: "error", title: "Stornierung fehlgeschlagen", description: getErrorMessage(err) });
|
||||||
|
|
@ -472,7 +475,7 @@ export default function RepairEstimatesSection({
|
||||||
<div className="mt-4 flex flex-wrap gap-2">
|
<div className="mt-4 flex flex-wrap gap-2">
|
||||||
{canUpdate && ["draft", "sent"].includes(estimate.status) && <Button type="button" variant="outline" size="sm" onClick={() => openEditDialog(estimate)}>Bearbeiten</Button>}
|
{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>}
|
{canSend && ["draft", "sent"].includes(estimate.status) && <Button type="button" size="sm" onClick={() => void sendEstimate(estimate)} disabled={pendingId === estimate.id || !customerEmail}><Send />Senden</Button>}
|
||||||
{canUpdate && ["draft", "sent"].includes(estimate.status) && <Button type="button" variant="outline" size="sm" onClick={() => void cancelEstimate(estimate)} disabled={pendingId === estimate.id}><XCircle />Stornieren</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>}
|
||||||
{canDelete && ["draft", "cancelled"].includes(estimate.status) && <Button type="button" variant="destructive" size="sm" onClick={() => setDeleteTarget(estimate)} disabled={pendingId === estimate.id}><Trash2 />Löschen</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>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
@ -606,6 +609,18 @@ export default function RepairEstimatesSection({
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
open={Boolean(cancelTarget)}
|
||||||
|
title="Kostenvoranschlag stornieren?"
|
||||||
|
description="Der Kostenvoranschlag wird storniert. Reservierte Lagerartikel werden freigegeben."
|
||||||
|
confirmLabel="Stornieren"
|
||||||
|
pending={pendingId === cancelTarget?.id}
|
||||||
|
onOpenChange={(open) => !open && setCancelTarget(null)}
|
||||||
|
onConfirm={() => void cancelEstimate()}
|
||||||
|
>
|
||||||
|
{cancelTarget && <p className="text-sm text-slate-600">{cancelTarget.estimate_number} · {cancelTarget.title}</p>}
|
||||||
|
</ConfirmDialog>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={Boolean(deleteTarget)}
|
open={Boolean(deleteTarget)}
|
||||||
title="Kostenvoranschlag löschen?"
|
title="Kostenvoranschlag löschen?"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue