init: SR System — Helpdesk & ITAM Portal
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
'use client';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { X, Search } from 'lucide-react';
|
||||
import projectService from '@/services/projectService';
|
||||
import employeeService from '@/services/employeeService';
|
||||
import type { Project, EmployeeSearchResult } from '@/types';
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSaved: () => void;
|
||||
editData?: Project | null;
|
||||
}
|
||||
|
||||
export default function ProjectFormModal({ isOpen, onClose, onSaved, editData }: Props) {
|
||||
const isEdit = !!editData;
|
||||
|
||||
const [projectName, setProjectName] = useState('');
|
||||
const [projectType, setProjectType] = useState<string>('SI');
|
||||
const [contractCompany, setContractCompany] = useState('');
|
||||
const [endUserCompany, setEndUserCompany] = useState('');
|
||||
const [contractType, setContractType] = useState<string>('TURN_KEY');
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
const [workLocationType, setWorkLocationType] = useState<string>('ON_SITE');
|
||||
const [workAddress, setWorkAddress] = useState('');
|
||||
const [techEnvironment, setTechEnvironment] = useState('');
|
||||
const [mainTaskDesc, setMainTaskDesc] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Internal manager search
|
||||
const [mgrSearch, setMgrSearch] = useState('');
|
||||
const [mgrResults, setMgrResults] = useState<EmployeeSearchResult[]>([]);
|
||||
const [selectedMgr, setSelectedMgr] = useState<EmployeeSearchResult | null>(null);
|
||||
const [showMgrDropdown, setShowMgrDropdown] = useState(false);
|
||||
const debounceRef = useRef<number | undefined>(undefined);
|
||||
|
||||
// Pre-populate on edit
|
||||
useEffect(() => {
|
||||
if (!editData) {
|
||||
resetForm();
|
||||
return;
|
||||
}
|
||||
setProjectName(editData.projectName || '');
|
||||
setProjectType(editData.projectType || 'SI');
|
||||
setContractCompany(editData.contractCompany || '');
|
||||
setEndUserCompany(editData.endUserCompany || '');
|
||||
setContractType(editData.contractType || 'TURN_KEY');
|
||||
setStartDate(editData.startDate || '');
|
||||
setEndDate(editData.endDate || '');
|
||||
setWorkLocationType(editData.workLocationType || 'ON_SITE');
|
||||
setWorkAddress(editData.workAddress || '');
|
||||
setTechEnvironment(editData.techEnvironment || '');
|
||||
setMainTaskDesc(editData.mainTaskDesc || '');
|
||||
if (editData.internalManager) {
|
||||
setSelectedMgr(editData.internalManager as EmployeeSearchResult);
|
||||
setMgrSearch(editData.internalManager.name);
|
||||
}
|
||||
}, [editData, isOpen]);
|
||||
|
||||
const resetForm = () => {
|
||||
setProjectName('');
|
||||
setProjectType('SI');
|
||||
setContractCompany('');
|
||||
setEndUserCompany('');
|
||||
setContractType('TURN_KEY');
|
||||
setStartDate('');
|
||||
setEndDate('');
|
||||
setWorkLocationType('ON_SITE');
|
||||
setWorkAddress('');
|
||||
setTechEnvironment('');
|
||||
setMainTaskDesc('');
|
||||
setMgrSearch('');
|
||||
setSelectedMgr(null);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleMgrSearch = (val: string) => {
|
||||
setMgrSearch(val);
|
||||
setShowMgrDropdown(true);
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||
debounceRef.current = window.setTimeout(() => {
|
||||
if (val.trim().length >= 1) {
|
||||
employeeService.search(val.trim()).then((res) => {
|
||||
setMgrResults(res.data || []);
|
||||
}).catch(() => {});
|
||||
} else {
|
||||
setMgrResults([]);
|
||||
}
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!projectName.trim() || !contractCompany.trim() || !startDate) {
|
||||
setError('Project name, contract company, and start date are required.');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
projectName: projectName.trim(),
|
||||
projectType,
|
||||
contractCompany: contractCompany.trim(),
|
||||
endUserCompany: endUserCompany.trim() || undefined,
|
||||
contractType,
|
||||
startDate,
|
||||
endDate: endDate || undefined,
|
||||
workLocationType,
|
||||
workAddress: workAddress.trim() || undefined,
|
||||
techEnvironment: techEnvironment.trim() || undefined,
|
||||
mainTaskDesc: mainTaskDesc.trim() || undefined,
|
||||
internalManagerId: selectedMgr?.empId || null,
|
||||
};
|
||||
if (isEdit) {
|
||||
await projectService.update(editData!.projectId, payload);
|
||||
} else {
|
||||
await projectService.create(payload);
|
||||
}
|
||||
onSaved();
|
||||
onClose();
|
||||
resetForm();
|
||||
} catch {
|
||||
setError('Failed to save project. Please try again.');
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={onClose}>
|
||||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-xl mx-4 max-h-[90vh] overflow-y-auto" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-between p-6 border-b border-border">
|
||||
<h3 className="font-outfit font-semibold text-lg text-text-main">
|
||||
{isEdit ? 'Edit Project' : 'New Project'}
|
||||
</h3>
|
||||
<button onClick={onClose} className="p-1 hover:bg-gray-100 rounded-lg transition-colors">
|
||||
<X className="w-5 h-5 text-text-muted" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-text-muted mb-1.5">
|
||||
Project Name <span className="text-status-critical-text">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={projectName}
|
||||
onChange={(e) => setProjectName(e.target.value)}
|
||||
placeholder="e.g. Samsung Pay SI"
|
||||
className="w-full px-3 py-2 rounded-xl border border-border text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-text-muted mb-1.5">Project Type</label>
|
||||
<select
|
||||
value={projectType}
|
||||
onChange={(e) => setProjectType(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-xl border border-border text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
>
|
||||
<option value="SI">SI</option>
|
||||
<option value="SM">SM</option>
|
||||
<option value="MIGRATION">MIGRATION</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-text-muted mb-1.5">Contract Type</label>
|
||||
<select
|
||||
value={contractType}
|
||||
onChange={(e) => setContractType(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-xl border border-border text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
>
|
||||
<option value="TURN_KEY">TURN_KEY</option>
|
||||
<option value="MM">MM</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-text-muted mb-1.5">
|
||||
Contract Company <span className="text-status-critical-text">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={contractCompany}
|
||||
onChange={(e) => setContractCompany(e.target.value)}
|
||||
placeholder="e.g. Samsung Electronics"
|
||||
className="w-full px-3 py-2 rounded-xl border border-border text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-text-muted mb-1.5">End User Company</label>
|
||||
<input
|
||||
type="text"
|
||||
value={endUserCompany}
|
||||
onChange={(e) => setEndUserCompany(e.target.value)}
|
||||
placeholder="e.g. Samsung SDS"
|
||||
className="w-full px-3 py-2 rounded-xl border border-border text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-text-muted mb-1.5">
|
||||
Start Date <span className="text-status-critical-text">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-xl border border-border text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-text-muted mb-1.5">End Date</label>
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-xl border border-border text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-text-muted mb-1.5">Work Location Type</label>
|
||||
<select
|
||||
value={workLocationType}
|
||||
onChange={(e) => setWorkLocationType(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-xl border border-border text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
>
|
||||
<option value="ON_SITE">ON_SITE</option>
|
||||
<option value="REMOTE">REMOTE</option>
|
||||
<option value="HYBRID">HYBRID</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-text-muted mb-1.5">Work Address</label>
|
||||
<input
|
||||
type="text"
|
||||
value={workAddress}
|
||||
onChange={(e) => setWorkAddress(e.target.value)}
|
||||
placeholder="e.g. Seoul, Gangnam-gu"
|
||||
className="w-full px-3 py-2 rounded-xl border border-border text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-text-muted mb-1.5">Tech Environment</label>
|
||||
<input
|
||||
type="text"
|
||||
value={techEnvironment}
|
||||
onChange={(e) => setTechEnvironment(e.target.value)}
|
||||
placeholder="e.g. Java, Spring Boot, React, PostgreSQL"
|
||||
className="w-full px-3 py-2 rounded-xl border border-border text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-text-muted mb-1.5">Main Task Description</label>
|
||||
<textarea
|
||||
value={mainTaskDesc}
|
||||
onChange={(e) => setMainTaskDesc(e.target.value)}
|
||||
placeholder="Describe the main tasks and goals of the project..."
|
||||
className="w-full px-3 py-2 rounded-xl border border-border text-sm focus:outline-none focus:ring-2 focus:ring-primary resize-none"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Internal Manager Search */}
|
||||
<div className="relative">
|
||||
<label className="block text-xs font-medium text-text-muted mb-1.5">Internal Manager</label>
|
||||
<div className="relative">
|
||||
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-text-muted" />
|
||||
<input
|
||||
type="text"
|
||||
value={mgrSearch}
|
||||
onChange={(e) => handleMgrSearch(e.target.value)}
|
||||
onFocus={() => setShowMgrDropdown(true)}
|
||||
placeholder="Search manager by name..."
|
||||
className="w-full pl-9 pr-3 py-2 rounded-xl border border-border text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
{showMgrDropdown && mgrResults.length > 0 && (
|
||||
<div className="absolute z-10 w-full mt-1 bg-white border border-border rounded-xl shadow-lg max-h-48 overflow-y-auto">
|
||||
{mgrResults.map((emp) => (
|
||||
<button
|
||||
key={emp.empId}
|
||||
type="button"
|
||||
onClick={() => { setSelectedMgr(emp); setMgrSearch(emp.name); setShowMgrDropdown(false); }}
|
||||
className="w-full text-left px-3 py-2 text-sm hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
{emp.name} <span className="text-text-muted text-xs">({emp.email})</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{selectedMgr && !showMgrDropdown && (
|
||||
<p className="text-xs text-primary mt-1">Selected: {selectedMgr.name}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-status-critical-bg border border-status-critical text-status-critical-text rounded-xl px-4 py-3 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<button type="button" onClick={onClose} className="px-4 py-2 rounded-xl text-sm font-medium text-text-muted hover:bg-gray-100 transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !projectName.trim() || !contractCompany.trim() || !startDate}
|
||||
className="px-4 py-2 rounded-xl text-sm font-semibold bg-primary text-white hover:bg-primary-hover transition-colors disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Saving...' : isEdit ? 'Save Changes' : 'Create Project'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user