resolve confilict

This commit is contained in:
marshal
2026-06-05 14:07:26 +03:00
40 changed files with 3823 additions and 768 deletions

View File

@@ -1,19 +1,35 @@
import { useMemo } from "react";
import { ShieldCheck } from "lucide-react";
import { useMemo, useState } from "react";
import { Check, ShieldCheck } from "lucide-react";
import type { BookingApprovalStep, BookingDetail } from "@/types/booking";
import { BookingConfirmDialog } from "./BookingConfirmDialog";
import { useAuth } from "@/auth/useAuth";
import { formatApprovalProgress } from "@/features/bookings/approval-progress";
import { getNextPendingApprovalStep } from "@/features/bookings/booking-actions.config";
import {
buildApproveActionForStep,
canActOnApprovalStep,
getNextPendingApprovalStep,
} from "@/features/bookings/booking-actions.config";
import type { useBookingMutations } from "@/hooks/bookings/useBookings";
import type { BookingApprovalStep, BookingDetail } from "@/types/booking";
import { bookingGlass, bookingSurface } from "./booking-ui.styles";
import { Badge } from "@edr/ui-common";
import { Badge, Button } from "@edr/ui-common";
import { cn } from "@/lib/utils";
type Mutations = ReturnType<typeof useBookingMutations>;
interface ApprovalStepsCardProps {
booking: BookingDetail;
mutations: Mutations;
}
/** Read-only approval chain visualization; actions live in BookingActionsToolbar. */
export function ApprovalStepsCard({ booking }: ApprovalStepsCardProps) {
/** Approval chain with inline approve on the current pending step. */
export function ApprovalStepsCard({ booking, mutations }: ApprovalStepsCardProps) {
const { user } = useAuth();
const [confirmOpen, setConfirmOpen] = useState(false);
const [pendingStep, setPendingStep] = useState<BookingApprovalStep | null>(
null,
);
const steps = useMemo(
() =>
[...(booking.approvalSteps ?? [])].sort(
@@ -24,57 +40,110 @@ export function ApprovalStepsCard({ booking }: ApprovalStepsCardProps) {
const nextPending = getNextPendingApprovalStep(steps);
const summary = formatApprovalProgress(booking.status, steps);
const pendingAction = pendingStep
? buildApproveActionForStep(pendingStep)
: null;
const openApprove = (step: BookingApprovalStep) => {
setPendingStep(step);
setConfirmOpen(true);
};
const closeApprove = () => {
setConfirmOpen(false);
setPendingStep(null);
};
const runApprove = () => {
if (!pendingStep) return;
mutations.approveStep.mutate(
{ stepId: pendingStep.id, requiredRole: pendingStep.requiredRole },
{ onSuccess: () => closeApprove() },
);
};
return (
<div className={cn(bookingSurface.sectionCard, bookingGlass.activeTab)}>
<div className={bookingSurface.sectionHeader}>
<div className={bookingSurface.sectionIcon}>
<ShieldCheck className="size-4" strokeWidth={1.75} />
<>
<div className={cn(bookingSurface.sectionCard, bookingGlass.activeTab)}>
<div className={bookingSurface.sectionHeader}>
<div className={bookingSurface.sectionIcon}>
<ShieldCheck className="size-4" strokeWidth={1.75} />
</div>
<div>
<h2 className="text-sm font-semibold text-foreground">
Approval chain
</h2>
<p className="text-xs text-muted-foreground">
{summary.detail ||
(nextPending
? `Next: ${nextPending.requiredRole} · step ${nextPending.stepOrder}`
: steps.length
? "All steps complete"
: "Accept submission to begin")}
</p>
</div>
</div>
<div>
<h2 className="text-sm font-semibold text-foreground">
Approval chain
</h2>
<p className="text-xs text-muted-foreground">
{summary.detail ||
(nextPending
? `Next: ${nextPending.requiredRole} · step ${nextPending.stepOrder}`
: steps.length
? "All steps complete"
: "Accept submission to begin")}
</p>
<div className="px-5 py-5">
{steps.length === 0 ? (
<p className="rounded-lg border border-dashed border-border/60 bg-muted/10 px-4 py-6 text-center text-sm text-muted-foreground backdrop-blur-sm">
Use{" "}
<strong className="font-semibold text-foreground">
Accept for approval
</strong>{" "}
in staff actions to instantiate steps.
</p>
) : (
<ul className="space-y-2">
{steps.map((step) => (
<StepRow
key={step.id}
step={step}
steps={steps}
user={user}
isNext={nextPending?.id === step.id}
isPending={mutations.approveStep.isPending}
onApprove={openApprove}
/>
))}
</ul>
)}
</div>
</div>
<div className="px-5 py-5">
{steps.length === 0 ? (
<p className="rounded-lg border border-dashed border-border/60 bg-muted/10 px-4 py-6 text-center text-sm text-muted-foreground backdrop-blur-sm">
Use <strong className="font-semibold text-foreground">Accept for approval</strong>{" "}
in staff actions to instantiate steps.
</p>
) : (
<ul className="space-y-2">
{steps.map((step) => (
<StepRow
key={step.id}
step={step}
isNext={nextPending?.id === step.id}
/>
))}
</ul>
)}
</div>
</div>
<BookingConfirmDialog
open={confirmOpen}
onOpenChange={(open) => {
if (!open) closeApprove();
else setConfirmOpen(true);
}}
action={pendingAction}
reference={booking.reference}
inputValue=""
onInputChange={() => {}}
onConfirm={runApprove}
isPending={mutations.approveStep.isPending}
/>
</>
);
}
function StepRow({
step,
steps,
user,
isNext,
isPending,
onApprove,
}: {
step: BookingApprovalStep;
steps: BookingApprovalStep[];
user: ReturnType<typeof useAuth>["user"];
isNext: boolean;
isPending: boolean;
onApprove: (step: BookingApprovalStep) => void;
}) {
const canApprove = canActOnApprovalStep(user, step, steps);
const statusStyles =
step.status === "APPROVED"
? "border-emerald-500/25 bg-emerald-500/10 text-black"
@@ -88,9 +157,7 @@ function StepRow({
<li
className={cn(
"flex items-center justify-between gap-3 rounded-lg border px-4 py-3 transition-colors backdrop-blur-sm",
isNext
? bookingGlass.activeTab
: "border-border/50 bg-card/60",
isNext ? bookingGlass.activeTab : "border-border/50 bg-card/60",
)}
>
<div className="flex min-w-0 items-center gap-3">
@@ -115,12 +182,26 @@ function StepRow({
)}
</div>
</div>
<Badge
variant="outline"
className={cn("shrink-0 border text-[9px] uppercase", statusStyles)}
>
{step.status}
</Badge>
<div className="flex shrink-0 items-center gap-2">
{canApprove && (
<Button
type="button"
size="sm"
className="h-8 gap-1.5 shadow-sm"
disabled={isPending}
onClick={() => onApprove(step)}
>
<Check className="size-3.5" />
Approve
</Button>
)}
<Badge
variant="outline"
className={cn("shrink-0 border text-[9px] uppercase", statusStyles)}
>
{step.status}
</Badge>
</div>
</li>
);
}