296 lines
14 KiB
TypeScript
296 lines
14 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { X, Save, Calendar, Clock, FileText, User } from 'lucide-react';
|
|
import { KuntaeModel } from '@/types';
|
|
|
|
interface KuntaeEditModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onSave: (data: KuntaeFormData) => Promise<void>;
|
|
initialData?: KuntaeModel | null;
|
|
mode: 'add' | 'edit' | 'copy';
|
|
}
|
|
|
|
export interface KuntaeFormData {
|
|
idx?: number;
|
|
cate: string;
|
|
sdate: string;
|
|
edate: string;
|
|
term: number;
|
|
crtime: number;
|
|
termDr: number;
|
|
drTime: number;
|
|
contents: string;
|
|
uid: string;
|
|
}
|
|
|
|
const CATE_OPTIONS = ['연차', '대체', '공가', '경조', '병가', '오전반차', '오후반차', '조퇴', '외출', '지각', '결근', '휴직', '교육', '출장', '재택', '특근', '당직', '기타'];
|
|
|
|
export function KuntaeEditModal({ isOpen, onClose, onSave, initialData, mode }: KuntaeEditModalProps) {
|
|
const [formData, setFormData] = useState<KuntaeFormData>({
|
|
cate: '연차',
|
|
sdate: new Date().toISOString().split('T')[0],
|
|
edate: new Date().toISOString().split('T')[0],
|
|
term: 1,
|
|
crtime: 0,
|
|
termDr: 0,
|
|
drTime: 0,
|
|
contents: '',
|
|
uid: '',
|
|
});
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
if (initialData) {
|
|
setFormData({
|
|
idx: mode === 'edit' ? initialData.idx : undefined,
|
|
cate: initialData.cate || '연차',
|
|
sdate: initialData.sdate || new Date().toISOString().split('T')[0],
|
|
edate: initialData.edate || new Date().toISOString().split('T')[0],
|
|
term: initialData.term || 0,
|
|
crtime: initialData.crtime || 0,
|
|
termDr: initialData.termDr || 0,
|
|
drTime: initialData.DrTime || 0,
|
|
contents: initialData.contents || '',
|
|
uid: initialData.uid || '',
|
|
});
|
|
} else {
|
|
// 초기화
|
|
setFormData({
|
|
cate: '연차',
|
|
sdate: new Date().toISOString().split('T')[0],
|
|
edate: new Date().toISOString().split('T')[0],
|
|
term: 1,
|
|
crtime: 0,
|
|
termDr: 0,
|
|
drTime: 0,
|
|
contents: '',
|
|
uid: '', // 상위 컴포넌트에서 현재 사용자 ID를 주입받거나 여기서 처리해야 함
|
|
});
|
|
}
|
|
}
|
|
}, [isOpen, initialData, mode]);
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
|
const { name, value } = e.target;
|
|
setFormData(prev => ({
|
|
...prev,
|
|
[name]: name === 'term' || name === 'crtime' || name === 'termDr' || name === 'drTime'
|
|
? parseFloat(value) || 0
|
|
: value
|
|
}));
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setSaving(true);
|
|
try {
|
|
await onSave(formData);
|
|
onClose();
|
|
} catch (error) {
|
|
console.error('Save error:', error);
|
|
alert('저장 중 오류가 발생했습니다.');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
// 날짜 변경 시 기간 자동 계산 (단순 1일 차이)
|
|
useEffect(() => {
|
|
if (formData.sdate && formData.edate) {
|
|
const start = new Date(formData.sdate);
|
|
const end = new Date(formData.edate);
|
|
const diffTime = Math.abs(end.getTime() - start.getTime());
|
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
|
|
|
|
// 연차/휴가 등일 때만 자동 계산
|
|
if (['연차', '공가', '경조', '병가', '교육', '출장'].includes(formData.cate)) {
|
|
setFormData(prev => ({ ...prev, term: diffDays }));
|
|
}
|
|
}
|
|
}, [formData.sdate, formData.edate, formData.cate]);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
const title = mode === 'add' ? '근태 등록' : mode === 'edit' ? '근태 수정' : '근태 복사 등록';
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in">
|
|
<div className="bg-[#1e1e2e] rounded-2xl shadow-2xl w-full max-w-lg border border-white/10 overflow-hidden">
|
|
{/* 헤더 */}
|
|
<div className="px-6 py-4 border-b border-white/10 flex justify-between items-center bg-white/5">
|
|
<h2 className="text-xl font-bold text-white flex items-center">
|
|
<Calendar className="w-5 h-5 mr-2 text-primary-400" />
|
|
{title}
|
|
</h2>
|
|
<button onClick={onClose} className="text-white/50 hover:text-white transition-colors">
|
|
<X className="w-6 h-6" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* 폼 */}
|
|
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
|
|
|
{/* 구분 및 사용자 */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-white/70 text-sm font-medium mb-1">구분</label>
|
|
<select
|
|
name="cate"
|
|
value={formData.cate}
|
|
onChange={handleChange}
|
|
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
|
|
>
|
|
{CATE_OPTIONS.map(opt => (
|
|
<option key={opt} value={opt} className="bg-[#1e1e2e]">{opt}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-white/70 text-sm font-medium mb-1">사용자 ID</label>
|
|
<div className="relative">
|
|
<User className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-white/50" />
|
|
<input
|
|
type="text"
|
|
name="uid"
|
|
value={formData.uid}
|
|
onChange={handleChange}
|
|
readOnly={mode === 'edit'} // 수정 시에는 사용자 변경 불가
|
|
className={`w-full bg-white/5 border border-white/10 rounded-lg pl-10 pr-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 ${mode === 'edit' ? 'opacity-50 cursor-not-allowed' : ''}`}
|
|
placeholder="사번 입력"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 기간 */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-white/70 text-sm font-medium mb-1">시작일</label>
|
|
<input
|
|
type="date"
|
|
name="sdate"
|
|
value={formData.sdate}
|
|
onChange={handleChange}
|
|
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-white/70 text-sm font-medium mb-1">종료일</label>
|
|
<input
|
|
type="date"
|
|
name="edate"
|
|
value={formData.edate}
|
|
onChange={handleChange}
|
|
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 사용량 (일/시간) */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-white/70 text-sm font-medium mb-1">사용 (일)</label>
|
|
<div className="relative">
|
|
<Calendar className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-white/50" />
|
|
<input
|
|
type="number"
|
|
name="term"
|
|
value={formData.term}
|
|
onChange={handleChange}
|
|
step="0.5"
|
|
className="w-full bg-white/5 border border-white/10 rounded-lg pl-10 pr-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-white/70 text-sm font-medium mb-1">사용 (시간)</label>
|
|
<div className="relative">
|
|
<Clock className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-white/50" />
|
|
<input
|
|
type="number"
|
|
name="crtime"
|
|
value={formData.crtime}
|
|
onChange={handleChange}
|
|
step="0.5"
|
|
className="w-full bg-white/5 border border-white/10 rounded-lg pl-10 pr-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 발생량 (일/시간) - 대체근무 등 발생 시 */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-white/70 text-sm font-medium mb-1">발생 (일)</label>
|
|
<input
|
|
type="number"
|
|
name="termDr"
|
|
value={formData.termDr}
|
|
onChange={handleChange}
|
|
step="0.5"
|
|
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-white/70 text-sm font-medium mb-1">발생 (시간)</label>
|
|
<input
|
|
type="number"
|
|
name="drTime"
|
|
value={formData.drTime}
|
|
onChange={handleChange}
|
|
step="0.5"
|
|
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 내용 */}
|
|
<div>
|
|
<label className="block text-white/70 text-sm font-medium mb-1">내용</label>
|
|
<div className="relative">
|
|
<FileText className="absolute left-3 top-3 w-4 h-4 text-white/50" />
|
|
<textarea
|
|
name="contents"
|
|
value={formData.contents}
|
|
onChange={handleChange}
|
|
rows={3}
|
|
className="w-full bg-white/5 border border-white/10 rounded-lg pl-10 pr-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 resize-none"
|
|
placeholder="근태 사유 또는 내용 입력"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 버튼 */}
|
|
<div className="flex justify-end space-x-3 pt-4 border-t border-white/10">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="px-4 py-2 rounded-lg text-white/70 hover:text-white hover:bg-white/10 transition-colors"
|
|
>
|
|
취소
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={saving}
|
|
className="bg-primary-500 hover:bg-primary-600 text-white px-6 py-2 rounded-lg transition-colors flex items-center disabled:opacity-50"
|
|
>
|
|
{saving ? (
|
|
<>
|
|
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2" />
|
|
저장 중...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Save className="w-4 h-4 mr-2" />
|
|
저장
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|