This repository has been archived on 2026-08-11. You can view files and clone it, but cannot push or open issues or pull requests.
Gishen-Admin/src/components/detail/ApprovalStepper.tsx
kirukib fce0149b1a Ship full Admin console polish and update backend spec.
Add medication Items catalogue, counter POS/QR purchase, profile/login,
detail pages, loyalty ledger, Cmd+K nav, KPI strips/charts, avatars, and
i18n; document the new APIs and open questions for the shared backend team.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 00:03:21 +03:00

84 lines
3.0 KiB
TypeScript

import { Check, X } from 'lucide-react'
import { cn, formatDateTime } from '@/lib/utils'
export type ApprovalStepState = 'done' | 'current' | 'upcoming' | 'failed'
export type ApprovalStep = {
key: string
label: string
state: ApprovalStepState
/** Who moved the record into this stage. */
actor?: string
at?: string
hint?: string
}
const markerClasses: Record<ApprovalStepState, string> = {
done: 'border-brand-500 bg-brand-500 text-white',
current: 'border-orange bg-orange/10 text-orange-deep',
upcoming: 'border-grayScale-200 bg-grayScale-100 text-grayScale-400',
failed: 'border-destructive bg-destructive text-white',
}
const labelClasses: Record<ApprovalStepState, string> = {
done: 'text-foreground',
current: 'text-orange-deep',
upcoming: 'text-grayScale-400',
failed: 'text-destructive',
}
/**
* Horizontal review progress for records that move through an approval
* workflow. Stacks to a vertical list below `sm`.
*/
export function ApprovalStepper({ steps, className }: { steps: ApprovalStep[]; className?: string }) {
return (
<ol className={cn('flex flex-col gap-4 sm:flex-row sm:gap-0', className)}>
{steps.map((step, index) => {
const isLast = index === steps.length - 1
const connectorDone = step.state === 'done'
return (
<li key={step.key} className="flex min-w-0 gap-3 sm:flex-1 sm:flex-col sm:gap-0">
<div className="flex flex-col items-center sm:w-full sm:flex-row">
<span
className={cn(
'flex h-7 w-7 shrink-0 items-center justify-center rounded-full border-2 text-[11px] font-semibold',
markerClasses[step.state],
)}
>
{step.state === 'done' ? (
<Check className="h-3.5 w-3.5" aria-hidden="true" />
) : step.state === 'failed' ? (
<X className="h-3.5 w-3.5" aria-hidden="true" />
) : (
index + 1
)}
</span>
{!isLast ? (
<span
className={cn(
'my-1 w-0.5 flex-1 rounded-full sm:my-0 sm:ml-2 sm:h-0.5 sm:w-auto',
connectorDone ? 'bg-brand-300' : 'bg-grayScale-200',
)}
aria-hidden="true"
/>
) : null}
</div>
<div className={cn('min-w-0 pb-1 sm:mt-3 sm:pr-4', isLast && 'sm:pr-0')}>
<p className={cn('text-sm font-semibold', labelClasses[step.state])}>{step.label}</p>
{step.actor ? (
<p className="mt-0.5 truncate text-xs text-muted-foreground">{step.actor}</p>
) : null}
{step.at ? (
<p className="mt-0.5 text-[11px] text-muted-foreground">{formatDateTime(step.at)}</p>
) : null}
{step.hint ? <p className="mt-0.5 text-[11px] text-grayScale-400">{step.hint}</p> : null}
</div>
</li>
)
})}
</ol>
)
}