Clean risk matrix

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-04-14 00:28:35 -07:00
parent 3711bb4bbb
commit ce6916309d
5 changed files with 335 additions and 292 deletions

View File

@@ -1,29 +1,29 @@
"use client";
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch };
export { Switch }

View File

@@ -105,11 +105,11 @@ function EditRiskViewContent({
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [inherentLikelihood, setInherentLikelihood] =
useState<string>("MEDIUM");
const [inherentImpact, setInherentImpact] = useState<string>("MEDIUM");
useState<string>("OCCASIONAL");
const [inherentImpact, setInherentImpact] = useState<string>("MODERATE");
const [residualLikelihood, setResidualLikelihood] =
useState<string>("MEDIUM");
const [residualImpact, setResidualImpact] = useState<string>("MEDIUM");
useState<string>("OCCASIONAL");
const [residualImpact, setResidualImpact] = useState<string>("MODERATE");
const [treatment, setTreatment] = useState<RiskTreatment>("MITIGATED");
const [ownerId, setOwnerId] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -119,34 +119,34 @@ function EditRiskViewContent({
// Helper function to convert float to likelihood string
const floatToLikelihood = (value: number): string => {
if (value <= 0.2) return "VERY_LOW";
if (value <= 0.4) return "LOW";
if (value <= 0.6) return "MEDIUM";
if (value <= 0.8) return "HIGH";
return "VERY_HIGH";
if (value <= 0.2) return "IMPROBABLE";
if (value <= 0.4) return "REMOTE";
if (value <= 0.6) return "OCCASIONAL";
if (value <= 0.8) return "PROBABLE";
return "FREQUENT";
};
// Helper function to convert float to impact string
const floatToImpact = (value: number): string => {
if (value <= 0.2) return "VERY_LOW";
if (value <= 0.2) return "NEGLIGIBLE";
if (value <= 0.4) return "LOW";
if (value <= 0.6) return "MEDIUM";
if (value <= 0.8) return "HIGH";
return "VERY_HIGH";
if (value <= 0.6) return "MODERATE";
if (value <= 0.8) return "SIGNIFICANT";
return "CATASTROPHIC";
};
// Map string values to float values
const likelihoodToFloat = (value: string): number => {
switch (value) {
case "VERY_LOW":
case "IMPROBABLE":
return 0.1;
case "LOW":
case "REMOTE":
return 0.3;
case "MEDIUM":
case "OCCASIONAL":
return 0.5;
case "HIGH":
case "PROBABLE":
return 0.7;
case "VERY_HIGH":
case "FREQUENT":
return 0.9;
default:
return 0.5;
@@ -155,15 +155,15 @@ function EditRiskViewContent({
const impactToFloat = (value: string): number => {
switch (value) {
case "VERY_LOW":
case "NEGLIGIBLE":
return 0.1;
case "LOW":
return 0.3;
case "MEDIUM":
case "MODERATE":
return 0.5;
case "HIGH":
case "SIGNIFICANT":
return 0.7;
case "VERY_HIGH":
case "CATASTROPHIC":
return 0.9;
default:
return 0.5;
@@ -308,15 +308,15 @@ function EditRiskViewContent({
value={inherentLikelihood}
onValueChange={setInherentLikelihood}
>
<SelectTrigger id="inherentLikelihood">
<SelectTrigger className="w-full">
<SelectValue placeholder="Select likelihood" />
</SelectTrigger>
<SelectContent className="max-h-[300px] overflow-y-auto">
<SelectItem value="VERY_LOW">Very Low</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MEDIUM">Medium</SelectItem>
<SelectItem value="HIGH">High</SelectItem>
<SelectItem value="VERY_HIGH">Very High</SelectItem>
<SelectContent>
<SelectItem value="IMPROBABLE">Improbable</SelectItem>
<SelectItem value="REMOTE">Remote</SelectItem>
<SelectItem value="OCCASIONAL">Occasional</SelectItem>
<SelectItem value="PROBABLE">Probable</SelectItem>
<SelectItem value="FREQUENT">Frequent</SelectItem>
</SelectContent>
</Select>
</div>
@@ -327,15 +327,15 @@ function EditRiskViewContent({
value={inherentImpact}
onValueChange={setInherentImpact}
>
<SelectTrigger id="inherentImpact">
<SelectTrigger className="w-full">
<SelectValue placeholder="Select impact" />
</SelectTrigger>
<SelectContent className="max-h-[300px] overflow-y-auto">
<SelectItem value="VERY_LOW">Very Low</SelectItem>
<SelectContent>
<SelectItem value="NEGLIGIBLE">Negligible</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MEDIUM">Medium</SelectItem>
<SelectItem value="HIGH">High</SelectItem>
<SelectItem value="VERY_HIGH">Very High</SelectItem>
<SelectItem value="MODERATE">Moderate</SelectItem>
<SelectItem value="SIGNIFICANT">Significant</SelectItem>
<SelectItem value="CATASTROPHIC">Catastrophic</SelectItem>
</SelectContent>
</Select>
</div>
@@ -382,15 +382,15 @@ function EditRiskViewContent({
value={residualLikelihood}
onValueChange={setResidualLikelihood}
>
<SelectTrigger id="residualLikelihood">
<SelectValue placeholder="Select residual likelihood" />
<SelectTrigger className="w-full">
<SelectValue placeholder="Select likelihood" />
</SelectTrigger>
<SelectContent className="max-h-[300px] overflow-y-auto">
<SelectItem value="VERY_LOW">Very Low</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MEDIUM">Medium</SelectItem>
<SelectItem value="HIGH">High</SelectItem>
<SelectItem value="VERY_HIGH">Very High</SelectItem>
<SelectContent>
<SelectItem value="IMPROBABLE">Improbable</SelectItem>
<SelectItem value="REMOTE">Remote</SelectItem>
<SelectItem value="OCCASIONAL">Occasional</SelectItem>
<SelectItem value="PROBABLE">Probable</SelectItem>
<SelectItem value="FREQUENT">Frequent</SelectItem>
</SelectContent>
</Select>
</div>
@@ -401,15 +401,15 @@ function EditRiskViewContent({
value={residualImpact}
onValueChange={setResidualImpact}
>
<SelectTrigger id="residualImpact">
<SelectValue placeholder="Select residual impact" />
<SelectTrigger className="w-full">
<SelectValue placeholder="Select impact" />
</SelectTrigger>
<SelectContent className="max-h-[300px] overflow-y-auto">
<SelectItem value="VERY_LOW">Very Low</SelectItem>
<SelectContent>
<SelectItem value="NEGLIGIBLE">Negligible</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MEDIUM">Medium</SelectItem>
<SelectItem value="HIGH">High</SelectItem>
<SelectItem value="VERY_HIGH">Very High</SelectItem>
<SelectItem value="MODERATE">Moderate</SelectItem>
<SelectItem value="SIGNIFICANT">Significant</SelectItem>
<SelectItem value="CATASTROPHIC">Catastrophic</SelectItem>
</SelectContent>
</Select>
</div>

View File

@@ -22,7 +22,7 @@ import { RiskViewSkeleton } from "./ListRiskPage";
import { ListRiskViewPaginationQuery } from "./__generated__/ListRiskViewPaginationQuery.graphql";
import { ListRiskView_risks$key } from "./__generated__/ListRiskView_risks.graphql";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Card, CardContent } from "@/components/ui/card";
import { Link } from "react-router";
import { Plus, Trash2, Edit } from "lucide-react";
import { useToast } from "@/hooks/use-toast";
@@ -110,10 +110,19 @@ const deleteRiskMutation = graphql`
`;
// Helper function to convert float values to percentage
// This is kept for potential future use, like tooltips
const floatToPercentage = (value: number): string => {
return `${Math.round(value * 100)}%`;
};
// Helper function to convert risk score to severity label
const riskScoreToSeverity = (score: number): string => {
if (score >= 0.75) return "Catastrophic";
if (score >= 0.5) return "Critical";
if (score >= 0.25) return "Marginal";
return "Negligible";
};
// Helper function to format treatment value
const formatTreatment = (treatment: string): string => {
const treatmentMap: Record<string, string> = {
@@ -182,20 +191,16 @@ function LoadBelowButton({
// Define colors for risk matrix cells
const riskMatrixColors = {
lowest: "bg-green-500 text-white",
low: "bg-lime-300 text-black",
low: "bg-green-500 text-white",
medium: "bg-yellow-300 text-black",
high: "bg-amber-400 text-white",
highest: "bg-red-500 text-white",
high: "bg-red-500 text-white",
};
// Empty cell variants (lighter colors)
const emptyRiskMatrixColors = {
lowest: "bg-green-50 text-black",
low: "bg-lime-50 text-black",
low: "bg-green-50 text-black",
medium: "bg-yellow-50 text-black",
high: "bg-amber-50 text-black",
highest: "bg-red-50 text-black",
high: "bg-red-50 text-black",
};
// Risk Matrix Component
@@ -213,31 +218,44 @@ function RiskMatrix({
}>;
isResidual?: boolean;
}): JSX.Element {
// Define likelihood and impact ranges for the 5x5 matrix
const likelihoodRanges: [number, number][] = [
[0.8, 1], // Highest likelihood
[0.6, 0.8], // High likelihood
[0.4, 0.6], // Medium likelihood
[0.2, 0.4], // Low likelihood
[0, 0.2], // Lowest likelihood
];
// Define impact ranges for the vertical axis (rows)
const impactRanges: [number, number][] = [
[0, 0.2], // Lowest impact
[0.2, 0.4], // Low impact
[0.4, 0.6], // Medium impact
[0.8, 1], // Highest impact - top row
[0.6, 0.8], // High impact
[0.8, 1], // Highest impact
[0.4, 0.6], // Medium impact
[0.2, 0.4], // Low impact
[0, 0.2], // Lowest impact - bottom row
];
const likelihoodLabels = ["Very High", "High", "Medium", "Low", "Very Low"];
// Define likelihood ranges for the horizontal axis (columns)
const likelihoodRanges: [number, number][] = [
[0, 0.2], // Lowest likelihood - leftmost column
[0.2, 0.4], // Low likelihood
[0.4, 0.6], // Medium likelihood
[0.6, 0.8], // High likelihood
[0.8, 1], // Highest likelihood - rightmost column
];
const impactLabels = ["Very Low", "Low", "Medium", "High", "Very High"];
const impactLabels = [
"Catastrophic",
"Significant",
"Moderate",
"Low",
"Negligible",
];
const likelihoodLabels = [
"Improbable",
"Remote",
"Occasional",
"Probable",
"Frequent",
];
// Function to get cell content with risks that fall in this cell
const getCellContent = (
likelihoodRange: [number, number],
impactRange: [number, number]
impactRange: [number, number],
likelihoodRange: [number, number]
) => {
return risks.filter((risk) => {
const likelihood = isResidual
@@ -257,48 +275,41 @@ function RiskMatrix({
};
// Helper to determine cell color based on position in matrix
// New matrix has rows indexed from top to bottom (0 = highest likelihood, 4 = lowest likelihood)
// and columns indexed from left to right (0 = lowest impact, 4 = highest impact)
// Matrix has rows indexed from top to bottom (0 = highest impact, 4 = lowest impact)
// and columns indexed from left to right (0 = lowest likelihood, 4 = highest likelihood)
const getCellColor = (row: number, col: number, isEmpty: boolean): string => {
const colorSet = isEmpty ? emptyRiskMatrixColors : riskMatrixColors;
// Top row (highest likelihood)
// Top row (highest impact - Catastrophic)
if (row === 0) {
if (col === 0) return colorSet.lowest;
if (col === 1) return colorSet.high;
return colorSet.highest;
if (col <= 1) return colorSet.medium; // Yellow for first two cells
return colorSet.high; // Red for the rest
}
// Second row
// Second row (Significant impact)
if (row === 1) {
if (col === 0) return colorSet.lowest;
if (col === 1) return colorSet.medium;
if (col === 2) return colorSet.high;
return colorSet.highest;
if (col === 0) return colorSet.low; // Green for first cell
if (col <= 2) return colorSet.medium; // Yellow for next two
return colorSet.high; // Red for the rest
}
// Middle row
// Middle row (Moderate impact)
if (row === 2) {
if (col === 0) return colorSet.lowest;
if (col === 1) return colorSet.low;
if (col === 2) return colorSet.medium;
if (col === 3) return colorSet.high;
return colorSet.highest;
if (col === 0) return colorSet.low; // Green for first cell
if (col <= 3) return colorSet.medium; // Yellow for next three
return colorSet.high; // Red for last cell
}
// Fourth row
// Fourth row (Low impact)
if (row === 3) {
if (col === 0) return colorSet.lowest;
if (col === 1) return colorSet.low;
if (col === 2 || col === 3) return colorSet.medium;
return colorSet.high;
if (col <= 1) return colorSet.low; // Green for first two
return colorSet.medium; // Yellow for the rest
}
// Bottom row (lowest likelihood)
// Bottom row (lowest impact - Negligible)
if (row === 4) {
if (col <= 1) return colorSet.lowest;
if (col <= 3) return colorSet.low;
return colorSet.medium;
if (col <= 3) return colorSet.low; // Green for first four
return colorSet.medium; // Yellow for last cell
}
return "bg-gray-100";
@@ -308,15 +319,15 @@ function RiskMatrix({
const RiskCell = ({
rowIndex,
colIndex,
likelihoodRange,
impactRange,
likelihoodRange,
}: {
rowIndex: number;
colIndex: number;
likelihoodRange: [number, number];
impactRange: [number, number];
likelihoodRange: [number, number];
}) => {
const cellRisks = getCellContent(likelihoodRange, impactRange);
const cellRisks = getCellContent(impactRange, likelihoodRange);
const isEmpty = cellRisks.length === 0;
const cellColor = getCellColor(rowIndex, colIndex, isEmpty);
@@ -360,43 +371,61 @@ function RiskMatrix({
};
return (
<div className="overflow-x-auto">
<div className="flex flex-col">
<div className="text-xs font-medium text-center mb-1">CONSEQUENCE</div>
<table className="w-full border-collapse table-fixed">
<thead>
<tr>
<th className="p-1 text-center border w-14"></th>
{impactLabels.map((label, index) => (
<th
key={index}
className="p-1 text-xs text-center border font-medium w-14"
>
{label}
</th>
))}
</tr>
</thead>
<tbody>
{likelihoodRanges.map((likelihoodRange, rowIndex) => (
<tr key={rowIndex}>
<th className="p-1 text-xs text-center border font-medium w-14 h-14">
{likelihoodLabels[rowIndex]}
</th>
{impactRanges.map((impactRange, colIndex) => (
<RiskCell
key={colIndex}
rowIndex={rowIndex}
colIndex={colIndex}
likelihoodRange={likelihoodRange}
impactRange={impactRange}
/>
<div className="space-y-2">
<div className="overflow-x-auto">
<div className="flex flex-col">
<div className="flex">
<div
className="text-xs font-semibold flex items-center justify-center mr-2"
style={{
writingMode: "vertical-rl",
transform: "rotate(180deg)",
alignSelf: "center",
}}
>
Impact
</div>
<table className="w-full border-collapse table-fixed">
<tbody>
{impactRanges.map((impactRange, rowIndex) => (
<tr key={rowIndex}>
<th className="p-1 text-xs text-center border-0 font-medium w-14 h-14">
{impactLabels[rowIndex]}
</th>
{likelihoodRanges.map((likelihoodRange, colIndex) => (
<RiskCell
key={colIndex}
rowIndex={rowIndex}
colIndex={colIndex}
impactRange={impactRange}
likelihoodRange={likelihoodRange}
/>
))}
</tr>
))}
</tr>
))}
</tbody>
</table>
<div className="text-xs font-medium ml-2 mt-1">LIKELIHOOD</div>
</tbody>
<tfoot>
<tr>
<th className="p-1 text-center border-0 w-14"></th>
{likelihoodLabels.map((label, index) => (
<th
key={index}
className="p-1 text-xs text-center border-t font-medium w-14"
>
{label}
</th>
))}
</tr>
</tfoot>
</table>
</div>
<div className="flex">
<div style={{ width: "3.5rem" }}></div>
<div className="text-center text-xs font-semibold mt-2 flex-1">
Likelihood
</div>
</div>
</div>
</div>
</div>
);
@@ -519,41 +548,43 @@ function ListRiskViewContent({
{/* Combined Risk Matrix with Toggle */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle>
{showResidualRisk
? "Residual Risk Matrix"
: "Initial Risk Matrix"}
</CardTitle>
<div className="flex items-center space-x-4 border-2 border-gray-300 rounded-lg p-3 bg-gray-50 shadow-md">
<Label
htmlFor="risk-toggle"
className={`text-sm font-semibold cursor-pointer ${
!showResidualRisk ? "text-primary" : "text-muted-foreground"
}`}
>
Initial
</Label>
<div className="relative">
<Switch
id="risk-toggle"
checked={showResidualRisk}
onCheckedChange={setShowResidualRisk}
className="border-2 border-gray-400 data-[state=checked]:border-primary"
/>
<CardContent className="pt-6">
<div className="flex flex-col w-full">
<div className="flex justify-between items-center mb-4">
<h3 className="text-base font-semibold">
{showResidualRisk ? "Residual risk" : "Current risk"}
</h3>
<div className="flex items-center space-x-4">
<Label
htmlFor="risk-toggle"
className={`text-sm font-semibold cursor-pointer ${
!showResidualRisk
? "text-primary"
: "text-muted-foreground"
}`}
>
Initial
</Label>
<Switch
id="risk-toggle"
checked={showResidualRisk}
onCheckedChange={setShowResidualRisk}
className="bg-white border-2 border-gray-300 data-[state=checked]:bg-white data-[state=checked]:border-gray-300 [&_span]:bg-gray-300"
/>
<Label
htmlFor="risk-toggle"
className={`text-sm font-semibold cursor-pointer ${
showResidualRisk
? "text-primary"
: "text-muted-foreground"
}`}
>
Residual
</Label>
</div>
</div>
<Label
htmlFor="risk-toggle"
className={`text-sm font-semibold cursor-pointer ${
showResidualRisk ? "text-primary" : "text-muted-foreground"
}`}
>
Residual
</Label>
<RiskMatrix risks={risks} isResidual={showResidualRisk} />
</div>
</CardHeader>
<CardContent>
<RiskMatrix risks={risks} isResidual={showResidualRisk} />
</CardContent>
</Card>
@@ -617,7 +648,7 @@ function ListRiskViewContent({
to={`/organizations/${organizationId}/risks/${risk.id}`}
className="block p-4 h-full w-full"
>
{floatToPercentage(
{riskScoreToSeverity(
risk.inherentLikelihood * risk.inherentImpact
)}
</Link>
@@ -628,7 +659,7 @@ function ListRiskViewContent({
className="block p-4 h-full w-full"
>
{risk.residualLikelihood && risk.residualImpact
? floatToPercentage(
? riskScoreToSeverity(
risk.residualLikelihood * risk.residualImpact
)
: "Not set"}
@@ -671,21 +702,6 @@ function ListRiskViewContent({
</CardContent>
</Card>
<LoadBelowButton
isLoading={isLoadingNext}
hasMore={hasNext}
onLoadMore={() => {
startTransition(() => {
setSearchParams((prev) => {
prev.set("after", pageInfo?.endCursor || "");
prev.delete("before");
return prev;
});
loadNext(defaultPageSize);
});
}}
/>
{/* Delete Confirmation Dialog */}
<Dialog
open={!!riskToDelete}
@@ -717,6 +733,21 @@ function ListRiskViewContent({
</DialogFooter>
</DialogContent>
</Dialog>
<LoadBelowButton
isLoading={isLoadingNext}
hasMore={hasNext}
onLoadMore={() => {
startTransition(() => {
setSearchParams((prev) => {
prev.set("after", pageInfo?.endCursor || "");
prev.delete("before");
return prev;
});
loadNext(defaultPageSize);
});
}}
/>
</div>
</PageTemplate>
);
@@ -740,7 +771,7 @@ export default function ListRiskView() {
last: before ? defaultPageSize : undefined,
before: before || undefined,
});
}, [loadQuery, organizationId]);
}, [loadQuery, organizationId, searchParams]);
if (!queryRef) {
return <RiskViewSkeleton />;

View File

@@ -33,7 +33,6 @@ import PeopleSelector from "@/components/PeopleSelector";
import { User } from "lucide-react";
import { Suspense } from "react";
import type { NewRiskViewQuery } from "./__generated__/NewRiskViewQuery.graphql";
import type { NewRiskViewCreateRiskMutation } from "./__generated__/NewRiskViewCreateRiskMutation.graphql";
interface RiskTemplate {
name: string;
@@ -89,11 +88,11 @@ function NewRiskForm({
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [inherentLikelihood, setinherentLikelihood] =
useState<string>("MEDIUM");
const [inherentImpact, setinherentImpact] = useState<string>("MEDIUM");
useState<string>("OCCASIONAL");
const [inherentImpact, setinherentImpact] = useState<string>("MODERATE");
const [residualLikelihood, setResidualLikelihood] =
useState<string>("MEDIUM");
const [residualImpact, setResidualImpact] = useState<string>("MEDIUM");
useState<string>("OCCASIONAL");
const [residualImpact, setResidualImpact] = useState<string>("MODERATE");
const [treatment, setTreatment] = useState<string>("MITIGATED");
const [ownerId, setOwnerId] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -125,15 +124,15 @@ function NewRiskForm({
// Map string values to float values
const likelihoodToFloat = (value: string): number => {
switch (value) {
case "VERY_LOW":
case "IMPROBABLE":
return 0.1;
case "LOW":
case "REMOTE":
return 0.3;
case "MEDIUM":
case "OCCASIONAL":
return 0.5;
case "HIGH":
case "PROBABLE":
return 0.7;
case "VERY_HIGH":
case "FREQUENT":
return 0.9;
default:
return 0.5;
@@ -142,15 +141,15 @@ function NewRiskForm({
const impactToFloat = (value: string): number => {
switch (value) {
case "VERY_LOW":
case "NEGLIGIBLE":
return 0.1;
case "LOW":
return 0.3;
case "MEDIUM":
case "MODERATE":
return 0.5;
case "HIGH":
case "SIGNIFICANT":
return 0.7;
case "VERY_HIGH":
case "CATASTROPHIC":
return 0.9;
default:
return 0.5;
@@ -165,10 +164,10 @@ function NewRiskForm({
// Clear form if "Select a template" is chosen
setName("");
setDescription("");
setinherentLikelihood("MEDIUM");
setinherentImpact("MEDIUM");
setResidualLikelihood("MEDIUM");
setResidualImpact("MEDIUM");
setinherentLikelihood("OCCASIONAL");
setinherentImpact("MODERATE");
setResidualLikelihood("OCCASIONAL");
setResidualImpact("MODERATE");
setTreatment("MITIGATED");
return;
}
@@ -197,20 +196,20 @@ function NewRiskForm({
// Helper function to convert float likelihood to string
const floatTolikelihood = (value: number): string => {
if (value <= 0.2) return "VERY_LOW";
if (value <= 0.4) return "LOW";
if (value <= 0.6) return "MEDIUM";
if (value <= 0.8) return "HIGH";
return "VERY_HIGH";
if (value <= 0.2) return "IMPROBABLE";
if (value <= 0.4) return "REMOTE";
if (value <= 0.6) return "OCCASIONAL";
if (value <= 0.8) return "PROBABLE";
return "FREQUENT";
};
// Helper function to convert float impact to string
const floatToImpact = (value: number): string => {
if (value <= 0.2) return "VERY_LOW";
if (value <= 0.2) return "NEGLIGIBLE";
if (value <= 0.4) return "LOW";
if (value <= 0.6) return "MEDIUM";
if (value <= 0.8) return "HIGH";
return "VERY_HIGH";
if (value <= 0.6) return "MODERATE";
if (value <= 0.8) return "SIGNIFICANT";
return "CATASTROPHIC";
};
const handleSubmit = (e: React.FormEvent) => {
@@ -361,15 +360,15 @@ function NewRiskForm({
value={inherentLikelihood}
onValueChange={setinherentLikelihood}
>
<SelectTrigger id="inherentLikelihood">
<SelectValue placeholder="Select inherentLikelihood" />
<SelectTrigger className="w-full">
<SelectValue placeholder="Select likelihood" />
</SelectTrigger>
<SelectContent className="max-h-[300px] overflow-y-auto">
<SelectItem value="VERY_LOW">Very Low</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MEDIUM">Medium</SelectItem>
<SelectItem value="HIGH">High</SelectItem>
<SelectItem value="VERY_HIGH">Very High</SelectItem>
<SelectContent>
<SelectItem value="IMPROBABLE">Improbable</SelectItem>
<SelectItem value="REMOTE">Remote</SelectItem>
<SelectItem value="OCCASIONAL">Occasional</SelectItem>
<SelectItem value="PROBABLE">Probable</SelectItem>
<SelectItem value="FREQUENT">Frequent</SelectItem>
</SelectContent>
</Select>
</div>
@@ -380,15 +379,15 @@ function NewRiskForm({
value={inherentImpact}
onValueChange={setinherentImpact}
>
<SelectTrigger id="inherentImpact">
<SelectValue placeholder="Select inherentImpact" />
<SelectTrigger className="w-full">
<SelectValue placeholder="Select impact" />
</SelectTrigger>
<SelectContent className="max-h-[300px] overflow-y-auto">
<SelectItem value="VERY_LOW">Very Low</SelectItem>
<SelectContent>
<SelectItem value="NEGLIGIBLE">Negligible</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MEDIUM">Medium</SelectItem>
<SelectItem value="HIGH">High</SelectItem>
<SelectItem value="VERY_HIGH">Very High</SelectItem>
<SelectItem value="MODERATE">Moderate</SelectItem>
<SelectItem value="SIGNIFICANT">Significant</SelectItem>
<SelectItem value="CATASTROPHIC">Catastrophic</SelectItem>
</SelectContent>
</Select>
</div>
@@ -429,15 +428,15 @@ function NewRiskForm({
value={residualLikelihood}
onValueChange={setResidualLikelihood}
>
<SelectTrigger id="residualLikelihood">
<SelectValue placeholder="Select residual likelihood" />
<SelectTrigger className="w-full">
<SelectValue placeholder="Select likelihood" />
</SelectTrigger>
<SelectContent className="max-h-[300px] overflow-y-auto">
<SelectItem value="VERY_LOW">Very Low</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MEDIUM">Medium</SelectItem>
<SelectItem value="HIGH">High</SelectItem>
<SelectItem value="VERY_HIGH">Very High</SelectItem>
<SelectContent>
<SelectItem value="IMPROBABLE">Improbable</SelectItem>
<SelectItem value="REMOTE">Remote</SelectItem>
<SelectItem value="OCCASIONAL">Occasional</SelectItem>
<SelectItem value="PROBABLE">Probable</SelectItem>
<SelectItem value="FREQUENT">Frequent</SelectItem>
</SelectContent>
</Select>
</div>
@@ -448,15 +447,15 @@ function NewRiskForm({
value={residualImpact}
onValueChange={setResidualImpact}
>
<SelectTrigger id="residualImpact">
<SelectValue placeholder="Select residual impact" />
<SelectTrigger className="w-full">
<SelectValue placeholder="Select impact" />
</SelectTrigger>
<SelectContent className="max-h-[300px] overflow-y-auto">
<SelectItem value="VERY_LOW">Very Low</SelectItem>
<SelectContent>
<SelectItem value="NEGLIGIBLE">Negligible</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MEDIUM">Medium</SelectItem>
<SelectItem value="HIGH">High</SelectItem>
<SelectItem value="VERY_HIGH">Very High</SelectItem>
<SelectItem value="MODERATE">Moderate</SelectItem>
<SelectItem value="SIGNIFICANT">Significant</SelectItem>
<SelectItem value="CATASTROPHIC">Catastrophic</SelectItem>
</SelectContent>
</Select>
</div>

View File

@@ -210,10 +210,30 @@ const deleteRiskPolicyMappingMutation = graphql`
function getRiskSeverity(likelihood: number, impact: number) {
const score = likelihood * impact;
if (score >= 0.75) return { level: "High", class: "bg-red-100 text-red-800" };
if (score >= 0.4)
return { level: "Medium", class: "bg-yellow-100 text-yellow-800" };
return { level: "Low", class: "bg-green-100 text-green-800" };
if (score >= 0.75)
return { level: "Catastrophic", class: "bg-red-100 text-red-800" };
if (score >= 0.5)
return { level: "Critical", class: "bg-orange-100 text-orange-800" };
if (score >= 0.25)
return { level: "Marginal", class: "bg-yellow-100 text-yellow-800" };
return { level: "Negligible", class: "bg-green-100 text-green-800" };
}
// Add helper functions to convert numerical values to labels
function getLikelihoodLabel(likelihood: number): string {
if (likelihood >= 0.8) return "Very High";
if (likelihood >= 0.6) return "High";
if (likelihood >= 0.4) return "Medium";
if (likelihood >= 0.2) return "Low";
return "Very Low";
}
function getImpactLabel(impact: number): string {
if (impact >= 0.8) return "Critical";
if (impact >= 0.6) return "Major";
if (impact >= 0.4) return "Moderate";
if (impact >= 0.2) return "Minor";
return "Insignificant";
}
function ShowRiskViewContent({
@@ -755,13 +775,13 @@ function ShowRiskViewContent({
Likelihood
</h3>
<p className="mt-1 text-lg">
{(risk.inherentLikelihood! * 100).toFixed(0)}%
{getLikelihoodLabel(risk.inherentLikelihood!)}
</p>
</div>
<div>
<h3 className="text-sm font-medium text-secondary">Impact</h3>
<p className="mt-1 text-lg">
{(risk.inherentImpact! * 100).toFixed(0)}%
{getImpactLabel(risk.inherentImpact!)}
</p>
</div>
<div>
@@ -830,16 +850,12 @@ function ShowRiskViewContent({
Residual Likelihood
</h3>
<p className="mt-1 text-lg">
{(risk.residualLikelihood! * 100).toFixed(0)}%
{getLikelihoodLabel(risk.residualLikelihood!)}
</p>
<p className="text-xs text-secondary mt-1">
{risk.residualLikelihood !== risk.inherentLikelihood
? `Reduced by ${(
(risk.inherentLikelihood! -
risk.residualLikelihood!) *
100
).toFixed(0)}%`
: "No reduction"}
? `Mitigated`
: "No mitigation"}
</p>
</div>
<div>
@@ -847,15 +863,12 @@ function ShowRiskViewContent({
Residual Impact
</h3>
<p className="mt-1 text-lg">
{(risk.residualImpact! * 100).toFixed(0)}%
{getImpactLabel(risk.residualImpact!)}
</p>
<p className="text-xs text-secondary mt-1">
{risk.residualImpact !== risk.inherentImpact
? `Reduced by ${(
(risk.inherentImpact! - risk.residualImpact!) *
100
).toFixed(0)}%`
: "No reduction"}
? `Mitigated`
: "No mitigation"}
</p>
</div>
<div>