erp-platform/ui/src/views/project/components/RiskModal.tsx

366 lines
12 KiB
TypeScript
Raw Normal View History

2025-09-15 09:31:47 +00:00
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">
ı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ıı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;