nr 구매 제한 기능 추가
- 트리거를 이용하여 기존 프로그램 사용자도 오류가 발생하도록 함
This commit is contained in:
507
Project/frontend/src/components/holiday/HolidayRequestDialog.tsx
Normal file
507
Project/frontend/src/components/holiday/HolidayRequestDialog.tsx
Normal file
@@ -0,0 +1,507 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { X, Save, Calendar, Clock, MapPin, User, FileText, AlertCircle } from 'lucide-react';
|
||||
import { comms } from '@/communication';
|
||||
import { HolidayRequest, CommonCode } from '@/types';
|
||||
|
||||
interface HolidayRequestDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: () => void;
|
||||
initialData?: HolidayRequest | null;
|
||||
userLevel: number;
|
||||
currentUserId: string;
|
||||
currentUserName: string;
|
||||
}
|
||||
|
||||
export function HolidayRequestDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
initialData,
|
||||
userLevel,
|
||||
currentUserId,
|
||||
// currentUserName
|
||||
}: HolidayRequestDialogProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [codes, setCodes] = useState<{
|
||||
cate: CommonCode[];
|
||||
reason: CommonCode[];
|
||||
location: CommonCode[];
|
||||
backup: CommonCode[];
|
||||
}>({
|
||||
cate: [],
|
||||
reason: [],
|
||||
location: [],
|
||||
backup: []
|
||||
});
|
||||
const [users, setUsers] = useState<Array<{ id: string; name: string }>>([]);
|
||||
|
||||
// Form State
|
||||
const [formData, setFormData] = useState<HolidayRequest>({
|
||||
idx: 0,
|
||||
gcode: '',
|
||||
uid: currentUserId,
|
||||
cate: '',
|
||||
sdate: new Date().toISOString().split('T')[0],
|
||||
edate: new Date().toISOString().split('T')[0],
|
||||
Remark: '',
|
||||
wuid: currentUserId,
|
||||
wdate: '',
|
||||
Response: '',
|
||||
conf: 0,
|
||||
HolyReason: '',
|
||||
HolyBackup: '',
|
||||
HolyLocation: '',
|
||||
HolyDays: 0,
|
||||
HolyTimes: 0,
|
||||
stime: '09:00',
|
||||
etime: '18:00',
|
||||
sendmail: false
|
||||
});
|
||||
|
||||
const [requestType, setRequestType] = useState<'day' | 'time' | 'out'>('day'); // day: 휴가, time: 대체, out: 외출
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
loadCodes();
|
||||
if (userLevel >= 5) {
|
||||
loadUsers();
|
||||
}
|
||||
|
||||
if (initialData) {
|
||||
setFormData({ ...initialData });
|
||||
// Determine request type based on data
|
||||
if (initialData.cate === '외출') {
|
||||
setRequestType('out');
|
||||
} else if (initialData.cate === '대체') {
|
||||
setRequestType('time');
|
||||
} else {
|
||||
setRequestType('day');
|
||||
}
|
||||
} else {
|
||||
// Reset form for new entry
|
||||
setFormData({
|
||||
idx: 0,
|
||||
gcode: '',
|
||||
uid: currentUserId,
|
||||
cate: '',
|
||||
sdate: new Date().toISOString().split('T')[0],
|
||||
edate: new Date().toISOString().split('T')[0],
|
||||
Remark: '',
|
||||
wuid: currentUserId,
|
||||
wdate: '',
|
||||
Response: '',
|
||||
conf: 0,
|
||||
HolyReason: '',
|
||||
HolyBackup: '',
|
||||
HolyLocation: '',
|
||||
HolyDays: 0,
|
||||
HolyTimes: 0,
|
||||
stime: '09:00',
|
||||
etime: '18:00',
|
||||
sendmail: false
|
||||
});
|
||||
setRequestType('day');
|
||||
}
|
||||
}
|
||||
}, [isOpen, initialData, currentUserId]);
|
||||
|
||||
const loadCodes = async () => {
|
||||
try {
|
||||
const [cateRes, reasonRes, locationRes, backupRes] = await Promise.all([
|
||||
comms.getCommonList('50'),
|
||||
comms.getCommonList('51'),
|
||||
comms.getCommonList('52'),
|
||||
comms.getCommonList('53')
|
||||
]);
|
||||
setCodes({
|
||||
cate: cateRes || [],
|
||||
reason: reasonRes || [],
|
||||
location: locationRes || [],
|
||||
backup: backupRes || []
|
||||
});
|
||||
|
||||
// Set default category if new
|
||||
if (!initialData && cateRes && cateRes.length > 0) {
|
||||
setFormData(prev => ({ ...prev, cate: cateRes[0].svalue }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load codes:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadUsers = async () => {
|
||||
try {
|
||||
const userList = await comms.getUserList('');
|
||||
if (userList && userList.length > 0) {
|
||||
const mappedUsers = userList.map((u: any) => ({
|
||||
id: u.id || u.Id,
|
||||
name: u.name || u.NameK || u.id
|
||||
}));
|
||||
setUsers(mappedUsers);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load users:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
// Validation
|
||||
if (!formData.cate && requestType === 'day') {
|
||||
alert('구분을 선택하세요.');
|
||||
return;
|
||||
}
|
||||
if (formData.sdate > formData.edate) {
|
||||
alert('종료일이 시작일보다 빠를 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare data based on type
|
||||
const dataToSave = { ...formData };
|
||||
if (requestType === 'out') {
|
||||
dataToSave.cate = '외출';
|
||||
dataToSave.HolyDays = 0;
|
||||
// Calculate times if needed, or rely on user input?
|
||||
// WinForms doesn't seem to auto-calc times for 'out', just saves stime/etime.
|
||||
} else if (requestType === 'time') {
|
||||
dataToSave.cate = '대체';
|
||||
dataToSave.HolyDays = 0;
|
||||
} else {
|
||||
// Day
|
||||
dataToSave.HolyTimes = 0;
|
||||
dataToSave.stime = '';
|
||||
dataToSave.etime = '';
|
||||
|
||||
// Calculate days
|
||||
const start = new Date(dataToSave.sdate);
|
||||
const end = new Date(dataToSave.edate);
|
||||
const diffTime = Math.abs(end.getTime() - start.getTime());
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
|
||||
dataToSave.HolyDays = diffDays;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await comms.saveHolidayRequest(dataToSave);
|
||||
if (response.Success) {
|
||||
onSave();
|
||||
onClose();
|
||||
} else {
|
||||
alert('저장 실패: ' + response.Message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Save error:', error);
|
||||
alert('저장 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
|
||||
<div className="bg-white rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-100">
|
||||
<h2 className="text-xl font-bold text-gray-800">
|
||||
{formData.idx === 0 ? '휴가/외출 신청' : '신청 내역 수정'}
|
||||
</h2>
|
||||
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-full transition-colors">
|
||||
<X className="w-5 h-5 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Request Type */}
|
||||
<div className="flex gap-4 p-4 bg-gray-50 rounded-lg">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="type"
|
||||
checked={requestType === 'day'}
|
||||
onChange={() => setRequestType('day')}
|
||||
className="w-4 h-4 text-blue-600"
|
||||
disabled={formData.idx > 0 && initialData?.cate !== '대체' && initialData?.cate !== '외출'}
|
||||
/>
|
||||
<span className="font-medium text-gray-700">일반휴가</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="type"
|
||||
checked={requestType === 'time'}
|
||||
onChange={() => setRequestType('time')}
|
||||
className="w-4 h-4 text-blue-600"
|
||||
disabled={formData.idx > 0 && initialData?.cate !== '대체'}
|
||||
/>
|
||||
<span className="font-medium text-gray-700">대체휴가(시간)</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="type"
|
||||
checked={requestType === 'out'}
|
||||
onChange={() => setRequestType('out')}
|
||||
className="w-4 h-4 text-blue-600"
|
||||
disabled={formData.idx > 0 && initialData?.cate !== '외출'}
|
||||
/>
|
||||
<span className="font-medium text-gray-700">외출</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* User Selection (Admin only) */}
|
||||
{userLevel >= 5 && (
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
|
||||
<User className="w-4 h-4" /> 신청자
|
||||
</label>
|
||||
<select
|
||||
value={formData.uid}
|
||||
onChange={(e) => setFormData({ ...formData, uid: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
disabled={formData.idx > 0}
|
||||
>
|
||||
{users.map(user => (
|
||||
<option key={user.id} value={user.id}>{user.name} ({user.id})</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Date & Time */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4" /> 시작일
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={formData.sdate}
|
||||
onChange={(e) => setFormData({ ...formData, sdate: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4" /> 종료일
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={formData.edate}
|
||||
onChange={(e) => setFormData({ ...formData, edate: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(requestType === 'time' || requestType === 'out') && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" /> 시작 시간
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
value={formData.stime}
|
||||
onChange={(e) => setFormData({ ...formData, stime: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" /> 종료 시간
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
value={formData.etime}
|
||||
onChange={(e) => setFormData({ ...formData, etime: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Category & Reason */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
|
||||
<FileText className="w-4 h-4" /> 구분
|
||||
</label>
|
||||
{requestType === 'day' ? (
|
||||
<select
|
||||
value={formData.cate}
|
||||
onChange={(e) => setFormData({ ...formData, cate: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
{codes.cate.map(code => (
|
||||
<option key={code.code} value={code.svalue}>{code.svalue}</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={requestType === 'out' ? '외출' : '대체'}
|
||||
disabled
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-gray-100 text-gray-500"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4" /> 사유
|
||||
</label>
|
||||
<select
|
||||
value={formData.HolyReason}
|
||||
onChange={(e) => setFormData({ ...formData, HolyReason: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="">선택하세요</option>
|
||||
{codes.reason.map(code => (
|
||||
<option key={code.code} value={code.svalue}>{code.svalue}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Location & Backup */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4" /> 행선지
|
||||
</label>
|
||||
<select
|
||||
value={formData.HolyLocation}
|
||||
onChange={(e) => setFormData({ ...formData, HolyLocation: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="">선택하세요</option>
|
||||
{codes.location.map(code => (
|
||||
<option key={code.code} value={code.svalue}>{code.svalue}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
|
||||
<User className="w-4 h-4" /> 업무대행
|
||||
</label>
|
||||
<select
|
||||
value={formData.HolyBackup}
|
||||
onChange={(e) => setFormData({ ...formData, HolyBackup: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="">선택하세요</option>
|
||||
{codes.backup.map(code => (
|
||||
<option key={code.code} value={code.svalue}>{code.svalue}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Days & Times (Manual Override) */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">일수</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.5"
|
||||
value={formData.HolyDays}
|
||||
onChange={(e) => setFormData({ ...formData, HolyDays: parseFloat(e.target.value) })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
disabled={requestType !== 'day'}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">시간</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.5"
|
||||
value={formData.HolyTimes}
|
||||
onChange={(e) => setFormData({ ...formData, HolyTimes: parseFloat(e.target.value) })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
disabled={requestType === 'day'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Remark */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">비고</label>
|
||||
<textarea
|
||||
value={formData.Remark}
|
||||
onChange={(e) => setFormData({ ...formData, Remark: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent h-20 resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Admin Response & Confirmation */}
|
||||
{userLevel >= 5 && (
|
||||
<div className="p-4 bg-blue-50 rounded-lg space-y-4 border border-blue-100">
|
||||
<h3 className="font-semibold text-blue-800">관리자 승인</h3>
|
||||
<div className="flex gap-4">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="conf"
|
||||
checked={formData.conf === 0}
|
||||
onChange={() => setFormData({ ...formData, conf: 0 })}
|
||||
className="w-4 h-4 text-blue-600"
|
||||
/>
|
||||
<span className="text-gray-700">미승인</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="conf"
|
||||
checked={formData.conf === 1}
|
||||
onChange={() => setFormData({ ...formData, conf: 1 })}
|
||||
className="w-4 h-4 text-green-600"
|
||||
/>
|
||||
<span className="text-gray-700">승인</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="conf"
|
||||
checked={formData.conf === 2}
|
||||
onChange={() => setFormData({ ...formData, conf: 2 })}
|
||||
className="w-4 h-4 text-red-600"
|
||||
/>
|
||||
<span className="text-gray-700">반려</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">관리자 메모</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.Response}
|
||||
onChange={(e) => setFormData({ ...formData, Response: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-gray-100 bg-gray-50 rounded-b-xl">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-gray-600 hover:bg-gray-200 rounded-lg transition-colors font-medium"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-2 px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg shadow-lg shadow-blue-500/30 transition-all font-medium disabled:opacity-50"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
{loading ? '저장 중...' : '저장'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -319,7 +319,7 @@ export function JobReportDayDialog({ isOpen, onClose, initialMonth }: JobReportD
|
||||
);
|
||||
})}
|
||||
<td className="px-3 py-2 text-center text-sm text-white border border-white/10 font-medium whitespace-nowrap">
|
||||
{row.totalHrs}+{row.totalOt}(*{row.totalHolidayOt})
|
||||
{row.totalHrs.toFixed(1)}+{row.totalOt.toFixed(1)}(*{row.totalHolidayOt.toFixed(1)})
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
|
||||
@@ -95,12 +95,15 @@ export function JobreportEditModal({
|
||||
try {
|
||||
// WebSocket 모드에서는 같은 응답타입을 사용하므로 순차적으로 로드
|
||||
const requestPart = await comms.getCommonList('13'); // 요청부서
|
||||
if (requestPart) requestPart.sort((a, b) => (a.memo || a.svalue || '').localeCompare(b.memo || b.svalue || ''));
|
||||
setRequestPartList(requestPart || []);
|
||||
|
||||
const packages = await comms.getCommonList('14'); // 패키지
|
||||
if (packages) packages.sort((a, b) => (a.memo || a.svalue || '').localeCompare(b.memo || b.svalue || ''));
|
||||
setPackageList(packages || []);
|
||||
|
||||
const processes = await comms.getCommonList('16'); // 공정(프로세스)
|
||||
if (processes) processes.sort((a, b) => (a.memo || a.svalue || '').localeCompare(b.memo || b.svalue || ''));
|
||||
setProcessList(processes || []);
|
||||
|
||||
const statuses = await comms.getCommonList('12'); // 상태
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
User,
|
||||
Users,
|
||||
CalendarDays,
|
||||
Calendar,
|
||||
Mail,
|
||||
Shield,
|
||||
List,
|
||||
@@ -81,6 +82,7 @@ const leftDropdownMenus: DropdownMenuConfig[] = [
|
||||
items: [
|
||||
{ type: 'link', path: '/kuntae', icon: List, label: '목록' },
|
||||
{ type: 'action', icon: AlertTriangle, label: '오류검사', action: 'kuntaeErrorCheck' },
|
||||
{ type: 'link', path: '/holiday-request', icon: Calendar, label: '휴가/외출 신청' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user