first commit
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
Label,
|
||||
} from "ikoncomponents";
|
||||
import { Separator } from "ikoncomponents";
|
||||
import { Textarea } from "ikoncomponents";
|
||||
import { Input } from "ikoncomponents";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "ikoncomponents";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "ikoncomponents";
|
||||
|
||||
import RiskWorkflow, { RiskStatus } from "../riskWorkflow";
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import z from "zod";
|
||||
import { editRiskSchema } from "./schema";
|
||||
import { getProjectByIdentifierApi } from "@/app/utils/api/projectApi";
|
||||
import { RiskData } from "@/app/utils/interface/risk";
|
||||
import { useAppCache } from "@/app/utils/context/AppCacheContext";
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
interface EditRiskModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
riskData: RiskData;
|
||||
onSave: (updatedData: RiskData) => void;
|
||||
}
|
||||
|
||||
function normalizeStatus(raw: string = ""): RiskStatus {
|
||||
const map: Record<string, RiskStatus> = {
|
||||
identifier: "identified",
|
||||
identified: "identified",
|
||||
mitigation_planned: "mitigation_planned",
|
||||
mitigationplanned: "mitigation_planned",
|
||||
monitoring: "monitoring",
|
||||
closed: "closed",
|
||||
};
|
||||
return map[raw.toLowerCase().replace(/[\s-]/g, "_")] ?? "identified";
|
||||
}
|
||||
|
||||
export default function EditRiskModal({
|
||||
open,
|
||||
onClose,
|
||||
riskData,
|
||||
onSave,
|
||||
}: EditRiskModalProps) {
|
||||
const params = useParams();
|
||||
const { userNameMap } = useAppCache();
|
||||
|
||||
const [assigneeOptions, setAssigneeOptions] = useState<string[]>([]);
|
||||
const [projectIdentifier, setProjectIdentifier] = useState(
|
||||
params.projectIdentifier as string,
|
||||
);
|
||||
console.log("Created risk data ", riskData);
|
||||
|
||||
type EditRiskFormValues = z.infer<typeof editRiskSchema>;
|
||||
|
||||
const form = useForm<EditRiskFormValues>({
|
||||
resolver: zodResolver(editRiskSchema),
|
||||
defaultValues: {
|
||||
riskDescription: riskData.riskDescription,
|
||||
riskProbability: riskData.riskProbability,
|
||||
riskImpact: String(riskData.riskImpact),
|
||||
riskOwner: riskData.riskOwner,
|
||||
riskStatus: normalizeStatus(riskData.riskStatus),
|
||||
},
|
||||
});
|
||||
|
||||
const { control, handleSubmit } = form;
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const projectData = await getProjectByIdentifierApi(projectIdentifier);
|
||||
|
||||
console.log("Project Data ", projectData);
|
||||
|
||||
const {
|
||||
projectTeam = [],
|
||||
projectTeamUnderProjectManager = [],
|
||||
projectManagerDelegates,
|
||||
} = projectData;
|
||||
|
||||
const managerDelegatesArr = projectManagerDelegates
|
||||
? Array.isArray(projectManagerDelegates)
|
||||
? projectManagerDelegates
|
||||
: [projectManagerDelegates]
|
||||
: [];
|
||||
|
||||
const mergedAssignees = [
|
||||
...projectTeam,
|
||||
...projectTeamUnderProjectManager,
|
||||
...managerDelegatesArr,
|
||||
];
|
||||
|
||||
const uniqueAssignees = [...new Set(mergedAssignees)];
|
||||
|
||||
console.log("Owner members ", uniqueAssignees);
|
||||
|
||||
setAssigneeOptions(uniqueAssignees);
|
||||
} catch (error) {
|
||||
console.error("Error fetching project:", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (projectIdentifier) {
|
||||
fetchProjects();
|
||||
}
|
||||
}, [projectIdentifier]);
|
||||
|
||||
const onSubmit = (values: EditRiskFormValues) => {
|
||||
console.log("edit risk data update ", values);
|
||||
|
||||
const { riskIdentifier, riskCreatedDate, ...cleanRiskData } = riskData;
|
||||
|
||||
onSave({
|
||||
...cleanRiskData,
|
||||
...values,
|
||||
});
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||
<DialogContent className="max-w-3xl!">
|
||||
{/* Header */}
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base font-semibold">
|
||||
Edit Risk {riskData.projectIdentifier}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-xs text-muted-foreground" />
|
||||
</DialogHeader>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Body */}
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
{/* RiskWorkflow */}
|
||||
<FormField
|
||||
control={control}
|
||||
name="riskStatus"
|
||||
render={({ field }) => (
|
||||
<RiskWorkflow
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
standalone={false}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Description */}
|
||||
<FormField
|
||||
control={control}
|
||||
name="riskDescription"
|
||||
render={({ field, fieldState }) => (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-semibold">Description</Label>
|
||||
<Textarea
|
||||
{...field}
|
||||
placeholder="Describe the risk..."
|
||||
className="resize-none text-sm min-h-[90px]"
|
||||
/>
|
||||
{fieldState.error && (
|
||||
<p className="text-xs text-red-500">
|
||||
{fieldState.error.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Probability + Impact */}
|
||||
<div className="grid grid-cols-10 gap-4">
|
||||
{/* Probability (small) */}
|
||||
<div className="col-span-2">
|
||||
<FormField
|
||||
control={control}
|
||||
name="riskProbability"
|
||||
render={({ field, fieldState }) => (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-semibold">Probability</Label>
|
||||
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
placeholder="Enter probability"
|
||||
className="text-sm h-9"
|
||||
/>
|
||||
|
||||
{fieldState.error && (
|
||||
<p className="text-xs text-red-500">
|
||||
{fieldState.error.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Impact (wide) */}
|
||||
<div className="col-span-8">
|
||||
<FormField
|
||||
control={control}
|
||||
name="riskImpact"
|
||||
render={({ field, fieldState }) => (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-semibold">Impact</Label>
|
||||
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="text-sm h-9">
|
||||
<SelectValue placeholder="Select..." />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="Catastrophic Impact on Schedule/Cost to business/Quality/HSSE">
|
||||
5: Catastrophic Impact on Schedule/Cost to
|
||||
business/Quality/HSSE
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="High Impact on Schedule/Cost to business/Quality/HSSE">
|
||||
4: High Impact on Schedule/Cost to
|
||||
business/Quality/HSSE
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="Medium Impact on Schedule/Cost to business/Quality/HSSE">
|
||||
3: Medium Impact on Schedule/Cost to
|
||||
business/Quality/HSSE
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="Some impact">
|
||||
2: Some impact
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="Little impact">
|
||||
1: Little impact
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{fieldState.error && (
|
||||
<p className="text-xs text-red-500">
|
||||
{fieldState.error.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Owner */}
|
||||
<div className="col-span-6">
|
||||
<FormField
|
||||
control={control}
|
||||
name="riskOwner"
|
||||
render={({ field, fieldState }) => (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-semibold">
|
||||
Risk Owner <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="text-sm h-9">
|
||||
<SelectValue placeholder="Select Risk Owner" />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
{assigneeOptions.map((id) => (
|
||||
<SelectItem key={id} value={id}>
|
||||
{userNameMap.get(id) || "Loading..."}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{fieldState.error && (
|
||||
<p className="text-xs text-red-500">
|
||||
{fieldState.error.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end gap-2 px-6 py-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onClose}
|
||||
className="h-9 px-4 text-sm"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
className="h-9 px-5 text-sm font-semibold"
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const editRiskSchema = z.object({
|
||||
riskDescription: z.string().min(5, "Description must be at least 5 characters"),
|
||||
riskProbability: z.number().min(0).max(100),
|
||||
riskImpact: z.string().min(1, "Impact is required"),
|
||||
riskOwner: z.string().min(1, "Owner is required"),
|
||||
riskStatus: z.enum([
|
||||
"identified",
|
||||
"mitigation_planned",
|
||||
"monitoring",
|
||||
"closed",
|
||||
]),
|
||||
});
|
||||
@@ -0,0 +1,442 @@
|
||||
"use client";
|
||||
|
||||
import { use, useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
import {
|
||||
Checkbox,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "ikoncomponents";
|
||||
|
||||
import { Input } from "ikoncomponents";
|
||||
import { Textarea } from "ikoncomponents";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from "ikoncomponents";
|
||||
import { Button } from "ikoncomponents";
|
||||
|
||||
import { RiskData } from "@/app/utils/interface/risk";
|
||||
import { getProjectByIdentifierApi } from "@/app/utils/api/projectApi";
|
||||
import { useAppCache } from "@/app/utils/context/AppCacheContext";
|
||||
import { riskSchema } from "./schema";
|
||||
|
||||
type FormValues = z.infer<typeof riskSchema>;
|
||||
|
||||
interface Props {
|
||||
projectIdentifier: string;
|
||||
onSubmit: (data: RiskData) => void;
|
||||
}
|
||||
|
||||
export default function RiskForm({ projectIdentifier, onSubmit }: Props) {
|
||||
const { userNameMap } = useAppCache();
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(riskSchema),
|
||||
defaultValues: {
|
||||
riskTitle: "",
|
||||
riskProbability: 0,
|
||||
grossRiskValue: 0,
|
||||
probableRiskValue: 0,
|
||||
probableRiskValueInUSD: 0,
|
||||
riskImpact: "",
|
||||
riskOwner: "",
|
||||
riskDescription: "",
|
||||
projectIdentifier,
|
||||
financialRisk: false,
|
||||
riskCreatedDate: new Date().toISOString(),
|
||||
riskOptionsSelectId: "predefinedRisks",
|
||||
riskStatus: "identified",
|
||||
riskAge: 0,
|
||||
effectedSprintId: "",
|
||||
},
|
||||
});
|
||||
|
||||
const probability = form.watch("riskProbability");
|
||||
const gross = form.watch("grossRiskValue");
|
||||
const createdDate = form.watch("riskCreatedDate");
|
||||
|
||||
const [assigneeOptions, setAssigneeOptions] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const projectData = await getProjectByIdentifierApi(projectIdentifier);
|
||||
|
||||
console.log("Project Data ", projectData);
|
||||
|
||||
const {
|
||||
projectTeam = [],
|
||||
projectTeamUnderProjectManager = [],
|
||||
projectManagerDelegates,
|
||||
} = projectData;
|
||||
|
||||
const managerDelegatesArr = projectManagerDelegates
|
||||
? Array.isArray(projectManagerDelegates)
|
||||
? projectManagerDelegates
|
||||
: [projectManagerDelegates]
|
||||
: [];
|
||||
|
||||
const mergedAssignees = [
|
||||
...projectTeam,
|
||||
...projectTeamUnderProjectManager,
|
||||
...managerDelegatesArr,
|
||||
];
|
||||
|
||||
const uniqueAssignees = [...new Set(mergedAssignees)];
|
||||
|
||||
setAssigneeOptions(uniqueAssignees);
|
||||
} catch (error) {
|
||||
console.error("Error fetching project:", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (projectIdentifier) {
|
||||
fetchProjects();
|
||||
}
|
||||
}, [projectIdentifier]);
|
||||
|
||||
useEffect(() => {
|
||||
const probable = probability && gross ? (probability * gross) / 100 : 0;
|
||||
|
||||
form.setValue("probableRiskValue", probable);
|
||||
form.setValue("probableRiskValueInUSD", probable);
|
||||
}, [probability, gross, form]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!createdDate) return;
|
||||
|
||||
const created = new Date(createdDate);
|
||||
const today = new Date();
|
||||
const diff = Math.floor(
|
||||
(today.getTime() - created.getTime()) / (1000 * 60 * 60 * 24),
|
||||
);
|
||||
|
||||
if (diff >= 0) {
|
||||
form.setValue("riskAge", diff);
|
||||
}
|
||||
}, [createdDate, form]);
|
||||
|
||||
const handleSubmit = (data: FormValues) => {
|
||||
onSubmit(data);
|
||||
form.reset();
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
className="grid grid-cols-12 gap-4 "
|
||||
>
|
||||
{/* Risk Option */}
|
||||
<div className="col-span-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="riskOptionsSelectId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="mb-2 mt-2 ml-2">
|
||||
Risk Options <span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<FormControl className="w-full">
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="predefinedRisks">
|
||||
Predefined Risks
|
||||
</SelectItem>
|
||||
<SelectItem value="manualRisks">Manual Risks</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Risk Title */}
|
||||
<div className="col-span-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="riskTitle"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="mb-2">
|
||||
Risk Title <span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Probability */}
|
||||
<div className="col-span-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="riskProbability"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="mb-2">
|
||||
Probability % <span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Gross Risk Value */}
|
||||
<div className="col-span-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="grossRiskValue"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="mb-2">Gross Risk Value</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Probable Risk Value */}
|
||||
<div className="col-span-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="probableRiskValue"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="mb-2">Probable Risk Value</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" value={field.value} disabled />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Impact */}
|
||||
<div className="col-span-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="riskImpact"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="mb-2">
|
||||
Impact (1-5) <span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
|
||||
<Select
|
||||
onValueChange={(value) => field.onChange(String(value))}
|
||||
defaultValue={field.value?.toString()}
|
||||
>
|
||||
<FormControl className="w-full">
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue
|
||||
placeholder="Select Impact"
|
||||
className="className=mb-1"
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="Catastrophic Impact on Schedule/Cost to business/Quality/HSSE">
|
||||
5: Catastrophic Impact on Schedule/Cost to
|
||||
business/Quality/HSSE
|
||||
</SelectItem>
|
||||
<SelectItem value="High Impact on Schedule/Cost to business/Quality/HSSE">
|
||||
4: High Impact on Schedule/Cost to business/Quality/HSSE
|
||||
</SelectItem>
|
||||
<SelectItem value="Medium Impact on Schedule/Cost to business/Quality/HSSE">
|
||||
3: Medium Impact on Schedule/Cost to business/Quality/HSSE
|
||||
</SelectItem>
|
||||
<SelectItem value="Some impact">2: Some impact</SelectItem>
|
||||
<SelectItem value="Little impact">
|
||||
1: Little impact
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Risk Owner */}
|
||||
<div className="col-span-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="riskOwner"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="mb-2">
|
||||
Risk Owner <span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select Risk Owner" />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
{assigneeOptions.map((id) => (
|
||||
<SelectItem key={id} value={id}>
|
||||
{userNameMap.get(id) || "Loading..."}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Financial Risk */}
|
||||
<div className="col-span-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="financialRisk"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="mb-2">Financial Risk</FormLabel>
|
||||
<Select
|
||||
value={String(field.value)}
|
||||
onValueChange={(val) => field.onChange(val === "true")}
|
||||
>
|
||||
<FormControl className="w-full">
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="false">No</SelectItem>
|
||||
<SelectItem value="true">Yes</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Created Date */}
|
||||
<div className="col-span-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="riskCreatedDate"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="mb-2">Created On</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
value={new Date(field.value).toLocaleDateString()}
|
||||
disabled
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Risk Status */}
|
||||
{/* <div className="col-span-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="riskStatus"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="mb-2">Risk Status</FormLabel>
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="Active">Active</SelectItem>
|
||||
<SelectItem value="On hold">On Hold</SelectItem>
|
||||
<SelectItem value="Resolved">Resolved</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div> */}
|
||||
|
||||
{/* Risk Age */}
|
||||
{/* <div className="col-span-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="riskAge"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="mb-2">Risk Age (Days)</FormLabel>
|
||||
<FormControl>
|
||||
<Input value={field.value} disabled />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div> */}
|
||||
|
||||
{/* Description */}
|
||||
<div className="col-span-12">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="riskDescription"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="mb-2">
|
||||
Risk Description <span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea rows={4} {...field} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 flex justify-end">
|
||||
<Button type="submit">Add Risk</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import z from "zod";
|
||||
|
||||
export const riskSchema = z.object({
|
||||
riskTitle: z.string().min(1),
|
||||
riskProbability: z.number().min(0).max(100),
|
||||
grossRiskValue: z.number().min(0),
|
||||
probableRiskValue: z.number(),
|
||||
probableRiskValueInUSD: z.number(),
|
||||
riskImpact: z.string(),
|
||||
riskOwner: z.string().min(1),
|
||||
riskDescription: z.string().min(1),
|
||||
projectIdentifier: z.string(),
|
||||
financialRisk: z.boolean(),
|
||||
riskCreatedDate: z.string(),
|
||||
riskOptionsSelectId: z.string(),
|
||||
riskStatus: z.string().optional(),
|
||||
riskAge: z.number().optional(),
|
||||
effectedSprintId: z.string().optional(),
|
||||
});
|
||||
@@ -0,0 +1,445 @@
|
||||
import React, { useState } from "react";
|
||||
import { Search, Shield, Eye, Lock, LucideIcon } from "lucide-react";
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Separator,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
cn,
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "ikoncomponents";
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export type RiskStatus =
|
||||
| "identified"
|
||||
| "mitigation_planned"
|
||||
| "monitoring"
|
||||
| "closed";
|
||||
|
||||
interface Stage {
|
||||
id: RiskStatus;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
}
|
||||
|
||||
interface NavRule {
|
||||
canProceed: RiskStatus[];
|
||||
canGoBack: RiskStatus[];
|
||||
}
|
||||
|
||||
interface StatusConfig {
|
||||
hex: string;
|
||||
}
|
||||
|
||||
interface RiskWorkflowProps {
|
||||
value?: RiskStatus;
|
||||
initialStatus?: RiskStatus;
|
||||
onChange?: (status: RiskStatus) => void;
|
||||
standalone?: boolean;
|
||||
}
|
||||
|
||||
// ── Data ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
const STAGES: Stage[] = [
|
||||
{ id: "identified", label: "Identified", icon: Search },
|
||||
{ id: "mitigation_planned", label: "Mitigation Planned", icon: Shield },
|
||||
{ id: "monitoring", label: "Monitoring", icon: Eye },
|
||||
{ id: "closed", label: "Closed", icon: Lock },
|
||||
];
|
||||
|
||||
const NAV_RULES: Record<RiskStatus, NavRule> = {
|
||||
identified: { canProceed: ["mitigation_planned"], canGoBack: [] },
|
||||
mitigation_planned: { canProceed: ["monitoring"], canGoBack: ["identified"] },
|
||||
monitoring: { canProceed: ["closed"], canGoBack: ["mitigation_planned", "identified"] },
|
||||
closed: { canProceed: [], canGoBack: [] },
|
||||
};
|
||||
|
||||
const STATUS_CONFIG: Record<RiskStatus, StatusConfig> = {
|
||||
identified: { hex: "#818cf8" },
|
||||
mitigation_planned: { hex: "#fbbf24" },
|
||||
monitoring: { hex: "#60a5fa" },
|
||||
closed: { hex: "#4ade80" },
|
||||
};
|
||||
|
||||
const STAGE_IDS = STAGES.map((s) => s.id);
|
||||
|
||||
function getLabel(id: RiskStatus): string {
|
||||
return STAGES.find((s) => s.id === id)?.label ?? id;
|
||||
}
|
||||
|
||||
// ── StageNode ─────────────────────────────────────────────────────────────────
|
||||
|
||||
interface StageNodeProps {
|
||||
stage: Stage;
|
||||
isActive: boolean;
|
||||
isCompleted: boolean;
|
||||
cfg: StatusConfig;
|
||||
isClickable: boolean;
|
||||
onClickNode: () => void;
|
||||
tooltipText: string;
|
||||
}
|
||||
|
||||
function StageNode({
|
||||
stage,
|
||||
isActive,
|
||||
isCompleted,
|
||||
cfg,
|
||||
isClickable,
|
||||
onClickNode,
|
||||
tooltipText,
|
||||
}: StageNodeProps) {
|
||||
const Icon = stage.icon;
|
||||
|
||||
const circleStyle: React.CSSProperties = isActive
|
||||
? {
|
||||
borderColor: cfg.hex,
|
||||
backgroundColor: `${cfg.hex}20`,
|
||||
boxShadow: `0 0 0 3px ${cfg.hex}30`,
|
||||
}
|
||||
: isCompleted
|
||||
? {
|
||||
borderColor: `${cfg.hex}60`,
|
||||
backgroundColor: `${cfg.hex}12`,
|
||||
}
|
||||
: {};
|
||||
|
||||
const iconColor = isActive
|
||||
? cfg.hex
|
||||
: isCompleted
|
||||
? `${cfg.hex}80`
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={150}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={isClickable ? onClickNode : undefined}
|
||||
disabled={!isClickable}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-2.5 select-none outline-none group",
|
||||
isClickable ? "cursor-pointer" : "cursor-default"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-11 h-11 rounded-full flex items-center justify-center border-2 transition-all duration-500 mb-2",
|
||||
isActive && "scale-110 shadow-lg",
|
||||
!isActive && !isCompleted && "border-muted bg-muted/20",
|
||||
isClickable && !isActive && "group-hover:scale-105 group-hover:opacity-90"
|
||||
)}
|
||||
style={circleStyle}
|
||||
>
|
||||
<Icon
|
||||
size={17}
|
||||
className="transition-colors duration-300"
|
||||
style={{
|
||||
color: iconColor ?? (isActive || isCompleted ? undefined : "hsl(var(--muted-foreground) / 0.3)"),
|
||||
}}
|
||||
/>
|
||||
{isCompleted && (
|
||||
<span
|
||||
className="absolute -top-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-background"
|
||||
style={{ backgroundColor: cfg.hex }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
"text-[11px] font-medium text-center leading-tight max-w-[76px] transition-colors duration-300",
|
||||
isActive && "font-semibold",
|
||||
!isActive && !isCompleted && "text-muted-foreground/30"
|
||||
)}
|
||||
style={{
|
||||
color: isActive
|
||||
? cfg.hex
|
||||
: isCompleted
|
||||
? "hsl(var(--muted-foreground) / 0.6)"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{stage.label}
|
||||
</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="text-xs max-w-[160px] text-center">
|
||||
{tooltipText}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Connector ─────────────────────────────────────────────────────────────────
|
||||
|
||||
interface ConnectorProps {
|
||||
isCompleted: boolean;
|
||||
cfg: StatusConfig;
|
||||
}
|
||||
|
||||
function Connector({ isCompleted, cfg }: ConnectorProps) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center pb-7 mx-1 min-w-5">
|
||||
<div className="relative w-full h-[2px] rounded-full bg-muted/40 overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 rounded-full transition-all duration-700 ease-in-out"
|
||||
style={{
|
||||
width: isCompleted ? "100%" : "0%",
|
||||
backgroundColor: isCompleted ? cfg.hex : "transparent",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Confirmation Dialog ───────────────────────────────────────────────────────
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
open: boolean;
|
||||
targetStatus: RiskStatus | null;
|
||||
currentStatus: RiskStatus;
|
||||
cfg: StatusConfig;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
function ConfirmDialog({
|
||||
open,
|
||||
targetStatus,
|
||||
currentStatus,
|
||||
cfg,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: ConfirmDialogProps) {
|
||||
if (!targetStatus) return null;
|
||||
|
||||
const currentIndex = STAGE_IDS.indexOf(currentStatus);
|
||||
const targetIndex = STAGE_IDS.indexOf(targetStatus);
|
||||
const isGoingBack = targetIndex < currentIndex;
|
||||
const targetCfg = STATUS_CONFIG[targetStatus];
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={(v) => !v && onCancel()}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="flex items-center gap-2">
|
||||
{isGoingBack ? "Go back to" : "Proceed to"}{" "}
|
||||
<span
|
||||
className="font-bold"
|
||||
style={{ color: targetCfg.hex }}
|
||||
>
|
||||
{getLabel(targetStatus)}
|
||||
</span>
|
||||
?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-sm text-muted-foreground">
|
||||
{isGoingBack
|
||||
? `This will move the risk status back from "${getLabel(currentStatus)}" to "${getLabel(targetStatus)}". Are you sure you want to recall this stage?`
|
||||
: `This will advance the risk status from "${getLabel(currentStatus)}" to "${getLabel(targetStatus)}". Do you want to proceed?`}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
{/* Stage transition visual */}
|
||||
<div className="flex items-center justify-center gap-3 py-2">
|
||||
<span
|
||||
className="text-xs font-semibold px-3 py-1 rounded-full border"
|
||||
style={{
|
||||
color: cfg.hex,
|
||||
backgroundColor: `${cfg.hex}15`,
|
||||
borderColor: `${cfg.hex}30`,
|
||||
}}
|
||||
>
|
||||
{getLabel(currentStatus)}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{"→"}
|
||||
</span>
|
||||
<span
|
||||
className="text-xs font-semibold px-3 py-1 rounded-full border"
|
||||
style={{
|
||||
color: targetCfg.hex,
|
||||
backgroundColor: `${targetCfg.hex}15`,
|
||||
borderColor: `${targetCfg.hex}30`,
|
||||
}}
|
||||
>
|
||||
{getLabel(targetStatus)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={onCancel}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={onConfirm}
|
||||
style={{ backgroundColor: targetCfg.hex, color: "#0d1117", border: "none" }}
|
||||
>
|
||||
{isGoingBack ? "Yes, Go Back" : "Yes, Proceed"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
|
||||
// ── RiskWorkflow ──────────────────────────────────────────────────────────────
|
||||
|
||||
export default function RiskWorkflow({
|
||||
value,
|
||||
initialStatus = "identified",
|
||||
onChange,
|
||||
standalone,
|
||||
}: RiskWorkflowProps) {
|
||||
const isControlled = value !== undefined;
|
||||
const [internalStatus, setInternalStatus] = useState<RiskStatus>(initialStatus);
|
||||
const currentStatus: RiskStatus = isControlled ? value! : internalStatus;
|
||||
|
||||
// Confirmation dialog state
|
||||
const [pendingTarget, setPendingTarget] = useState<RiskStatus | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
const cfg = STATUS_CONFIG[currentStatus] ?? STATUS_CONFIG["identified"];
|
||||
const activeIndex = STAGE_IDS.indexOf(currentStatus);
|
||||
const rules = NAV_RULES[currentStatus] ?? { canProceed: [], canGoBack: [] };
|
||||
|
||||
// Step 1: user clicks a stage → open confirmation dialog
|
||||
function requestNavigate(targetId: RiskStatus) {
|
||||
setPendingTarget(targetId);
|
||||
setDialogOpen(true);
|
||||
}
|
||||
|
||||
// Step 2a: user confirms → actually navigate
|
||||
function confirmNavigate() {
|
||||
if (!pendingTarget) return;
|
||||
if (!isControlled) setInternalStatus(pendingTarget);
|
||||
onChange?.(pendingTarget);
|
||||
setDialogOpen(false);
|
||||
setPendingTarget(null);
|
||||
}
|
||||
|
||||
// Step 2b: user cancels → dismiss
|
||||
function cancelNavigate() {
|
||||
setDialogOpen(false);
|
||||
setPendingTarget(null);
|
||||
}
|
||||
|
||||
function getTooltip(stageId: RiskStatus, idx: number): string {
|
||||
if (stageId === currentStatus) return "Current stage";
|
||||
if (rules.canProceed.includes(stageId)) return `Click to proceed → ${getLabel(stageId)}`;
|
||||
if (rules.canGoBack.includes(stageId)) return `Click to go back → ${getLabel(stageId)}`;
|
||||
if (idx < activeIndex) return "Completed";
|
||||
return "Not reachable from current stage";
|
||||
}
|
||||
|
||||
function isClickable(stageId: RiskStatus): boolean {
|
||||
return rules.canProceed.includes(stageId) || rules.canGoBack.includes(stageId);
|
||||
}
|
||||
|
||||
const card = (
|
||||
<>
|
||||
<Card
|
||||
className="w-full transition-all duration-500"
|
||||
style={{
|
||||
borderColor: `${cfg.hex}28`,
|
||||
boxShadow: `0 0 0 1px ${cfg.hex}12, 0 8px 40px ${cfg.hex}12`,
|
||||
}}
|
||||
>
|
||||
<CardHeader className="pb-3 pt-5 px-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-sm font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
Current Stage
|
||||
</CardTitle>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-xs font-semibold px-2.5 py-0.5 rounded-full border"
|
||||
style={{
|
||||
color: cfg.hex,
|
||||
backgroundColor: `${cfg.hex}15`,
|
||||
borderColor: `${cfg.hex}30`,
|
||||
}}
|
||||
>
|
||||
{getLabel(currentStatus)}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<Separator />
|
||||
|
||||
<CardContent className="px-6 pt-5 pb-6">
|
||||
<div className="flex items-start">
|
||||
{STAGES.map((stage, i) => {
|
||||
const isActive = i === activeIndex;
|
||||
const isCompleted = i < activeIndex;
|
||||
return (
|
||||
<div key={stage.id} className="flex items-center flex-1 last:flex-none">
|
||||
<StageNode
|
||||
stage={stage}
|
||||
isActive={isActive}
|
||||
isCompleted={isCompleted}
|
||||
cfg={cfg}
|
||||
isClickable={isClickable(stage.id)}
|
||||
onClickNode={() => requestNavigate(stage.id)}
|
||||
tooltipText={getTooltip(stage.id, i)}
|
||||
/>
|
||||
{i < STAGES.length - 1 && (
|
||||
<Connector isCompleted={isCompleted} cfg={cfg} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{currentStatus === "closed" && (
|
||||
<div className="flex justify-center mt-4">
|
||||
<Badge
|
||||
className="text-xs px-3 py-1 rounded-full"
|
||||
style={{
|
||||
backgroundColor: `${cfg.hex}20`,
|
||||
color: cfg.hex,
|
||||
border: `1px solid ${cfg.hex}30`,
|
||||
}}
|
||||
>
|
||||
✓ Risk Closed
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Confirmation dialog — rendered outside Card so it's not clipped */}
|
||||
<ConfirmDialog
|
||||
open={dialogOpen}
|
||||
targetStatus={pendingTarget}
|
||||
currentStatus={currentStatus}
|
||||
cfg={cfg}
|
||||
onConfirm={confirmNavigate}
|
||||
onCancel={cancelNavigate}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
if (standalone) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-8">
|
||||
<div className="w-full max-w-[560px]">{card}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { DataTableLayout, ColumnDef } from "ikoncomponents";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "ikoncomponents";
|
||||
import { Plus, SquarePenIcon, Trash } from "lucide-react";
|
||||
|
||||
import { RiskData } from "@/app/utils/interface/risk";
|
||||
import RiskForm from "./components/riskModal";
|
||||
import { IconTextButtonWithTooltip } from "ikoncomponents";
|
||||
import EditRiskModal from "./components/editRisk";
|
||||
import {
|
||||
createRiskApi,
|
||||
risksApi,
|
||||
updateRiskApi,
|
||||
} from "@/app/utils/api/riskApi";
|
||||
import { useAppCache } from "@/app/utils/context/AppCacheContext";
|
||||
|
||||
interface RiskComponentProps {
|
||||
projectIdentifier: string;
|
||||
}
|
||||
|
||||
export default function RiskComponent({
|
||||
projectIdentifier,
|
||||
}: RiskComponentProps) {
|
||||
const { userNameMap } = useAppCache();
|
||||
|
||||
// ── State ─────────────────────────────────────────────────────
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [selectedRisk, setSelectedRisk] = useState<RiskData | null>(null);
|
||||
const [data, setData] = useState<RiskData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// ── Fetch Risks from API ──────────────────────────────────────
|
||||
const fetchRisks = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await risksApi();
|
||||
|
||||
console.log("Risk list response -----> ", response);
|
||||
|
||||
// Adjust this depending on your API structure
|
||||
setData(response?.data || response || []);
|
||||
} catch (error) {
|
||||
console.error("Error fetching risks:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchRisks();
|
||||
}, []);
|
||||
|
||||
// ── Handlers ───────────────────────────────────────────────────
|
||||
const openForCreate = () => setCreateOpen(true);
|
||||
|
||||
const openForEdit = (risk: RiskData) => {
|
||||
setSelectedRisk(risk);
|
||||
setEditOpen(true);
|
||||
};
|
||||
|
||||
const handleCreateSubmit = async (formData: RiskData) => {
|
||||
try {
|
||||
const response = await createRiskApi(formData);
|
||||
console.log("Risk created -----> ", response);
|
||||
|
||||
setCreateOpen(false);
|
||||
fetchRisks();
|
||||
} catch (error) {
|
||||
console.error("Error creating risk:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// const handleEditSave = async (updated: RiskData) => {
|
||||
// console.log("Updated Risk:", updated);
|
||||
// const response = await updateRiskApi(updated);
|
||||
// console.log(response);
|
||||
// setEditOpen(false);
|
||||
// setSelectedRisk(null);
|
||||
|
||||
// fetchRisks(); //
|
||||
// };
|
||||
|
||||
const handleEditSave = async (updatedValues: RiskData) => {
|
||||
if (!selectedRisk?.riskIdentifier) {
|
||||
console.error("No risk selected for update");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Updated Risk:", updatedValues);
|
||||
|
||||
const response = await updateRiskApi(
|
||||
selectedRisk.riskIdentifier,
|
||||
updatedValues,
|
||||
);
|
||||
|
||||
console.log(response);
|
||||
|
||||
setEditOpen(false);
|
||||
setSelectedRisk(null);
|
||||
fetchRisks();
|
||||
};
|
||||
// ── Table Columns ─────────────────────────────────────────────
|
||||
const columns: ColumnDef<RiskData>[] = [
|
||||
{
|
||||
accessorKey: "riskDescription",
|
||||
header: () => <div className="text-center">Description</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "riskProbability",
|
||||
header: () => <div className="text-center">Probability (%)</div>,
|
||||
cell: (row) => <span>{row.riskProbability}%</span>,
|
||||
},
|
||||
{
|
||||
accessorKey: "riskImpact",
|
||||
header: () => <div className="text-center">Impact</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "probableRiskValue",
|
||||
header: () => <div className="text-center">Score</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "riskOwner",
|
||||
header: () => <div className="text-center">Owner</div>,
|
||||
cell: (row) => (
|
||||
<span>
|
||||
{row.riskOwner ? userMap[row.riskOwner] : "N/A"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "riskStatus",
|
||||
header: () => <div className="text-center">Status</div>,
|
||||
},
|
||||
{
|
||||
header: () => <div className="text-center">Actions</div>,
|
||||
cell: (row) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<IconTextButtonWithTooltip
|
||||
tooltipContent="Edit"
|
||||
variant="ghost"
|
||||
onClick={() => openForEdit(row)}
|
||||
>
|
||||
<SquarePenIcon />
|
||||
</IconTextButtonWithTooltip>
|
||||
<IconTextButtonWithTooltip
|
||||
tooltipContent="Delete"
|
||||
variant="ghost"
|
||||
onClick={() => console.log("Delete", row.riskIdentifier)}
|
||||
>
|
||||
<Trash />
|
||||
</IconTextButtonWithTooltip>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// ── Render ────────────────────────────────────────────────────
|
||||
return (
|
||||
<>
|
||||
<DataTableLayout
|
||||
columns={columns}
|
||||
data={data}
|
||||
extraTools={{
|
||||
keyExtractor: (row: RiskData) => row.riskIdentifier ?? row.riskDescription,
|
||||
totalPages: 0,
|
||||
currentPage: 0,
|
||||
isLoading: loading,
|
||||
actionNode: (
|
||||
<IconTextButtonWithTooltip
|
||||
key="create-risk"
|
||||
tooltipContent="Add new risk"
|
||||
variant="outline"
|
||||
onClick={openForCreate}
|
||||
>
|
||||
<Plus />
|
||||
</IconTextButtonWithTooltip>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
{selectedRisk && (
|
||||
<EditRiskModal
|
||||
open={editOpen}
|
||||
onClose={() => {
|
||||
setEditOpen(false);
|
||||
setSelectedRisk(null);
|
||||
}}
|
||||
riskData={selectedRisk}
|
||||
onSave={handleEditSave}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||
<DialogContent className="max-w-6xl! w-full">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add New Risk</DialogTitle>
|
||||
</DialogHeader>
|
||||
<RiskForm
|
||||
projectIdentifier={projectIdentifier}
|
||||
onSubmit={handleCreateSubmit}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user