init: SR System — Helpdesk & ITAM Portal

This commit is contained in:
pray4skylark
2026-06-02 13:50:39 +09:00
commit 832c9f096f
262 changed files with 30470 additions and 0 deletions
@@ -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>
);
}