ui improvemnt

This commit is contained in:
Marshal
2026-06-06 21:17:20 +00:00
parent b62ea3f95f
commit 08b46c306f
35 changed files with 2731 additions and 1604 deletions

View File

@@ -1,5 +1,6 @@
import { useMemo, useState } from "react";
import { Check, ShieldCheck } from "lucide-react";
import { Stack, Group, Text, Badge, Button, Box } from "@mantine/core";
import { BookingConfirmDialog } from "./BookingConfirmDialog";
import { useAuth } from "@/auth/useAuth";
@@ -11,9 +12,7 @@ import {
} 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, Button } from "@edr/ui-common";
import { cn } from "@/lib/utils";
import { SectionCard } from "./detail/SectionCard";
type Mutations = ReturnType<typeof useBookingMutations>;
@@ -26,23 +25,16 @@ interface ApprovalStepsCardProps {
export function ApprovalStepsCard({ booking, mutations }: ApprovalStepsCardProps) {
const { user } = useAuth();
const [confirmOpen, setConfirmOpen] = useState(false);
const [pendingStep, setPendingStep] = useState<BookingApprovalStep | null>(
null,
);
const [pendingStep, setPendingStep] = useState<BookingApprovalStep | null>(null);
const steps = useMemo(
() =>
[...(booking.approvalSteps ?? [])].sort(
(a, b) => a.stepOrder - b.stepOrder,
),
() => [...(booking.approvalSteps ?? [])].sort((a, b) => a.stepOrder - b.stepOrder),
[booking.approvalSteps],
);
const nextPending = getNextPendingApprovalStep(steps);
const summary = formatApprovalProgress(booking.status, steps);
const pendingAction = pendingStep
? buildApproveActionForStep(pendingStep)
: null;
const pendingAction = pendingStep ? buildApproveActionForStep(pendingStep) : null;
const openApprove = (step: BookingApprovalStep) => {
setPendingStep(step);
@@ -62,54 +54,60 @@ export function ApprovalStepsCard({ booking, mutations }: ApprovalStepsCardProps
);
};
const subtitle =
summary.detail ||
(nextPending
? `Next: ${nextPending.requiredRole} · step ${nextPending.stepOrder}`
: steps.length
? "All steps complete"
: "Accept submission to begin");
return (
<>
<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>
<SectionCard
icon={ShieldCheck}
title="Approval chain"
extra={
<Badge color="green" variant="light" radius="sm">
{steps.filter((s) => s.status === "APPROVED").length}/{steps.length}
</Badge>
}
>
<Text size="xs" c="dimmed" mb="sm">
{subtitle}
</Text>
<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>
{steps.length === 0 ? (
<Text
size="sm"
c="dimmed"
ta="center"
py="lg"
px="md"
style={{
borderRadius: 8,
border: "1px dashed var(--mantine-color-gray-3)",
background: "var(--mantine-color-gray-0)",
}}
>
Use <strong>Accept for approval</strong> in staff actions to instantiate steps.
</Text>
) : (
<Stack gap="xs">
{steps.map((step) => (
<StepRow
key={step.id}
step={step}
steps={steps}
user={user}
isNext={nextPending?.id === step.id}
isPending={mutations.approveStep.isPending}
onApprove={openApprove}
/>
))}
</Stack>
)}
</SectionCard>
<BookingConfirmDialog
open={confirmOpen}
@@ -144,64 +142,74 @@ function StepRow({
onApprove: (step: BookingApprovalStep) => void;
}) {
const canApprove = canActOnApprovalStep(user, step, steps);
const statusStyles =
const statusColor =
step.status === "APPROVED"
? "border-emerald-500/25 bg-emerald-500/10 text-black"
? "green"
: step.status === "REJECTED"
? "bg-red-500/10 text-red-800 dark:text-red-300"
? "red"
: isNext
? "border-emerald-500/25 bg-emerald-500/10 text-black"
: "bg-muted/40 text-muted-foreground";
? "green"
: "gray";
const highlighted = isNext || step.status === "APPROVED";
return (
<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",
)}
<Group
justify="space-between"
wrap="nowrap"
gap="sm"
px="sm"
py="xs"
style={{
borderRadius: 8,
border: `1px solid ${highlighted ? "var(--mantine-color-green-2)" : "var(--mantine-color-gray-2)"}`,
background: isNext ? "var(--mantine-color-green-0)" : "white",
}}
>
<div className="flex min-w-0 items-center gap-3">
<span
className={cn(
"flex size-8 shrink-0 items-center justify-center rounded-lg text-xs font-bold",
isNext
? cn(bookingGlass.iconWellGreen, "text-black")
: "bg-muted/40 text-muted-foreground",
)}
<Group gap="sm" wrap="nowrap" style={{ minWidth: 0 }}>
<Box
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 28,
height: 28,
borderRadius: 8,
flexShrink: 0,
fontSize: 12,
fontWeight: 700,
background: isNext ? "var(--mantine-color-green-1)" : "var(--mantine-color-gray-1)",
color: isNext ? "var(--mantine-color-green-8)" : "var(--mantine-color-gray-6)",
}}
>
{step.stepOrder}
</span>
<div className="min-w-0">
<p className="text-sm font-semibold text-foreground">
</Box>
<Box style={{ minWidth: 0 }}>
<Text size="sm" fw={600}>
{step.requiredRole}
</p>
</Text>
{step.remarks && (
<p className="truncate text-xs text-muted-foreground">
<Text size="xs" c="dimmed" truncate>
{step.remarks}
</p>
</Text>
)}
</div>
</div>
<div className="flex shrink-0 items-center gap-2">
</Box>
</Group>
<Group gap="xs" wrap="nowrap" style={{ flexShrink: 0 }}>
{canApprove && (
<Button
type="button"
size="sm"
className="h-8 gap-1.5 shadow-sm"
size="compact-sm"
color="green"
leftSection={<Check size={14} />}
disabled={isPending}
onClick={() => onApprove(step)}
>
<Check className="size-3.5" />
Approve
</Button>
)}
<Badge
variant="outline"
className={cn("shrink-0 border text-[9px] uppercase", statusStyles)}
>
<Badge variant="light" color={statusColor} size="sm" radius="sm" tt="uppercase">
{step.status}
</Badge>
</div>
</li>
</Group>
</Group>
);
}