@@ -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 { cn } from "@/lib/utils"
|
||||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
function Switch({
|
||||||
|
className,
|
||||||
const Switch = React.forwardRef<
|
...props
|
||||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
return (
|
||||||
>(({ className, ...props }, ref) => (
|
<SwitchPrimitive.Root
|
||||||
<SwitchPrimitives.Root
|
data-slot="switch"
|
||||||
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
|
|
||||||
className={cn(
|
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
|
||||||
)}
|
)}
|
||||||
/>
|
{...props}
|
||||||
</SwitchPrimitives.Root>
|
>
|
||||||
));
|
<SwitchPrimitive.Thumb
|
||||||
Switch.displayName = SwitchPrimitives.Root.displayName;
|
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 }
|
||||||
|
|||||||
@@ -105,11 +105,11 @@ function EditRiskViewContent({
|
|||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const [inherentLikelihood, setInherentLikelihood] =
|
const [inherentLikelihood, setInherentLikelihood] =
|
||||||
useState<string>("MEDIUM");
|
useState<string>("OCCASIONAL");
|
||||||
const [inherentImpact, setInherentImpact] = useState<string>("MEDIUM");
|
const [inherentImpact, setInherentImpact] = useState<string>("MODERATE");
|
||||||
const [residualLikelihood, setResidualLikelihood] =
|
const [residualLikelihood, setResidualLikelihood] =
|
||||||
useState<string>("MEDIUM");
|
useState<string>("OCCASIONAL");
|
||||||
const [residualImpact, setResidualImpact] = useState<string>("MEDIUM");
|
const [residualImpact, setResidualImpact] = useState<string>("MODERATE");
|
||||||
const [treatment, setTreatment] = useState<RiskTreatment>("MITIGATED");
|
const [treatment, setTreatment] = useState<RiskTreatment>("MITIGATED");
|
||||||
const [ownerId, setOwnerId] = useState<string | null>(null);
|
const [ownerId, setOwnerId] = useState<string | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
@@ -119,34 +119,34 @@ function EditRiskViewContent({
|
|||||||
|
|
||||||
// Helper function to convert float to likelihood string
|
// Helper function to convert float to likelihood string
|
||||||
const floatToLikelihood = (value: number): string => {
|
const floatToLikelihood = (value: number): string => {
|
||||||
if (value <= 0.2) return "VERY_LOW";
|
if (value <= 0.2) return "IMPROBABLE";
|
||||||
if (value <= 0.4) return "LOW";
|
if (value <= 0.4) return "REMOTE";
|
||||||
if (value <= 0.6) return "MEDIUM";
|
if (value <= 0.6) return "OCCASIONAL";
|
||||||
if (value <= 0.8) return "HIGH";
|
if (value <= 0.8) return "PROBABLE";
|
||||||
return "VERY_HIGH";
|
return "FREQUENT";
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to convert float to impact string
|
// Helper function to convert float to impact string
|
||||||
const floatToImpact = (value: number): 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.4) return "LOW";
|
||||||
if (value <= 0.6) return "MEDIUM";
|
if (value <= 0.6) return "MODERATE";
|
||||||
if (value <= 0.8) return "HIGH";
|
if (value <= 0.8) return "SIGNIFICANT";
|
||||||
return "VERY_HIGH";
|
return "CATASTROPHIC";
|
||||||
};
|
};
|
||||||
|
|
||||||
// Map string values to float values
|
// Map string values to float values
|
||||||
const likelihoodToFloat = (value: string): number => {
|
const likelihoodToFloat = (value: string): number => {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case "VERY_LOW":
|
case "IMPROBABLE":
|
||||||
return 0.1;
|
return 0.1;
|
||||||
case "LOW":
|
case "REMOTE":
|
||||||
return 0.3;
|
return 0.3;
|
||||||
case "MEDIUM":
|
case "OCCASIONAL":
|
||||||
return 0.5;
|
return 0.5;
|
||||||
case "HIGH":
|
case "PROBABLE":
|
||||||
return 0.7;
|
return 0.7;
|
||||||
case "VERY_HIGH":
|
case "FREQUENT":
|
||||||
return 0.9;
|
return 0.9;
|
||||||
default:
|
default:
|
||||||
return 0.5;
|
return 0.5;
|
||||||
@@ -155,15 +155,15 @@ function EditRiskViewContent({
|
|||||||
|
|
||||||
const impactToFloat = (value: string): number => {
|
const impactToFloat = (value: string): number => {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case "VERY_LOW":
|
case "NEGLIGIBLE":
|
||||||
return 0.1;
|
return 0.1;
|
||||||
case "LOW":
|
case "LOW":
|
||||||
return 0.3;
|
return 0.3;
|
||||||
case "MEDIUM":
|
case "MODERATE":
|
||||||
return 0.5;
|
return 0.5;
|
||||||
case "HIGH":
|
case "SIGNIFICANT":
|
||||||
return 0.7;
|
return 0.7;
|
||||||
case "VERY_HIGH":
|
case "CATASTROPHIC":
|
||||||
return 0.9;
|
return 0.9;
|
||||||
default:
|
default:
|
||||||
return 0.5;
|
return 0.5;
|
||||||
@@ -308,15 +308,15 @@ function EditRiskViewContent({
|
|||||||
value={inherentLikelihood}
|
value={inherentLikelihood}
|
||||||
onValueChange={setInherentLikelihood}
|
onValueChange={setInherentLikelihood}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="inherentLikelihood">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Select likelihood" />
|
<SelectValue placeholder="Select likelihood" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="max-h-[300px] overflow-y-auto">
|
<SelectContent>
|
||||||
<SelectItem value="VERY_LOW">Very Low</SelectItem>
|
<SelectItem value="IMPROBABLE">Improbable</SelectItem>
|
||||||
<SelectItem value="LOW">Low</SelectItem>
|
<SelectItem value="REMOTE">Remote</SelectItem>
|
||||||
<SelectItem value="MEDIUM">Medium</SelectItem>
|
<SelectItem value="OCCASIONAL">Occasional</SelectItem>
|
||||||
<SelectItem value="HIGH">High</SelectItem>
|
<SelectItem value="PROBABLE">Probable</SelectItem>
|
||||||
<SelectItem value="VERY_HIGH">Very High</SelectItem>
|
<SelectItem value="FREQUENT">Frequent</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -327,15 +327,15 @@ function EditRiskViewContent({
|
|||||||
value={inherentImpact}
|
value={inherentImpact}
|
||||||
onValueChange={setInherentImpact}
|
onValueChange={setInherentImpact}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="inherentImpact">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Select impact" />
|
<SelectValue placeholder="Select impact" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="max-h-[300px] overflow-y-auto">
|
<SelectContent>
|
||||||
<SelectItem value="VERY_LOW">Very Low</SelectItem>
|
<SelectItem value="NEGLIGIBLE">Negligible</SelectItem>
|
||||||
<SelectItem value="LOW">Low</SelectItem>
|
<SelectItem value="LOW">Low</SelectItem>
|
||||||
<SelectItem value="MEDIUM">Medium</SelectItem>
|
<SelectItem value="MODERATE">Moderate</SelectItem>
|
||||||
<SelectItem value="HIGH">High</SelectItem>
|
<SelectItem value="SIGNIFICANT">Significant</SelectItem>
|
||||||
<SelectItem value="VERY_HIGH">Very High</SelectItem>
|
<SelectItem value="CATASTROPHIC">Catastrophic</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -382,15 +382,15 @@ function EditRiskViewContent({
|
|||||||
value={residualLikelihood}
|
value={residualLikelihood}
|
||||||
onValueChange={setResidualLikelihood}
|
onValueChange={setResidualLikelihood}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="residualLikelihood">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Select residual likelihood" />
|
<SelectValue placeholder="Select likelihood" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="max-h-[300px] overflow-y-auto">
|
<SelectContent>
|
||||||
<SelectItem value="VERY_LOW">Very Low</SelectItem>
|
<SelectItem value="IMPROBABLE">Improbable</SelectItem>
|
||||||
<SelectItem value="LOW">Low</SelectItem>
|
<SelectItem value="REMOTE">Remote</SelectItem>
|
||||||
<SelectItem value="MEDIUM">Medium</SelectItem>
|
<SelectItem value="OCCASIONAL">Occasional</SelectItem>
|
||||||
<SelectItem value="HIGH">High</SelectItem>
|
<SelectItem value="PROBABLE">Probable</SelectItem>
|
||||||
<SelectItem value="VERY_HIGH">Very High</SelectItem>
|
<SelectItem value="FREQUENT">Frequent</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -401,15 +401,15 @@ function EditRiskViewContent({
|
|||||||
value={residualImpact}
|
value={residualImpact}
|
||||||
onValueChange={setResidualImpact}
|
onValueChange={setResidualImpact}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="residualImpact">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Select residual impact" />
|
<SelectValue placeholder="Select impact" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="max-h-[300px] overflow-y-auto">
|
<SelectContent>
|
||||||
<SelectItem value="VERY_LOW">Very Low</SelectItem>
|
<SelectItem value="NEGLIGIBLE">Negligible</SelectItem>
|
||||||
<SelectItem value="LOW">Low</SelectItem>
|
<SelectItem value="LOW">Low</SelectItem>
|
||||||
<SelectItem value="MEDIUM">Medium</SelectItem>
|
<SelectItem value="MODERATE">Moderate</SelectItem>
|
||||||
<SelectItem value="HIGH">High</SelectItem>
|
<SelectItem value="SIGNIFICANT">Significant</SelectItem>
|
||||||
<SelectItem value="VERY_HIGH">Very High</SelectItem>
|
<SelectItem value="CATASTROPHIC">Catastrophic</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { RiskViewSkeleton } from "./ListRiskPage";
|
|||||||
import { ListRiskViewPaginationQuery } from "./__generated__/ListRiskViewPaginationQuery.graphql";
|
import { ListRiskViewPaginationQuery } from "./__generated__/ListRiskViewPaginationQuery.graphql";
|
||||||
import { ListRiskView_risks$key } from "./__generated__/ListRiskView_risks.graphql";
|
import { ListRiskView_risks$key } from "./__generated__/ListRiskView_risks.graphql";
|
||||||
import { Button } from "@/components/ui/button";
|
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 { Link } from "react-router";
|
||||||
import { Plus, Trash2, Edit } from "lucide-react";
|
import { Plus, Trash2, Edit } from "lucide-react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
@@ -110,10 +110,19 @@ const deleteRiskMutation = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// Helper function to convert float values to percentage
|
// Helper function to convert float values to percentage
|
||||||
|
// This is kept for potential future use, like tooltips
|
||||||
const floatToPercentage = (value: number): string => {
|
const floatToPercentage = (value: number): string => {
|
||||||
return `${Math.round(value * 100)}%`;
|
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
|
// Helper function to format treatment value
|
||||||
const formatTreatment = (treatment: string): string => {
|
const formatTreatment = (treatment: string): string => {
|
||||||
const treatmentMap: Record<string, string> = {
|
const treatmentMap: Record<string, string> = {
|
||||||
@@ -182,20 +191,16 @@ function LoadBelowButton({
|
|||||||
|
|
||||||
// Define colors for risk matrix cells
|
// Define colors for risk matrix cells
|
||||||
const riskMatrixColors = {
|
const riskMatrixColors = {
|
||||||
lowest: "bg-green-500 text-white",
|
low: "bg-green-500 text-white",
|
||||||
low: "bg-lime-300 text-black",
|
|
||||||
medium: "bg-yellow-300 text-black",
|
medium: "bg-yellow-300 text-black",
|
||||||
high: "bg-amber-400 text-white",
|
high: "bg-red-500 text-white",
|
||||||
highest: "bg-red-500 text-white",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Empty cell variants (lighter colors)
|
// Empty cell variants (lighter colors)
|
||||||
const emptyRiskMatrixColors = {
|
const emptyRiskMatrixColors = {
|
||||||
lowest: "bg-green-50 text-black",
|
low: "bg-green-50 text-black",
|
||||||
low: "bg-lime-50 text-black",
|
|
||||||
medium: "bg-yellow-50 text-black",
|
medium: "bg-yellow-50 text-black",
|
||||||
high: "bg-amber-50 text-black",
|
high: "bg-red-50 text-black",
|
||||||
highest: "bg-red-50 text-black",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Risk Matrix Component
|
// Risk Matrix Component
|
||||||
@@ -213,31 +218,44 @@ function RiskMatrix({
|
|||||||
}>;
|
}>;
|
||||||
isResidual?: boolean;
|
isResidual?: boolean;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
// Define likelihood and impact ranges for the 5x5 matrix
|
// Define impact ranges for the vertical axis (rows)
|
||||||
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
|
|
||||||
];
|
|
||||||
|
|
||||||
const impactRanges: [number, number][] = [
|
const impactRanges: [number, number][] = [
|
||||||
[0, 0.2], // Lowest impact
|
[0.8, 1], // Highest impact - top row
|
||||||
[0.2, 0.4], // Low impact
|
|
||||||
[0.4, 0.6], // Medium impact
|
|
||||||
[0.6, 0.8], // High impact
|
[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
|
// Function to get cell content with risks that fall in this cell
|
||||||
const getCellContent = (
|
const getCellContent = (
|
||||||
likelihoodRange: [number, number],
|
impactRange: [number, number],
|
||||||
impactRange: [number, number]
|
likelihoodRange: [number, number]
|
||||||
) => {
|
) => {
|
||||||
return risks.filter((risk) => {
|
return risks.filter((risk) => {
|
||||||
const likelihood = isResidual
|
const likelihood = isResidual
|
||||||
@@ -257,48 +275,41 @@ function RiskMatrix({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Helper to determine cell color based on position in matrix
|
// 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)
|
// Matrix has rows indexed from top to bottom (0 = highest impact, 4 = lowest impact)
|
||||||
// and columns indexed from left to right (0 = lowest impact, 4 = highest impact)
|
// and columns indexed from left to right (0 = lowest likelihood, 4 = highest likelihood)
|
||||||
const getCellColor = (row: number, col: number, isEmpty: boolean): string => {
|
const getCellColor = (row: number, col: number, isEmpty: boolean): string => {
|
||||||
const colorSet = isEmpty ? emptyRiskMatrixColors : riskMatrixColors;
|
const colorSet = isEmpty ? emptyRiskMatrixColors : riskMatrixColors;
|
||||||
|
|
||||||
// Top row (highest likelihood)
|
// Top row (highest impact - Catastrophic)
|
||||||
if (row === 0) {
|
if (row === 0) {
|
||||||
if (col === 0) return colorSet.lowest;
|
if (col <= 1) return colorSet.medium; // Yellow for first two cells
|
||||||
if (col === 1) return colorSet.high;
|
return colorSet.high; // Red for the rest
|
||||||
return colorSet.highest;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second row
|
// Second row (Significant impact)
|
||||||
if (row === 1) {
|
if (row === 1) {
|
||||||
if (col === 0) return colorSet.lowest;
|
if (col === 0) return colorSet.low; // Green for first cell
|
||||||
if (col === 1) return colorSet.medium;
|
if (col <= 2) return colorSet.medium; // Yellow for next two
|
||||||
if (col === 2) return colorSet.high;
|
return colorSet.high; // Red for the rest
|
||||||
return colorSet.highest;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Middle row
|
// Middle row (Moderate impact)
|
||||||
if (row === 2) {
|
if (row === 2) {
|
||||||
if (col === 0) return colorSet.lowest;
|
if (col === 0) return colorSet.low; // Green for first cell
|
||||||
if (col === 1) return colorSet.low;
|
if (col <= 3) return colorSet.medium; // Yellow for next three
|
||||||
if (col === 2) return colorSet.medium;
|
return colorSet.high; // Red for last cell
|
||||||
if (col === 3) return colorSet.high;
|
|
||||||
return colorSet.highest;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fourth row
|
// Fourth row (Low impact)
|
||||||
if (row === 3) {
|
if (row === 3) {
|
||||||
if (col === 0) return colorSet.lowest;
|
if (col <= 1) return colorSet.low; // Green for first two
|
||||||
if (col === 1) return colorSet.low;
|
return colorSet.medium; // Yellow for the rest
|
||||||
if (col === 2 || col === 3) return colorSet.medium;
|
|
||||||
return colorSet.high;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bottom row (lowest likelihood)
|
// Bottom row (lowest impact - Negligible)
|
||||||
if (row === 4) {
|
if (row === 4) {
|
||||||
if (col <= 1) return colorSet.lowest;
|
if (col <= 3) return colorSet.low; // Green for first four
|
||||||
if (col <= 3) return colorSet.low;
|
return colorSet.medium; // Yellow for last cell
|
||||||
return colorSet.medium;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "bg-gray-100";
|
return "bg-gray-100";
|
||||||
@@ -308,15 +319,15 @@ function RiskMatrix({
|
|||||||
const RiskCell = ({
|
const RiskCell = ({
|
||||||
rowIndex,
|
rowIndex,
|
||||||
colIndex,
|
colIndex,
|
||||||
likelihoodRange,
|
|
||||||
impactRange,
|
impactRange,
|
||||||
|
likelihoodRange,
|
||||||
}: {
|
}: {
|
||||||
rowIndex: number;
|
rowIndex: number;
|
||||||
colIndex: number;
|
colIndex: number;
|
||||||
likelihoodRange: [number, number];
|
|
||||||
impactRange: [number, number];
|
impactRange: [number, number];
|
||||||
|
likelihoodRange: [number, number];
|
||||||
}) => {
|
}) => {
|
||||||
const cellRisks = getCellContent(likelihoodRange, impactRange);
|
const cellRisks = getCellContent(impactRange, likelihoodRange);
|
||||||
const isEmpty = cellRisks.length === 0;
|
const isEmpty = cellRisks.length === 0;
|
||||||
const cellColor = getCellColor(rowIndex, colIndex, isEmpty);
|
const cellColor = getCellColor(rowIndex, colIndex, isEmpty);
|
||||||
|
|
||||||
@@ -360,43 +371,61 @@ function RiskMatrix({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="space-y-2">
|
||||||
<div className="flex flex-col">
|
<div className="overflow-x-auto">
|
||||||
<div className="text-xs font-medium text-center mb-1">CONSEQUENCE</div>
|
<div className="flex flex-col">
|
||||||
<table className="w-full border-collapse table-fixed">
|
<div className="flex">
|
||||||
<thead>
|
<div
|
||||||
<tr>
|
className="text-xs font-semibold flex items-center justify-center mr-2"
|
||||||
<th className="p-1 text-center border w-14"></th>
|
style={{
|
||||||
{impactLabels.map((label, index) => (
|
writingMode: "vertical-rl",
|
||||||
<th
|
transform: "rotate(180deg)",
|
||||||
key={index}
|
alignSelf: "center",
|
||||||
className="p-1 text-xs text-center border font-medium w-14"
|
}}
|
||||||
>
|
>
|
||||||
{label}
|
Impact
|
||||||
</th>
|
</div>
|
||||||
))}
|
<table className="w-full border-collapse table-fixed">
|
||||||
</tr>
|
<tbody>
|
||||||
</thead>
|
{impactRanges.map((impactRange, rowIndex) => (
|
||||||
<tbody>
|
<tr key={rowIndex}>
|
||||||
{likelihoodRanges.map((likelihoodRange, rowIndex) => (
|
<th className="p-1 text-xs text-center border-0 font-medium w-14 h-14">
|
||||||
<tr key={rowIndex}>
|
{impactLabels[rowIndex]}
|
||||||
<th className="p-1 text-xs text-center border font-medium w-14 h-14">
|
</th>
|
||||||
{likelihoodLabels[rowIndex]}
|
{likelihoodRanges.map((likelihoodRange, colIndex) => (
|
||||||
</th>
|
<RiskCell
|
||||||
{impactRanges.map((impactRange, colIndex) => (
|
key={colIndex}
|
||||||
<RiskCell
|
rowIndex={rowIndex}
|
||||||
key={colIndex}
|
colIndex={colIndex}
|
||||||
rowIndex={rowIndex}
|
impactRange={impactRange}
|
||||||
colIndex={colIndex}
|
likelihoodRange={likelihoodRange}
|
||||||
likelihoodRange={likelihoodRange}
|
/>
|
||||||
impactRange={impactRange}
|
))}
|
||||||
/>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tbody>
|
||||||
))}
|
<tfoot>
|
||||||
</tbody>
|
<tr>
|
||||||
</table>
|
<th className="p-1 text-center border-0 w-14"></th>
|
||||||
<div className="text-xs font-medium ml-2 mt-1">LIKELIHOOD</div>
|
{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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -519,41 +548,43 @@ function ListRiskViewContent({
|
|||||||
|
|
||||||
{/* Combined Risk Matrix with Toggle */}
|
{/* Combined Risk Matrix with Toggle */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardContent className="pt-6">
|
||||||
<CardTitle>
|
<div className="flex flex-col w-full">
|
||||||
{showResidualRisk
|
<div className="flex justify-between items-center mb-4">
|
||||||
? "Residual Risk Matrix"
|
<h3 className="text-base font-semibold">
|
||||||
: "Initial Risk Matrix"}
|
{showResidualRisk ? "Residual risk" : "Current risk"}
|
||||||
</CardTitle>
|
</h3>
|
||||||
<div className="flex items-center space-x-4 border-2 border-gray-300 rounded-lg p-3 bg-gray-50 shadow-md">
|
<div className="flex items-center space-x-4">
|
||||||
<Label
|
<Label
|
||||||
htmlFor="risk-toggle"
|
htmlFor="risk-toggle"
|
||||||
className={`text-sm font-semibold cursor-pointer ${
|
className={`text-sm font-semibold cursor-pointer ${
|
||||||
!showResidualRisk ? "text-primary" : "text-muted-foreground"
|
!showResidualRisk
|
||||||
}`}
|
? "text-primary"
|
||||||
>
|
: "text-muted-foreground"
|
||||||
Initial
|
}`}
|
||||||
</Label>
|
>
|
||||||
<div className="relative">
|
Initial
|
||||||
<Switch
|
</Label>
|
||||||
id="risk-toggle"
|
<Switch
|
||||||
checked={showResidualRisk}
|
id="risk-toggle"
|
||||||
onCheckedChange={setShowResidualRisk}
|
checked={showResidualRisk}
|
||||||
className="border-2 border-gray-400 data-[state=checked]:border-primary"
|
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>
|
</div>
|
||||||
<Label
|
<RiskMatrix risks={risks} isResidual={showResidualRisk} />
|
||||||
htmlFor="risk-toggle"
|
|
||||||
className={`text-sm font-semibold cursor-pointer ${
|
|
||||||
showResidualRisk ? "text-primary" : "text-muted-foreground"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Residual
|
|
||||||
</Label>
|
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<RiskMatrix risks={risks} isResidual={showResidualRisk} />
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -617,7 +648,7 @@ function ListRiskViewContent({
|
|||||||
to={`/organizations/${organizationId}/risks/${risk.id}`}
|
to={`/organizations/${organizationId}/risks/${risk.id}`}
|
||||||
className="block p-4 h-full w-full"
|
className="block p-4 h-full w-full"
|
||||||
>
|
>
|
||||||
{floatToPercentage(
|
{riskScoreToSeverity(
|
||||||
risk.inherentLikelihood * risk.inherentImpact
|
risk.inherentLikelihood * risk.inherentImpact
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -628,7 +659,7 @@ function ListRiskViewContent({
|
|||||||
className="block p-4 h-full w-full"
|
className="block p-4 h-full w-full"
|
||||||
>
|
>
|
||||||
{risk.residualLikelihood && risk.residualImpact
|
{risk.residualLikelihood && risk.residualImpact
|
||||||
? floatToPercentage(
|
? riskScoreToSeverity(
|
||||||
risk.residualLikelihood * risk.residualImpact
|
risk.residualLikelihood * risk.residualImpact
|
||||||
)
|
)
|
||||||
: "Not set"}
|
: "Not set"}
|
||||||
@@ -671,21 +702,6 @@ function ListRiskViewContent({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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 */}
|
{/* Delete Confirmation Dialog */}
|
||||||
<Dialog
|
<Dialog
|
||||||
open={!!riskToDelete}
|
open={!!riskToDelete}
|
||||||
@@ -717,6 +733,21 @@ function ListRiskViewContent({
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<LoadBelowButton
|
||||||
|
isLoading={isLoadingNext}
|
||||||
|
hasMore={hasNext}
|
||||||
|
onLoadMore={() => {
|
||||||
|
startTransition(() => {
|
||||||
|
setSearchParams((prev) => {
|
||||||
|
prev.set("after", pageInfo?.endCursor || "");
|
||||||
|
prev.delete("before");
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
loadNext(defaultPageSize);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</PageTemplate>
|
</PageTemplate>
|
||||||
);
|
);
|
||||||
@@ -740,7 +771,7 @@ export default function ListRiskView() {
|
|||||||
last: before ? defaultPageSize : undefined,
|
last: before ? defaultPageSize : undefined,
|
||||||
before: before || undefined,
|
before: before || undefined,
|
||||||
});
|
});
|
||||||
}, [loadQuery, organizationId]);
|
}, [loadQuery, organizationId, searchParams]);
|
||||||
|
|
||||||
if (!queryRef) {
|
if (!queryRef) {
|
||||||
return <RiskViewSkeleton />;
|
return <RiskViewSkeleton />;
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import PeopleSelector from "@/components/PeopleSelector";
|
|||||||
import { User } from "lucide-react";
|
import { User } from "lucide-react";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import type { NewRiskViewQuery } from "./__generated__/NewRiskViewQuery.graphql";
|
import type { NewRiskViewQuery } from "./__generated__/NewRiskViewQuery.graphql";
|
||||||
import type { NewRiskViewCreateRiskMutation } from "./__generated__/NewRiskViewCreateRiskMutation.graphql";
|
|
||||||
|
|
||||||
interface RiskTemplate {
|
interface RiskTemplate {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -89,11 +88,11 @@ function NewRiskForm({
|
|||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const [inherentLikelihood, setinherentLikelihood] =
|
const [inherentLikelihood, setinherentLikelihood] =
|
||||||
useState<string>("MEDIUM");
|
useState<string>("OCCASIONAL");
|
||||||
const [inherentImpact, setinherentImpact] = useState<string>("MEDIUM");
|
const [inherentImpact, setinherentImpact] = useState<string>("MODERATE");
|
||||||
const [residualLikelihood, setResidualLikelihood] =
|
const [residualLikelihood, setResidualLikelihood] =
|
||||||
useState<string>("MEDIUM");
|
useState<string>("OCCASIONAL");
|
||||||
const [residualImpact, setResidualImpact] = useState<string>("MEDIUM");
|
const [residualImpact, setResidualImpact] = useState<string>("MODERATE");
|
||||||
const [treatment, setTreatment] = useState<string>("MITIGATED");
|
const [treatment, setTreatment] = useState<string>("MITIGATED");
|
||||||
const [ownerId, setOwnerId] = useState<string | null>(null);
|
const [ownerId, setOwnerId] = useState<string | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
@@ -125,15 +124,15 @@ function NewRiskForm({
|
|||||||
// Map string values to float values
|
// Map string values to float values
|
||||||
const likelihoodToFloat = (value: string): number => {
|
const likelihoodToFloat = (value: string): number => {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case "VERY_LOW":
|
case "IMPROBABLE":
|
||||||
return 0.1;
|
return 0.1;
|
||||||
case "LOW":
|
case "REMOTE":
|
||||||
return 0.3;
|
return 0.3;
|
||||||
case "MEDIUM":
|
case "OCCASIONAL":
|
||||||
return 0.5;
|
return 0.5;
|
||||||
case "HIGH":
|
case "PROBABLE":
|
||||||
return 0.7;
|
return 0.7;
|
||||||
case "VERY_HIGH":
|
case "FREQUENT":
|
||||||
return 0.9;
|
return 0.9;
|
||||||
default:
|
default:
|
||||||
return 0.5;
|
return 0.5;
|
||||||
@@ -142,15 +141,15 @@ function NewRiskForm({
|
|||||||
|
|
||||||
const impactToFloat = (value: string): number => {
|
const impactToFloat = (value: string): number => {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case "VERY_LOW":
|
case "NEGLIGIBLE":
|
||||||
return 0.1;
|
return 0.1;
|
||||||
case "LOW":
|
case "LOW":
|
||||||
return 0.3;
|
return 0.3;
|
||||||
case "MEDIUM":
|
case "MODERATE":
|
||||||
return 0.5;
|
return 0.5;
|
||||||
case "HIGH":
|
case "SIGNIFICANT":
|
||||||
return 0.7;
|
return 0.7;
|
||||||
case "VERY_HIGH":
|
case "CATASTROPHIC":
|
||||||
return 0.9;
|
return 0.9;
|
||||||
default:
|
default:
|
||||||
return 0.5;
|
return 0.5;
|
||||||
@@ -165,10 +164,10 @@ function NewRiskForm({
|
|||||||
// Clear form if "Select a template" is chosen
|
// Clear form if "Select a template" is chosen
|
||||||
setName("");
|
setName("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
setinherentLikelihood("MEDIUM");
|
setinherentLikelihood("OCCASIONAL");
|
||||||
setinherentImpact("MEDIUM");
|
setinherentImpact("MODERATE");
|
||||||
setResidualLikelihood("MEDIUM");
|
setResidualLikelihood("OCCASIONAL");
|
||||||
setResidualImpact("MEDIUM");
|
setResidualImpact("MODERATE");
|
||||||
setTreatment("MITIGATED");
|
setTreatment("MITIGATED");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -197,20 +196,20 @@ function NewRiskForm({
|
|||||||
|
|
||||||
// Helper function to convert float likelihood to string
|
// Helper function to convert float likelihood to string
|
||||||
const floatTolikelihood = (value: number): string => {
|
const floatTolikelihood = (value: number): string => {
|
||||||
if (value <= 0.2) return "VERY_LOW";
|
if (value <= 0.2) return "IMPROBABLE";
|
||||||
if (value <= 0.4) return "LOW";
|
if (value <= 0.4) return "REMOTE";
|
||||||
if (value <= 0.6) return "MEDIUM";
|
if (value <= 0.6) return "OCCASIONAL";
|
||||||
if (value <= 0.8) return "HIGH";
|
if (value <= 0.8) return "PROBABLE";
|
||||||
return "VERY_HIGH";
|
return "FREQUENT";
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to convert float impact to string
|
// Helper function to convert float impact to string
|
||||||
const floatToImpact = (value: number): 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.4) return "LOW";
|
||||||
if (value <= 0.6) return "MEDIUM";
|
if (value <= 0.6) return "MODERATE";
|
||||||
if (value <= 0.8) return "HIGH";
|
if (value <= 0.8) return "SIGNIFICANT";
|
||||||
return "VERY_HIGH";
|
return "CATASTROPHIC";
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
@@ -361,15 +360,15 @@ function NewRiskForm({
|
|||||||
value={inherentLikelihood}
|
value={inherentLikelihood}
|
||||||
onValueChange={setinherentLikelihood}
|
onValueChange={setinherentLikelihood}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="inherentLikelihood">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Select inherentLikelihood" />
|
<SelectValue placeholder="Select likelihood" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="max-h-[300px] overflow-y-auto">
|
<SelectContent>
|
||||||
<SelectItem value="VERY_LOW">Very Low</SelectItem>
|
<SelectItem value="IMPROBABLE">Improbable</SelectItem>
|
||||||
<SelectItem value="LOW">Low</SelectItem>
|
<SelectItem value="REMOTE">Remote</SelectItem>
|
||||||
<SelectItem value="MEDIUM">Medium</SelectItem>
|
<SelectItem value="OCCASIONAL">Occasional</SelectItem>
|
||||||
<SelectItem value="HIGH">High</SelectItem>
|
<SelectItem value="PROBABLE">Probable</SelectItem>
|
||||||
<SelectItem value="VERY_HIGH">Very High</SelectItem>
|
<SelectItem value="FREQUENT">Frequent</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -380,15 +379,15 @@ function NewRiskForm({
|
|||||||
value={inherentImpact}
|
value={inherentImpact}
|
||||||
onValueChange={setinherentImpact}
|
onValueChange={setinherentImpact}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="inherentImpact">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Select inherentImpact" />
|
<SelectValue placeholder="Select impact" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="max-h-[300px] overflow-y-auto">
|
<SelectContent>
|
||||||
<SelectItem value="VERY_LOW">Very Low</SelectItem>
|
<SelectItem value="NEGLIGIBLE">Negligible</SelectItem>
|
||||||
<SelectItem value="LOW">Low</SelectItem>
|
<SelectItem value="LOW">Low</SelectItem>
|
||||||
<SelectItem value="MEDIUM">Medium</SelectItem>
|
<SelectItem value="MODERATE">Moderate</SelectItem>
|
||||||
<SelectItem value="HIGH">High</SelectItem>
|
<SelectItem value="SIGNIFICANT">Significant</SelectItem>
|
||||||
<SelectItem value="VERY_HIGH">Very High</SelectItem>
|
<SelectItem value="CATASTROPHIC">Catastrophic</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -429,15 +428,15 @@ function NewRiskForm({
|
|||||||
value={residualLikelihood}
|
value={residualLikelihood}
|
||||||
onValueChange={setResidualLikelihood}
|
onValueChange={setResidualLikelihood}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="residualLikelihood">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Select residual likelihood" />
|
<SelectValue placeholder="Select likelihood" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="max-h-[300px] overflow-y-auto">
|
<SelectContent>
|
||||||
<SelectItem value="VERY_LOW">Very Low</SelectItem>
|
<SelectItem value="IMPROBABLE">Improbable</SelectItem>
|
||||||
<SelectItem value="LOW">Low</SelectItem>
|
<SelectItem value="REMOTE">Remote</SelectItem>
|
||||||
<SelectItem value="MEDIUM">Medium</SelectItem>
|
<SelectItem value="OCCASIONAL">Occasional</SelectItem>
|
||||||
<SelectItem value="HIGH">High</SelectItem>
|
<SelectItem value="PROBABLE">Probable</SelectItem>
|
||||||
<SelectItem value="VERY_HIGH">Very High</SelectItem>
|
<SelectItem value="FREQUENT">Frequent</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -448,15 +447,15 @@ function NewRiskForm({
|
|||||||
value={residualImpact}
|
value={residualImpact}
|
||||||
onValueChange={setResidualImpact}
|
onValueChange={setResidualImpact}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="residualImpact">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Select residual impact" />
|
<SelectValue placeholder="Select impact" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="max-h-[300px] overflow-y-auto">
|
<SelectContent>
|
||||||
<SelectItem value="VERY_LOW">Very Low</SelectItem>
|
<SelectItem value="NEGLIGIBLE">Negligible</SelectItem>
|
||||||
<SelectItem value="LOW">Low</SelectItem>
|
<SelectItem value="LOW">Low</SelectItem>
|
||||||
<SelectItem value="MEDIUM">Medium</SelectItem>
|
<SelectItem value="MODERATE">Moderate</SelectItem>
|
||||||
<SelectItem value="HIGH">High</SelectItem>
|
<SelectItem value="SIGNIFICANT">Significant</SelectItem>
|
||||||
<SelectItem value="VERY_HIGH">Very High</SelectItem>
|
<SelectItem value="CATASTROPHIC">Catastrophic</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -210,10 +210,30 @@ const deleteRiskPolicyMappingMutation = graphql`
|
|||||||
|
|
||||||
function getRiskSeverity(likelihood: number, impact: number) {
|
function getRiskSeverity(likelihood: number, impact: number) {
|
||||||
const score = likelihood * impact;
|
const score = likelihood * impact;
|
||||||
if (score >= 0.75) return { level: "High", class: "bg-red-100 text-red-800" };
|
if (score >= 0.75)
|
||||||
if (score >= 0.4)
|
return { level: "Catastrophic", class: "bg-red-100 text-red-800" };
|
||||||
return { level: "Medium", class: "bg-yellow-100 text-yellow-800" };
|
if (score >= 0.5)
|
||||||
return { level: "Low", class: "bg-green-100 text-green-800" };
|
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({
|
function ShowRiskViewContent({
|
||||||
@@ -755,13 +775,13 @@ function ShowRiskViewContent({
|
|||||||
Likelihood
|
Likelihood
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-1 text-lg">
|
<p className="mt-1 text-lg">
|
||||||
{(risk.inherentLikelihood! * 100).toFixed(0)}%
|
{getLikelihoodLabel(risk.inherentLikelihood!)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-medium text-secondary">Impact</h3>
|
<h3 className="text-sm font-medium text-secondary">Impact</h3>
|
||||||
<p className="mt-1 text-lg">
|
<p className="mt-1 text-lg">
|
||||||
{(risk.inherentImpact! * 100).toFixed(0)}%
|
{getImpactLabel(risk.inherentImpact!)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -830,16 +850,12 @@ function ShowRiskViewContent({
|
|||||||
Residual Likelihood
|
Residual Likelihood
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-1 text-lg">
|
<p className="mt-1 text-lg">
|
||||||
{(risk.residualLikelihood! * 100).toFixed(0)}%
|
{getLikelihoodLabel(risk.residualLikelihood!)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-secondary mt-1">
|
<p className="text-xs text-secondary mt-1">
|
||||||
{risk.residualLikelihood !== risk.inherentLikelihood
|
{risk.residualLikelihood !== risk.inherentLikelihood
|
||||||
? `Reduced by ${(
|
? `Mitigated`
|
||||||
(risk.inherentLikelihood! -
|
: "No mitigation"}
|
||||||
risk.residualLikelihood!) *
|
|
||||||
100
|
|
||||||
).toFixed(0)}%`
|
|
||||||
: "No reduction"}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -847,15 +863,12 @@ function ShowRiskViewContent({
|
|||||||
Residual Impact
|
Residual Impact
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-1 text-lg">
|
<p className="mt-1 text-lg">
|
||||||
{(risk.residualImpact! * 100).toFixed(0)}%
|
{getImpactLabel(risk.residualImpact!)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-secondary mt-1">
|
<p className="text-xs text-secondary mt-1">
|
||||||
{risk.residualImpact !== risk.inherentImpact
|
{risk.residualImpact !== risk.inherentImpact
|
||||||
? `Reduced by ${(
|
? `Mitigated`
|
||||||
(risk.inherentImpact! - risk.residualImpact!) *
|
: "No mitigation"}
|
||||||
100
|
|
||||||
).toFixed(0)}%`
|
|
||||||
: "No reduction"}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user