Workflow timeline visualization#
The frontend counterpart to the backend approval workflow. A modal that fetches the workflow steps for a record and renders them as a Mantine timeline.
Component#
// src/components/workflow/WorkflowTimeline.tsx
import { useEffect, useState } from 'react';
import { Badge, Button, Modal, Text, Timeline } from '@mantine/core';
import { IconCheck, IconX, IconClock } from '@tabler/icons-react';
import AppStoreFactory from '../../wire/StoreFactory';
interface Props {
entityId: string | number;
endPoint: string; // e.g. '/invoice/{invoiceId}/workflow'
open: boolean;
onClose: () => void;
title?: string;
}
const statusIcon: Record<string, any> = {
COMPLETED: <IconCheck size={14} />,
APPROVED: <IconCheck size={14} />,
REJECTED: <IconX size={14} />,
PENDING: <IconClock size={14} />,
};
const statusColor: Record<string, string> = {
COMPLETED: 'green', APPROVED: 'green', REJECTED: 'red', PENDING: 'gray',
};
interface Step {
stepOrder: number;
groupName: string;
status: string;
actedBy?: string;
actedAt?: string;
remarks?: string;
}
export default function WorkflowTimeline({ entityId, endPoint, open, onClose, title }: Props) {
const [steps, setSteps] = useState<Step[]>([]);
useEffect(() => {
if (!open) return;
const store = AppStoreFactory.getGridStore(
{ endPointOptions: { invoiceId: entityId } },
endPoint
);
store.query({ sortOrder: { stepOrder: 'asc' }, limit: 100 }).then(r => {
setSteps(r.result ?? []);
});
}, [open, entityId]);
return (
<Modal opened={open} onClose={onClose} title={title ?? 'Approval History'} size="lg">
{steps.length === 0 ? (
<Text c="dimmed">No workflow history found.</Text>
) : (
<Timeline active={steps.length - 1} bulletSize={24} lineWidth={2}>
{steps.map((step, i) => (
<Timeline.Item
key={i}
bullet={statusIcon[step.status] ?? <IconClock size={14} />}
color={statusColor[step.status] ?? 'gray'}
title={
<>
{step.groupName}
<Badge ml="xs" size="xs" color={statusColor[step.status] ?? 'gray'}>
{step.status}
</Badge>
</>
}
>
{step.actedBy && <Text size="sm">By: {step.actedBy}</Text>}
{step.actedAt && <Text size="xs" c="dimmed">{new Date(step.actedAt).toLocaleString()}</Text>}
{step.remarks && <Text size="sm" mt="xs" fs="italic">{step.remarks}</Text>}
</Timeline.Item>
))}
</Timeline>
)}
</Modal>
);
}Using it on a detail page#
import { useDisclosure } from '@mantine/hooks';
import WorkflowTimeline from '../../components/workflow/WorkflowTimeline';
function InvoiceViewPage() {
const params = useParams();
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Button variant="subtle" onClick={open}>View approval history</Button>
<WorkflowTimeline
entityId={params.id!}
endPoint="/invoice/{invoiceId}/workflow"
open={opened}
onClose={close}
title="Invoice Approval History"
/>
</>
);
}See also: Approval workflow, PalmyraGridStore.