nr 구매 제한 기능 추가
- 트리거를 이용하여 기존 프로그램 사용자도 오류가 발생하도록 함
This commit is contained in:
@@ -7,6 +7,8 @@ import {
|
||||
Info,
|
||||
Plus,
|
||||
Calendar,
|
||||
AlertTriangle,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { comms } from '@/communication';
|
||||
import { JobReportItem, JobReportUser } from '@/types';
|
||||
@@ -43,6 +45,11 @@ export function Jobreport() {
|
||||
// 오늘 근무시간 상태
|
||||
const [todayWork, setTodayWork] = useState({ hrs: 0, ot: 0 });
|
||||
|
||||
// 미등록 업무일지 상태
|
||||
const [unregisteredJobReportCount, setUnregisteredJobReportCount] = useState(0);
|
||||
const [unregisteredJobReportDays, setUnregisteredJobReportDays] = useState<{ date: string; hrs: number }[]>([]);
|
||||
const [showUnregisteredModal, setShowUnregisteredModal] = useState(false);
|
||||
|
||||
// 날짜 포맷 헬퍼 함수 (로컬 시간 기준)
|
||||
const formatDateLocal = (date: Date) => {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
@@ -70,6 +77,55 @@ export function Jobreport() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 미등록 업무일지 로드
|
||||
const loadUnregisteredJobReports = useCallback(async (userId: string) => {
|
||||
try {
|
||||
const now = new Date();
|
||||
const todayStr = formatDateLocal(now);
|
||||
|
||||
// 15일 전 날짜 계산
|
||||
const fifteenDaysAgoDate = new Date(now);
|
||||
fifteenDaysAgoDate.setDate(now.getDate() - 15);
|
||||
const fifteenDaysAgoStr = formatDateLocal(fifteenDaysAgoDate);
|
||||
|
||||
const response = await comms.getJobReportList(fifteenDaysAgoStr, todayStr, userId, '');
|
||||
|
||||
if (response.Success && response.Data) {
|
||||
const dailyWork: { [key: string]: number } = {};
|
||||
|
||||
// 날짜별 시간 합계 계산
|
||||
response.Data.forEach((item: JobReportItem) => {
|
||||
if (item.pdate) {
|
||||
const date = item.pdate.substring(0, 10);
|
||||
dailyWork[date] = (dailyWork[date] || 0) + (item.hrs || 0);
|
||||
}
|
||||
});
|
||||
|
||||
const insufficientDays: { date: string; hrs: number }[] = [];
|
||||
|
||||
// 어제부터 15일 전까지 확인 (오늘은 제외)
|
||||
for (let i = 1; i <= 15; i++) {
|
||||
const d = new Date(now);
|
||||
d.setDate(now.getDate() - i);
|
||||
const dStr = formatDateLocal(d);
|
||||
|
||||
// 주말(토:6, 일:0) 제외
|
||||
if (d.getDay() === 0 || d.getDay() === 6) continue;
|
||||
|
||||
const hrs = dailyWork[dStr] || 0;
|
||||
if (hrs < 8) {
|
||||
insufficientDays.push({ date: dStr, hrs });
|
||||
}
|
||||
}
|
||||
|
||||
setUnregisteredJobReportCount(insufficientDays.length);
|
||||
setUnregisteredJobReportDays(insufficientDays);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('미등록 업무일지 로드 오류:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 초기화 완료 플래그
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
@@ -129,6 +185,7 @@ export function Jobreport() {
|
||||
const handleSearchAndLoadToday = async () => {
|
||||
await handleSearch();
|
||||
loadTodayWork(selectedUser);
|
||||
loadUnregisteredJobReports(selectedUser);
|
||||
};
|
||||
|
||||
// 사용자 목록 로드
|
||||
@@ -182,7 +239,10 @@ export function Jobreport() {
|
||||
// 새 업무일지 추가 모달
|
||||
const openAddModal = () => {
|
||||
setEditingItem(null);
|
||||
setFormData(initialFormData);
|
||||
setFormData({
|
||||
...initialFormData,
|
||||
pdate: formatDateLocal(new Date())
|
||||
});
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
@@ -195,7 +255,7 @@ export function Jobreport() {
|
||||
const data = response.Data;
|
||||
setEditingItem(null); // 새로 추가하는 것이므로 null
|
||||
setFormData({
|
||||
pdate: new Date().toISOString().split('T')[0], // 오늘 날짜
|
||||
pdate: formatDateLocal(new Date()), // 오늘 날짜 (로컬 시간 기준)
|
||||
projectName: data.projectName || '',
|
||||
pidx: data.pidx ?? null, // pidx도 복사
|
||||
requestpart: data.requestpart || '',
|
||||
@@ -314,6 +374,7 @@ export function Jobreport() {
|
||||
setShowModal(false);
|
||||
loadData();
|
||||
loadTodayWork(selectedUser);
|
||||
loadUnregisteredJobReports(selectedUser);
|
||||
} else {
|
||||
alert(response.Message || '저장에 실패했습니다.');
|
||||
}
|
||||
@@ -336,6 +397,7 @@ export function Jobreport() {
|
||||
alert('삭제되었습니다.');
|
||||
loadData();
|
||||
loadTodayWork(selectedUser);
|
||||
loadUnregisteredJobReports(selectedUser);
|
||||
} else {
|
||||
alert(response.Message || '삭제에 실패했습니다.');
|
||||
}
|
||||
@@ -535,17 +597,34 @@ export function Jobreport() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 미등록 업무일지 카드 */}
|
||||
<div className="flex-shrink-0 w-40">
|
||||
<div
|
||||
className="bg-white/10 rounded-xl p-4 h-full flex flex-col justify-center cursor-pointer hover:bg-white/20 transition-colors"
|
||||
onClick={() => setShowUnregisteredModal(true)}
|
||||
>
|
||||
<div className="text-white/70 text-sm font-medium mb-2 text-center flex items-center justify-center gap-2">
|
||||
<AlertTriangle className="w-4 h-4 text-danger-400" />
|
||||
미등록
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<span className="text-3xl font-bold text-danger-400">{unregisteredJobReportCount}</span>
|
||||
<span className="text-white/70 text-lg ml-1">건</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 우측: 오늘 근무시간 */}
|
||||
<div className="flex-shrink-0 w-48">
|
||||
<div className="flex-shrink-0 w-40">
|
||||
<div className="bg-white/10 rounded-xl p-4 h-full flex flex-col justify-center">
|
||||
<div className="text-white/70 text-sm font-medium mb-2 text-center">오늘 근무시간</div>
|
||||
<div className="text-center">
|
||||
<span className="text-3xl font-bold text-white">{todayWork.hrs}</span>
|
||||
<span className="text-3xl font-bold text-white">{todayWork.hrs.toFixed(1)}</span>
|
||||
<span className="text-white/70 text-lg ml-1">시간</span>
|
||||
</div>
|
||||
{todayWork.ot > 0 && (
|
||||
<div className="text-center mt-1">
|
||||
<span className="text-warning-400 text-sm">OT: {todayWork.ot}시간</span>
|
||||
<span className="text-warning-400 text-sm">OT: {todayWork.ot.toFixed(1)}시간</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -568,13 +647,13 @@ export function Jobreport() {
|
||||
<thead className="bg-white/10">
|
||||
<tr>
|
||||
<th className="px-2 py-3 text-center text-xs font-medium text-white/70 uppercase w-10"></th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase w-24">날짜</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase" style={{ width: '35%' }}>프로젝트</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">업무형태</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">상태</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">시간</th>
|
||||
{canViewOT && <th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">OT</th>}
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">담당자</th>
|
||||
<th className="px-2 py-3 text-left text-xs font-medium text-white/70 uppercase w-24">날짜</th>
|
||||
<th className="px-2 py-3 text-left text-xs font-medium text-white/70 uppercase" style={{ width: '35%' }}>프로젝트</th>
|
||||
<th className="px-2 py-3 text-left text-xs font-medium text-white/70 uppercase">업무형태</th>
|
||||
<th className="px-2 py-3 text-left text-xs font-medium text-white/70 uppercase">상태</th>
|
||||
<th className="px-2 py-3 text-left text-xs font-medium text-white/70 uppercase">시간</th>
|
||||
{canViewOT && <th className="px-2 py-3 text-left text-xs font-medium text-white/70 uppercase">OT</th>}
|
||||
<th className="px-2 py-3 text-left text-xs font-medium text-white/70 uppercase">담당자</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/10">
|
||||
@@ -722,6 +801,66 @@ export function Jobreport() {
|
||||
endDate={endDate}
|
||||
userId={selectedUser}
|
||||
/>
|
||||
|
||||
{/* 업무일지 미등록 상세 모달 */}
|
||||
{showUnregisteredModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
|
||||
<div className="bg-slate-900 border border-white/10 rounded-2xl w-full max-w-md shadow-2xl overflow-hidden animate-scale-in">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10 bg-white/5">
|
||||
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<AlertTriangle className="w-5 h-5 text-danger-400" />
|
||||
업무일지 미등록 내역
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowUnregisteredModal(false)}
|
||||
className="text-white/50 hover:text-white transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 max-h-[60vh] overflow-y-auto">
|
||||
<p className="text-white/70 text-sm mb-4">
|
||||
최근 15일(평일 기준) 중 8시간 미만 등록된 날짜입니다.
|
||||
</p>
|
||||
|
||||
{unregisteredJobReportDays.length === 0 ? (
|
||||
<div className="text-center py-8 text-white/50">
|
||||
미등록 내역이 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{unregisteredJobReportDays.map((day, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-3 bg-white/5 rounded-lg border border-white/5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-danger-500/20 flex items-center justify-center text-danger-400 text-xs font-bold">
|
||||
{index + 1}
|
||||
</div>
|
||||
<span className="text-white font-medium">{day.date}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`font-bold ${day.hrs === 0 ? 'text-danger-400' : 'text-warning-400'}`}>
|
||||
{day.hrs}시간
|
||||
</span>
|
||||
<span className="text-white/40 text-xs">/ 8시간</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-4 border-t border-white/10 bg-white/5 flex justify-end">
|
||||
<button
|
||||
onClick={() => setShowUnregisteredModal(false)}
|
||||
className="px-4 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors text-sm font-medium"
|
||||
>
|
||||
닫기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user