366 lines
12 KiB
TypeScript
366 lines
12 KiB
TypeScript
|
|
import React, { useState, useEffect } from "react";
|
|||
|
|
import { FaTimes, FaSave } from "react-icons/fa";
|
|||
|
|
import {
|
|||
|
|
PsProjectRisk,
|
|||
|
|
RiskCategoryEnum,
|
|||
|
|
RiskProbabilityEnum,
|
|||
|
|
RiskImpactEnum,
|
|||
|
|
RiskLevelEnum,
|
|||
|
|
RiskStatusEnum,
|
|||
|
|
} from "../../../types/ps";
|
|||
|
|
import {
|
|||
|
|
getRiskCategoryText,
|
|||
|
|
getRiskProbabilityText,
|
|||
|
|
getRiskImpactText,
|
|||
|
|
getRiskLevelColor,
|
|||
|
|
getRiskLevelText,
|
|||
|
|
getRiskStatusText,
|
|||
|
|
} from "../../../utils/erp";
|
|||
|
|
|
|||
|
|
interface RiskModalProps {
|
|||
|
|
isOpen: boolean;
|
|||
|
|
onClose: () => void;
|
|||
|
|
risk?: PsProjectRisk | null;
|
|||
|
|
onSubmit: (riskData: Partial<PsProjectRisk>) => void;
|
|||
|
|
mode: "create" | "edit";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const RiskModal: React.FC<RiskModalProps> = ({
|
|||
|
|
isOpen,
|
|||
|
|
onClose,
|
|||
|
|
risk,
|
|||
|
|
onSubmit,
|
|||
|
|
mode,
|
|||
|
|
}) => {
|
|||
|
|
const [formData, setFormData] = useState({
|
|||
|
|
title: "",
|
|||
|
|
description: "",
|
|||
|
|
category: RiskCategoryEnum.Technical,
|
|||
|
|
probability: RiskProbabilityEnum.Medium,
|
|||
|
|
impact: RiskImpactEnum.Medium,
|
|||
|
|
riskLevel: RiskLevelEnum.Medium,
|
|||
|
|
status: RiskStatusEnum.Identified,
|
|||
|
|
mitigationPlan: "",
|
|||
|
|
contingencyPlan: "",
|
|||
|
|
ownerId: "",
|
|||
|
|
reviewDate: "",
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (mode === "edit" && risk) {
|
|||
|
|
setFormData({
|
|||
|
|
title: risk.title || "",
|
|||
|
|
description: risk.description || "",
|
|||
|
|
category: risk.category || RiskCategoryEnum.Technical,
|
|||
|
|
probability: risk.probability || RiskProbabilityEnum.Medium,
|
|||
|
|
impact: risk.impact || RiskImpactEnum.Medium,
|
|||
|
|
riskLevel: risk.riskLevel || RiskLevelEnum.Medium,
|
|||
|
|
status: risk.status || RiskStatusEnum.Identified,
|
|||
|
|
mitigationPlan: risk.mitigationPlan || "",
|
|||
|
|
contingencyPlan: risk.contingencyPlan || "",
|
|||
|
|
ownerId: risk.ownerId || "",
|
|||
|
|
reviewDate: risk.reviewDate
|
|||
|
|
? new Date(risk.reviewDate).toISOString().split("T")[0]
|
|||
|
|
: "",
|
|||
|
|
});
|
|||
|
|
} else if (mode === "create") {
|
|||
|
|
setFormData({
|
|||
|
|
title: "",
|
|||
|
|
description: "",
|
|||
|
|
category: RiskCategoryEnum.Technical,
|
|||
|
|
probability: RiskProbabilityEnum.Medium,
|
|||
|
|
impact: RiskImpactEnum.Medium,
|
|||
|
|
riskLevel: RiskLevelEnum.Medium,
|
|||
|
|
status: RiskStatusEnum.Identified,
|
|||
|
|
mitigationPlan: "",
|
|||
|
|
contingencyPlan: "",
|
|||
|
|
ownerId: "",
|
|||
|
|
reviewDate: "",
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}, [mode, risk, isOpen]);
|
|||
|
|
|
|||
|
|
// Auto-calculate risk level based on probability and impact
|
|||
|
|
useEffect(() => {
|
|||
|
|
const calculateRiskLevel = () => {
|
|||
|
|
const probValue = getRiskValue(formData.probability);
|
|||
|
|
const impactValue = getRiskValue(formData.impact);
|
|||
|
|
const total = probValue + impactValue;
|
|||
|
|
|
|||
|
|
if (total <= 4) {
|
|||
|
|
setFormData((prev) => ({ ...prev, riskLevel: RiskLevelEnum.Low }));
|
|||
|
|
} else if (total <= 6) {
|
|||
|
|
setFormData((prev) => ({ ...prev, riskLevel: RiskLevelEnum.Medium }));
|
|||
|
|
} else if (total <= 8) {
|
|||
|
|
setFormData((prev) => ({ ...prev, riskLevel: RiskLevelEnum.High }));
|
|||
|
|
} else {
|
|||
|
|
setFormData((prev) => ({ ...prev, riskLevel: RiskLevelEnum.Critical }));
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
calculateRiskLevel();
|
|||
|
|
}, [formData.probability, formData.impact]);
|
|||
|
|
|
|||
|
|
const getRiskValue = (risk: RiskProbabilityEnum | RiskImpactEnum): number => {
|
|||
|
|
const values = {
|
|||
|
|
VERY_LOW: 1,
|
|||
|
|
LOW: 2,
|
|||
|
|
MEDIUM: 3,
|
|||
|
|
HIGH: 4,
|
|||
|
|
VERY_HIGH: 5,
|
|||
|
|
};
|
|||
|
|
return values[risk as keyof typeof values] || 3;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleInputChange = (
|
|||
|
|
e: React.ChangeEvent<
|
|||
|
|
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
|
|||
|
|
>
|
|||
|
|
) => {
|
|||
|
|
const { name, value } = e.target;
|
|||
|
|
setFormData((prev) => ({ ...prev, [name]: value }));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|||
|
|
e.preventDefault();
|
|||
|
|
const submitData: Partial<PsProjectRisk> = {
|
|||
|
|
...formData,
|
|||
|
|
riskCode: mode === "create" ? `RISK-${Date.now()}` : risk?.riskCode,
|
|||
|
|
identifiedBy: mode === "create" ? "Current User" : risk?.identifiedBy,
|
|||
|
|
identifiedDate: mode === "create" ? new Date() : risk?.identifiedDate,
|
|||
|
|
reviewDate: formData.reviewDate
|
|||
|
|
? new Date(formData.reviewDate)
|
|||
|
|
: undefined,
|
|||
|
|
isActive: true,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
onSubmit(submitData);
|
|||
|
|
handleClose();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleClose = () => {
|
|||
|
|
setFormData({
|
|||
|
|
title: "",
|
|||
|
|
description: "",
|
|||
|
|
category: RiskCategoryEnum.Technical,
|
|||
|
|
probability: RiskProbabilityEnum.Medium,
|
|||
|
|
impact: RiskImpactEnum.Medium,
|
|||
|
|
riskLevel: RiskLevelEnum.Medium,
|
|||
|
|
status: RiskStatusEnum.Identified,
|
|||
|
|
mitigationPlan: "",
|
|||
|
|
contingencyPlan: "",
|
|||
|
|
ownerId: "",
|
|||
|
|
reviewDate: "",
|
|||
|
|
});
|
|||
|
|
onClose();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (!isOpen) return null;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|||
|
|
<div className="bg-white rounded-lg p-4 w-full max-w-xl max-h-[90vh] overflow-y-auto">
|
|||
|
|
<div className="flex items-center justify-between mb-4">
|
|||
|
|
<h2 className="text-lg font-semibold text-gray-900">
|
|||
|
|
{mode === "edit" ? "Risk Düzenle" : "Yeni Risk Ekle"}
|
|||
|
|
</h2>
|
|||
|
|
<button
|
|||
|
|
onClick={handleClose}
|
|||
|
|
className="text-gray-400 hover:text-gray-600"
|
|||
|
|
>
|
|||
|
|
<FaTimes className="w-5 h-5" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<form onSubmit={handleSubmit} className="space-y-3">
|
|||
|
|
{/* Title */}
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
Risk Başlığı *
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
name="title"
|
|||
|
|
value={formData.title}
|
|||
|
|
onChange={handleInputChange}
|
|||
|
|
className="w-full px-3 py-1.5 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|||
|
|
placeholder="Risk başlığını girin"
|
|||
|
|
required
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Description */}
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
Açıklama *
|
|||
|
|
</label>
|
|||
|
|
<textarea
|
|||
|
|
name="description"
|
|||
|
|
value={formData.description}
|
|||
|
|
onChange={handleInputChange}
|
|||
|
|
rows={2}
|
|||
|
|
className="w-full px-3 py-1.5 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|||
|
|
placeholder="Risk açıklamasını girin"
|
|||
|
|
required
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Category, Probability, Impact */}
|
|||
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
Kategori
|
|||
|
|
</label>
|
|||
|
|
<select
|
|||
|
|
name="category"
|
|||
|
|
value={formData.category}
|
|||
|
|
onChange={handleInputChange}
|
|||
|
|
className="w-full px-3 py-1.5 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|||
|
|
>
|
|||
|
|
{Object.values(RiskCategoryEnum).map((category) => (
|
|||
|
|
<option key={category} value={category}>
|
|||
|
|
{getRiskCategoryText(category)}
|
|||
|
|
</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
Olasılık
|
|||
|
|
</label>
|
|||
|
|
<select
|
|||
|
|
name="probability"
|
|||
|
|
value={formData.probability}
|
|||
|
|
onChange={handleInputChange}
|
|||
|
|
className="w-full px-3 py-1.5 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|||
|
|
>
|
|||
|
|
{Object.values(RiskProbabilityEnum).map((probability) => (
|
|||
|
|
<option key={probability} value={probability}>
|
|||
|
|
{getRiskProbabilityText(probability)}
|
|||
|
|
</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
Etki
|
|||
|
|
</label>
|
|||
|
|
<select
|
|||
|
|
name="impact"
|
|||
|
|
value={formData.impact}
|
|||
|
|
onChange={handleInputChange}
|
|||
|
|
className="w-full px-3 py-1.5 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|||
|
|
>
|
|||
|
|
{Object.values(RiskImpactEnum).map((impact) => (
|
|||
|
|
<option key={impact} value={impact}>
|
|||
|
|
{getRiskImpactText(impact)}
|
|||
|
|
</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Risk Level (Auto-calculated) */}
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
Risk Seviyesi (Otomatik Hesaplanan)
|
|||
|
|
</label>
|
|||
|
|
<div
|
|||
|
|
className={`inline-flex items-center px-2.5 py-1.5 rounded-full text-sm font-medium ${getRiskLevelColor(
|
|||
|
|
formData.riskLevel
|
|||
|
|
)}`}
|
|||
|
|
>
|
|||
|
|
{getRiskLevelText(formData.riskLevel)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Status */}
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
Durum
|
|||
|
|
</label>
|
|||
|
|
<select
|
|||
|
|
name="status"
|
|||
|
|
value={formData.status}
|
|||
|
|
onChange={handleInputChange}
|
|||
|
|
className="w-full px-3 py-1.5 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|||
|
|
>
|
|||
|
|
{Object.values(RiskStatusEnum).map((status) => (
|
|||
|
|
<option key={status} value={status}>
|
|||
|
|
{getRiskStatusText(status)}
|
|||
|
|
</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Mitigation Plan */}
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
Önlem Planı
|
|||
|
|
</label>
|
|||
|
|
<textarea
|
|||
|
|
name="mitigationPlan"
|
|||
|
|
value={formData.mitigationPlan}
|
|||
|
|
onChange={handleInputChange}
|
|||
|
|
rows={2}
|
|||
|
|
className="w-full px-3 py-1.5 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|||
|
|
placeholder="Risk için alınacak önlemleri açıklayın"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Contingency Plan */}
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
Acil Durum Planı
|
|||
|
|
</label>
|
|||
|
|
<textarea
|
|||
|
|
name="contingencyPlan"
|
|||
|
|
value={formData.contingencyPlan}
|
|||
|
|
onChange={handleInputChange}
|
|||
|
|
rows={2}
|
|||
|
|
className="w-full px-3 py-1.5 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|||
|
|
placeholder="Risk gerçekleşirse uygulanacak acil durum planını açıklayın"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Review Date */}
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
Değerlendirme Tarihi
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="date"
|
|||
|
|
name="reviewDate"
|
|||
|
|
value={formData.reviewDate}
|
|||
|
|
onChange={handleInputChange}
|
|||
|
|
className="w-full px-3 py-1.5 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Actions */}
|
|||
|
|
<div className="flex items-center justify-end space-x-2 pt-4">
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={handleClose}
|
|||
|
|
className="px-3 py-1.5 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md"
|
|||
|
|
>
|
|||
|
|
İptal
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
type="submit"
|
|||
|
|
className="flex items-center space-x-2 px-3 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md"
|
|||
|
|
>
|
|||
|
|
<FaSave className="w-4 h-4" />
|
|||
|
|
<span>{mode === "edit" ? "Güncelle" : "Ekle"}</span>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</form>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default RiskModal;
|