365 lines
12 KiB
TypeScript
365 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;
|