first commit

This commit is contained in:
Your NamebaishaliHolocron
2026-06-15 12:57:03 +05:30
commit b9ac5ae0b2
398 changed files with 49583 additions and 0 deletions

View File

@@ -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>
);
}

View File

@@ -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",
]),
});

View File

@@ -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>
);
}

View File

@@ -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(),
});

View File

@@ -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;
}

View File

@@ -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>
</>
);
}