Add measure listing page

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Jonathan
2025-05-26 00:20:26 +02:00
committed by Sacha Al Himdani
parent f9d1f033e0
commit 7903bd3a77
21 changed files with 1301 additions and 6 deletions

View File

@@ -7,7 +7,7 @@ import type {
import { Link } from "react-router";
import { tv, type VariantProps } from "tailwind-variants";
const button = tv({
export const button = tv({
base: "flex items-center justify-center gap-[6px] px-3 py-2 rounded-full cursor-pointer text-sm font-medium h-8 focus:outline-none",
variants: {
variant: {

View File

@@ -3,9 +3,12 @@ import { Card } from "../Card/Card";
import { Link } from "react-router";
import clsx from "clsx";
export function Table({ children }: PropsWithChildren) {
export function Table({
children,
className,
}: PropsWithChildren<{ className?: string }>) {
return (
<Card className="relative w-full overflow-auto">
<Card className={clsx("relative w-full overflow-auto", className)}>
<table className="w-full text-left">{children}</table>
</Card>
);
@@ -59,17 +62,26 @@ export function Td({
children,
noLink,
className,
}: PropsWithChildren<{ noLink?: boolean; className?: string }>) {
width,
}: PropsWithChildren<{
noLink?: boolean;
className?: string;
width?: number;
}>) {
const { to } = useContext(TrContext);
if (!to || noLink) {
return (
<td className={clsx("first:pl-6 last:pr-6 py-3", className)}>
<td
width={width}
className={clsx("first:pl-6 last:pr-6 py-3", className)}
>
{children}
</td>
);
}
return (
<td
width={width}
className={clsx(
"first:*:pl-6 *:block last:*:pr-6 *:py-3",
className,

View File

@@ -0,0 +1,12 @@
import { MeasureBadge } from "./MeasureBadge";
import type { Meta, StoryObj } from "@storybook/react";
export default {
title: "Molecules/Badges/MeasureBadge",
component: MeasureBadge,
argTypes: {},
} satisfies Meta<typeof MeasureBadge>;
type Story = StoryObj<typeof MeasureBadge>;
export const Default: Story = {};

View File

@@ -0,0 +1,28 @@
import { getMeasureStateLabel } from "@probo/helpers";
import { Badge } from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import type { ComponentProps } from "react";
import type { measureStates } from "@probo/helpers";
type Props = {
state: (typeof measureStates)[number];
};
const stateToVariant: Record<
Props["state"],
ComponentProps<typeof Badge>["variant"]
> = {
IMPLEMENTED: "success",
IN_PROGRESS: "info",
NOT_APPLICABLE: "neutral",
NOT_STARTED: "neutral",
};
export function MeasureBadge({ state }: Props) {
const { __ } = useTranslate();
return (
<Badge variant={stateToVariant[state]}>
{getMeasureStateLabel(__, state)}
</Badge>
);
}

View File

@@ -0,0 +1,16 @@
import { FileButton } from "./FileButton";
import type { Meta, StoryObj } from "@storybook/react";
export default {
title: "Molecules/FileButton",
component: FileButton,
argTypes: {},
} satisfies Meta<typeof FileButton>;
type Story = StoryObj<typeof FileButton>;
export const Default: Story = {
args: {
children: "Upload",
},
};

View File

@@ -0,0 +1,32 @@
import type {
ChangeEventHandler,
ComponentProps,
PropsWithChildren,
RefObject,
} from "react";
import { button, Button } from "../../Atoms/Button/Button.tsx";
type Props = PropsWithChildren<{
onChange: ChangeEventHandler<HTMLInputElement>;
accept?: string;
disabled?: boolean;
className?: string;
ref?: RefObject<HTMLInputElement | null>;
}> &
Pick<ComponentProps<typeof Button>, "disabled" | "variant" | "icon">;
export function FileButton({
onChange,
children,
icon: IconComponent,
ref,
...props
}: Props) {
return (
<label className={button({ ...props })}>
{IconComponent && <IconComponent size={16} className="flex-none" />}
{children}
<input type="file" onChange={onChange} hidden ref={ref} />
</label>
);
}

View File

@@ -0,0 +1,30 @@
import { MeasureImplementation } from "./MeasureImplementation";
import type { Meta, StoryObj } from "@storybook/react";
import { times } from "@probo/helpers";
export default {
title: "Molecules/MeasureImplementation",
component: MeasureImplementation,
argTypes: {},
} satisfies Meta<typeof MeasureImplementation>;
type Story = StoryObj<typeof MeasureImplementation>;
export const Default: Story = {
args: {
measures: [
...times(15, () => ({
state: "IMPLEMENTED",
})),
...times(10, () => ({
state: "IN_PROGRESS",
})),
...times(5, () => ({
state: "NOT_APPLICABLE",
})),
...times(5, () => ({
state: "NOT_STARTED",
})),
] as any,
},
};

View File

@@ -0,0 +1,72 @@
import type { ComponentProps } from "react";
import type { MeasureBadge } from "../Badge/MeasureBadge.tsx";
import { useTranslate } from "@probo/i18n";
import { getMeasureStateLabel, measureStates } from "@probo/helpers";
import clsx from "clsx";
type MeasureState = ComponentProps<typeof MeasureBadge>["state"];
type Props = {
measures: { state: MeasureState }[];
className?: string;
};
const stateToColor: Record<MeasureState, string> = {
IMPLEMENTED: "bg-border-success",
IN_PROGRESS: "bg-border-warning",
NOT_APPLICABLE: "bg-border-info",
NOT_STARTED: "bg-highlight",
};
export function MeasureImplementation({ measures, className }: Props) {
const { __ } = useTranslate();
const counts = measures.reduce(
(acc, measure) => {
acc[measure.state] = (acc[measure.state] ?? 0) + 1;
return acc;
},
{} as Record<MeasureState, number>,
);
const percent = Math.round(
(100 *
((counts["IMPLEMENTED"] ?? 0) + (counts["NOT_APPLICABLE"] ?? 0))) /
measures.length,
);
return (
<div className={clsx("space-y-3", className)}>
<h2 className="text-base font-medium">
{__("Measure implementation")}
</h2>
<div className="h-2 rounded bg-highlight flex justify-stretch item-stretch">
{measureStates.map((state) => (
<div
key={state}
className={clsx(stateToColor[state])}
style={{
flexGrow: counts[state] ?? 0,
}}
></div>
))}
</div>
<div className="flex gap-4 text-sm">
<div className="mr-auto">
{percent}% {__("Complete")}
</div>
{measureStates.map((state) => (
<div
key={state}
className="text-sm text-txt-secondary flex items-center gap-[6px]"
>
<div
className={clsx(
"size-[10px] rounded-full",
stateToColor[state],
)}
></div>
{getMeasureStateLabel(__, state)}
</div>
))}
</div>
</div>
);
}

View File

@@ -14,7 +14,7 @@ export function PageHeader({ title, description, children }: Props) {
<p className="text-sm text-txt-secondary">{description}</p>
)}
</div>
<div>{children}</div>
<div className="flex gap-3">{children}</div>
</div>
);
}

View File

@@ -46,6 +46,9 @@ export { ConfirmDialog } from "./Molecules/Dialog/ConfirmDialog.tsx";
export { RisksChart } from "./Molecules/Risks/RisksChart";
export { RiskOverview } from "./Molecules/Risks/RiskOverview";
export { Combobox, ComboboxItem } from "./Molecules/Combobox/Combobox";
export { FileButton } from "./Molecules/FileButton/FileButton";
export { MeasureBadge } from "./Molecules/Badge/MeasureBadge";
export { MeasureImplementation } from "./Molecules/MeasureImplementation/MeasureImplementation";
// Hooks
export { useToast, Toasts } from "./Atoms/Toasts/Toasts";