420 lines
15 KiB
TypeScript
420 lines
15 KiB
TypeScript
|
|
import React, { useState } from "react";
|
|||
|
|
import { FaTimes, FaSave, FaPlus, FaMinus } from "react-icons/fa";
|
|||
|
|
import { TeamRoleEnum } from "../../../types/common";
|
|||
|
|
import MultiSelectEmployee from "../../../components/common/MultiSelectEmployee";
|
|||
|
|
import { mockEmployees } from "../../../mocks/mockEmployees";
|
|||
|
|
import { Team, TeamMember } from "../../../types/common";
|
|||
|
|
|
|||
|
|
interface NewTeamModalProps {
|
|||
|
|
isOpen: boolean;
|
|||
|
|
onClose: () => void;
|
|||
|
|
onSave: (team: Partial<Team>) => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const NewTeamModal: React.FC<NewTeamModalProps> = ({
|
|||
|
|
isOpen,
|
|||
|
|
onClose,
|
|||
|
|
onSave,
|
|||
|
|
}) => {
|
|||
|
|
const [teamData, setTeamData] = useState<Partial<Team>>({
|
|||
|
|
code: "",
|
|||
|
|
name: "",
|
|||
|
|
description: "",
|
|||
|
|
isActive: true,
|
|||
|
|
specializations: [],
|
|||
|
|
members: [],
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const [selectedEmployees, setSelectedEmployees] = useState<string[]>([]);
|
|||
|
|
const [newSpecialization, setNewSpecialization] = useState("");
|
|||
|
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|||
|
|
|
|||
|
|
const validateForm = () => {
|
|||
|
|
const newErrors: Record<string, string> = {};
|
|||
|
|
|
|||
|
|
if (!teamData.code?.trim()) {
|
|||
|
|
newErrors.teamCode = "Ekip kodu gerekli";
|
|||
|
|
}
|
|||
|
|
if (!teamData.name?.trim()) {
|
|||
|
|
newErrors.teamName = "Ekip adı gerekli";
|
|||
|
|
}
|
|||
|
|
if (!teamData.description?.trim()) {
|
|||
|
|
newErrors.description = "Açıklama gerekli";
|
|||
|
|
}
|
|||
|
|
if (!teamData.managerId) {
|
|||
|
|
newErrors.managerId = "Ekip yöneticisi seçilmeli";
|
|||
|
|
}
|
|||
|
|
if (selectedEmployees.length === 0) {
|
|||
|
|
newErrors.members = "En az bir ekip üyesi seçilmeli";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setErrors(newErrors);
|
|||
|
|
return Object.keys(newErrors).length === 0;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleInputChange = (
|
|||
|
|
field: keyof Team,
|
|||
|
|
value: string | boolean | string[]
|
|||
|
|
) => {
|
|||
|
|
setTeamData((prev) => ({
|
|||
|
|
...prev,
|
|||
|
|
[field]: value,
|
|||
|
|
}));
|
|||
|
|
// Clear error when user starts typing
|
|||
|
|
if (errors[field]) {
|
|||
|
|
setErrors((prev) => ({
|
|||
|
|
...prev,
|
|||
|
|
[field]: "",
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const addSpecialization = () => {
|
|||
|
|
if (
|
|||
|
|
newSpecialization.trim() &&
|
|||
|
|
!teamData.specializations?.includes(newSpecialization.trim())
|
|||
|
|
) {
|
|||
|
|
setTeamData((prev) => ({
|
|||
|
|
...prev,
|
|||
|
|
specializations: [
|
|||
|
|
...(prev.specializations || []),
|
|||
|
|
newSpecialization.trim(),
|
|||
|
|
],
|
|||
|
|
}));
|
|||
|
|
setNewSpecialization("");
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const removeSpecialization = (index: number) => {
|
|||
|
|
setTeamData((prev) => ({
|
|||
|
|
...prev,
|
|||
|
|
specializations:
|
|||
|
|
prev.specializations?.filter((_, i) => i !== index) || [],
|
|||
|
|
}));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleEmployeeSelection = (employees: string[]) => {
|
|||
|
|
setSelectedEmployees(employees);
|
|||
|
|
if (errors.members) {
|
|||
|
|
setErrors((prev) => ({
|
|||
|
|
...prev,
|
|||
|
|
members: "",
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleMemberRoleChange = (employeeName: string, role: TeamRoleEnum) => {
|
|||
|
|
const employee = mockEmployees.find((emp) => emp.fullName === employeeName);
|
|||
|
|
if (role === TeamRoleEnum.Lead && employee) {
|
|||
|
|
handleInputChange("managerId", employee.id);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleSave = () => {
|
|||
|
|
if (validateForm()) {
|
|||
|
|
const members: TeamMember[] = selectedEmployees.map(
|
|||
|
|
(employeeName, index) => {
|
|||
|
|
const employee = mockEmployees.find(
|
|||
|
|
(emp) => emp.fullName === employeeName
|
|||
|
|
);
|
|||
|
|
const isLeader = index === 0; // First selected employee becomes leader
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
id: `TM${Date.now()}-${index}`,
|
|||
|
|
teamId: "",
|
|||
|
|
employeeId: employee?.id || "",
|
|||
|
|
employee: employee,
|
|||
|
|
role: isLeader ? TeamRoleEnum.Lead : TeamRoleEnum.Member,
|
|||
|
|
joinDate: new Date(),
|
|||
|
|
isActive: true,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const teamId = `MT${Date.now()}`;
|
|||
|
|
|
|||
|
|
// Update members with the teamId
|
|||
|
|
members.forEach((member) => {
|
|||
|
|
member.teamId = teamId;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const teamToSave: Partial<Team> = {
|
|||
|
|
...teamData,
|
|||
|
|
id: teamId,
|
|||
|
|
managerId: members.find((m) => m.role === TeamRoleEnum.Lead)
|
|||
|
|
?.employeeId,
|
|||
|
|
members,
|
|||
|
|
creationTime: new Date(),
|
|||
|
|
lastModificationTime: new Date(),
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
onSave(teamToSave);
|
|||
|
|
onClose();
|
|||
|
|
|
|||
|
|
// Reset form
|
|||
|
|
setTeamData({
|
|||
|
|
code: "",
|
|||
|
|
name: "",
|
|||
|
|
description: "",
|
|||
|
|
isActive: true,
|
|||
|
|
specializations: [],
|
|||
|
|
members: [],
|
|||
|
|
});
|
|||
|
|
setSelectedEmployees([]);
|
|||
|
|
setNewSpecialization("");
|
|||
|
|
setErrors({});
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
isOpen && (
|
|||
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|||
|
|
<div className="bg-white rounded-lg w-full max-w-3xl max-h-[90vh] overflow-y-auto">
|
|||
|
|
{/* Header */}
|
|||
|
|
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
|||
|
|
<h2 className="text-lg font-semibold text-gray-900">
|
|||
|
|
Yeni Ekip Oluştur
|
|||
|
|
</h2>
|
|||
|
|
<button
|
|||
|
|
onClick={onClose}
|
|||
|
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
|||
|
|
>
|
|||
|
|
<FaTimes className="w-5 h-5" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Content */}
|
|||
|
|
<div className="p-4 space-y-4">
|
|||
|
|
{/* Basic Info */}
|
|||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|||
|
|
Ekip Kodu *
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={teamData.code || ""}
|
|||
|
|
onChange={(e) => handleInputChange("code", e.target.value)}
|
|||
|
|
className={`w-full px-2.5 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${
|
|||
|
|
errors.code ? "border-red-500" : "border-gray-300"
|
|||
|
|
}`}
|
|||
|
|
placeholder="MEC-001"
|
|||
|
|
/>
|
|||
|
|
{errors.code && (
|
|||
|
|
<p className="text-red-500 text-xs mt-1">{errors.code}</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|||
|
|
Ekip Adı *
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={teamData.name || ""}
|
|||
|
|
onChange={(e) => handleInputChange("name", e.target.value)}
|
|||
|
|
className={`w-full px-2.5 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${
|
|||
|
|
errors.name ? "border-red-500" : "border-gray-300"
|
|||
|
|
}`}
|
|||
|
|
placeholder="Mekanik Bakım Ekibi"
|
|||
|
|
/>
|
|||
|
|
{errors.name && (
|
|||
|
|
<p className="text-red-500 text-xs mt-1">{errors.name}</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Description */}
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|||
|
|
Açıklama *
|
|||
|
|
</label>
|
|||
|
|
<textarea
|
|||
|
|
value={teamData.description || ""}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
handleInputChange("description", e.target.value)
|
|||
|
|
}
|
|||
|
|
rows={2}
|
|||
|
|
className={`w-full px-2.5 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${
|
|||
|
|
errors.description ? "border-red-500" : "border-gray-300"
|
|||
|
|
}`}
|
|||
|
|
placeholder="Ekip açıklaması ve sorumlulukları"
|
|||
|
|
/>
|
|||
|
|
{errors.description && (
|
|||
|
|
<p className="text-red-500 text-xs mt-1">
|
|||
|
|
{errors.description}
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Team Members */}
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|||
|
|
Ekip Üyeleri *
|
|||
|
|
</label>
|
|||
|
|
<MultiSelectEmployee
|
|||
|
|
selectedEmployees={selectedEmployees}
|
|||
|
|
onChange={handleEmployeeSelection}
|
|||
|
|
placeholder="Ekip üyelerini seçin"
|
|||
|
|
className={errors.members ? "border-red-500" : ""}
|
|||
|
|
/>
|
|||
|
|
{errors.members && (
|
|||
|
|
<p className="text-red-500 text-xs mt-1">{errors.members}</p>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Selected Members Display */}
|
|||
|
|
{selectedEmployees.length > 0 && (
|
|||
|
|
<div className="mt-3 p-3 bg-gray-50 rounded-lg">
|
|||
|
|
<h4 className="text-sm font-medium text-gray-700 mb-2">
|
|||
|
|
Seçili Ekip Üyeleri:
|
|||
|
|
</h4>
|
|||
|
|
<div className="space-y-1.5">
|
|||
|
|
{selectedEmployees.map((employeeName, index) => {
|
|||
|
|
const employee = mockEmployees.find(
|
|||
|
|
(emp) => emp.fullName === employeeName
|
|||
|
|
);
|
|||
|
|
return (
|
|||
|
|
<div
|
|||
|
|
key={index}
|
|||
|
|
className="flex items-center justify-between p-2 bg-white rounded border"
|
|||
|
|
>
|
|||
|
|
<div className="flex items-center space-x-3">
|
|||
|
|
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
|||
|
|
<span className="text-sm font-medium text-blue-600">
|
|||
|
|
{employee?.firstName?.charAt(0)}
|
|||
|
|
{employee?.lastName?.charAt(0)}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<p className="text-sm font-medium text-gray-900">
|
|||
|
|
{employeeName}
|
|||
|
|
</p>
|
|||
|
|
<p className="text-sm text-gray-600">
|
|||
|
|
{employee?.jobPosition?.name}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex items-center space-x-2">
|
|||
|
|
<select
|
|||
|
|
value={
|
|||
|
|
teamData.managerId === employee?.id
|
|||
|
|
? TeamRoleEnum.Lead
|
|||
|
|
: TeamRoleEnum.Member
|
|||
|
|
}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
handleMemberRoleChange(
|
|||
|
|
employeeName,
|
|||
|
|
e.target.value as TeamRoleEnum
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
className="text-xs px-1.5 py-1 border border-gray-300 rounded"
|
|||
|
|
>
|
|||
|
|
<option value={TeamRoleEnum.Member}>Üye</option>
|
|||
|
|
<option value={TeamRoleEnum.Lead}>Lider</option>
|
|||
|
|
<option value={TeamRoleEnum.Specialist}>
|
|||
|
|
Uzman
|
|||
|
|
</option>
|
|||
|
|
<option value={TeamRoleEnum.Manager}>
|
|||
|
|
Yönetici
|
|||
|
|
</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Specializations */}
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|||
|
|
Uzmanlık Alanları
|
|||
|
|
</label>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<div className="flex space-x-1.5">
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={newSpecialization}
|
|||
|
|
onChange={(e) => setNewSpecialization(e.target.value)}
|
|||
|
|
className="flex-1 px-2.5 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|||
|
|
placeholder="Uzmanlık alanı ekle"
|
|||
|
|
onKeyPress={(e) => {
|
|||
|
|
if (e.key === "Enter") {
|
|||
|
|
e.preventDefault();
|
|||
|
|
addSpecialization();
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={addSpecialization}
|
|||
|
|
className="px-2.5 py-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
|||
|
|
>
|
|||
|
|
<FaPlus className="w-4 h-4" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
{teamData.specializations &&
|
|||
|
|
teamData.specializations.length > 0 && (
|
|||
|
|
<div className="flex flex-wrap gap-2">
|
|||
|
|
{teamData.specializations.map((spec, index) => (
|
|||
|
|
<span
|
|||
|
|
key={index}
|
|||
|
|
className="inline-flex items-center px-2.5 py-1 rounded-full text-xs bg-blue-100 text-blue-800"
|
|||
|
|
>
|
|||
|
|
{spec}
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => removeSpecialization(index)}
|
|||
|
|
className="ml-2 text-blue-600 hover:text-blue-800"
|
|||
|
|
>
|
|||
|
|
<FaMinus className="w-3 h-3" />
|
|||
|
|
</button>
|
|||
|
|
</span>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Status */}
|
|||
|
|
<div>
|
|||
|
|
<label className="flex items-center">
|
|||
|
|
<input
|
|||
|
|
type="checkbox"
|
|||
|
|
checked={teamData.isActive || false}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
handleInputChange("isActive", e.target.checked)
|
|||
|
|
}
|
|||
|
|
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
|||
|
|
/>
|
|||
|
|
<span className="ml-2 text-sm text-gray-700">Ekip aktif</span>
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Footer */}
|
|||
|
|
<div className="flex items-center justify-end space-x-2 p-4 border-t border-gray-200">
|
|||
|
|
<button
|
|||
|
|
onClick={onClose}
|
|||
|
|
className="px-3 py-1.5 text-sm text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
|||
|
|
>
|
|||
|
|
İptal
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={handleSave}
|
|||
|
|
className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2"
|
|||
|
|
>
|
|||
|
|
<FaSave className="w-4 h-4" />
|
|||
|
|
<span>Kaydet</span>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default NewTeamModal;
|