Files
GW-renewal/frontend/src/app/(main)/tam/0030/page.tsx
JAE SIK CHO f8427ee1d0 share job
2026-04-09 11:12:12 +09:00

249 lines
9.2 KiB
TypeScript

'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
getApvappList, multiApprove, multiReject,
APRVL_STUS_LABEL, APRVL_KIND_LABEL, ApvreqListItem,
} from '@/lib/api/tam';
import { localYmd, localYmdDaysAgo } from '@/lib/utils/date';
const PAGE_SIZE = 20;
const TODAY = localYmd();
const MONTH_AGO = localYmdDaysAgo(30);
export default function Tam0030Page() {
const router = useRouter();
const qc = useQueryClient();
const [staYmd, setStaYmd] = useState(MONTH_AGO);
const [endYmd, setEndYmd] = useState(TODAY);
const [pageNo, setPageNo] = useState(1);
const [filter, setFilter] = useState({ staYmd: MONTH_AGO, endYmd: TODAY, pageNo: 1 });
const [selected, setSelected] = useState<Set<string>>(new Set());
const [batchComment, setBatchComment] = useState('');
const { data, isLoading, refetch } = useQuery({
queryKey: ['apvapp', filter],
queryFn: () => getApvappList({ ...filter, pageSize: PAGE_SIZE }),
});
const handleSearch = () => {
const f = { staYmd, endYmd, pageNo: 1 };
const same = JSON.stringify(f) === JSON.stringify(filter);
setFilter(f);
setPageNo(1);
setSelected(new Set());
if (same) refetch();
};
const handlePageChange = (p: number) => {
setPageNo(p);
setFilter((prev) => ({ ...prev, pageNo: p }));
setSelected(new Set());
};
// 결재중인 항목만 체크박스 대상
const appringItems = (data?.list ?? []).filter((item) => item.aprvlStusCd === '0002');
const toggleAll = () => {
if (selected.size === appringItems.length) {
setSelected(new Set());
} else {
setSelected(new Set(appringItems.map((i) => i.aprvlDocId)));
}
};
const toggleOne = (id: string) => {
setSelected((prev) => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
};
const getSelectedItems = () =>
(data?.list ?? [])
.filter((i) => selected.has(i.aprvlDocId))
.map((i) => ({ aprvlDocId: i.aprvlDocId, aprvlSno: i.aprvlCmplSno + 1 }));
const approveMut = useMutation({
mutationFn: () => multiApprove(getSelectedItems(), batchComment),
onSuccess: () => {
alert(`${selected.size}건 일괄 승인 처리되었습니다.`);
setSelected(new Set());
setBatchComment('');
qc.invalidateQueries({ queryKey: ['apvapp'] });
},
onError: (e: any) => alert(e?.response?.data?.message ?? '일괄 승인 오류'),
});
const rejectMut = useMutation({
mutationFn: () => multiReject(getSelectedItems(), batchComment),
onSuccess: () => {
alert(`${selected.size}건 일괄 반려 처리되었습니다.`);
setSelected(new Set());
setBatchComment('');
qc.invalidateQueries({ queryKey: ['apvapp'] });
},
onError: (e: any) => alert(e?.response?.data?.message ?? '일괄 반려 오류'),
});
const handleBatchApprove = () => {
if (selected.size === 0) { alert('선택된 항목이 없습니다.'); return; }
if (!confirm(`${selected.size}건을 일괄 승인하시겠습니까?`)) return;
approveMut.mutate();
};
const handleBatchReject = () => {
if (selected.size === 0) { alert('선택된 항목이 없습니다.'); return; }
if (!batchComment.trim()) { alert('반려 사유를 입력해주세요.'); return; }
if (!confirm(`${selected.size}건을 일괄 반려하시겠습니까?`)) return;
rejectMut.mutate();
};
const totalPages = data ? Math.ceil(data.total / PAGE_SIZE) : 1;
const isPending = approveMut.isPending || rejectMut.isPending;
const statusBadge = (cd: string) => {
const colors: Record<string, string> = {
'0001': 'bg-gray-100 text-gray-600',
'0002': 'bg-blue-100 text-blue-700',
'0003': 'bg-green-100 text-green-700',
'0004': 'bg-red-100 text-red-700',
};
return (
<span className={`px-2 py-0.5 rounded text-xs font-medium ${colors[cd] ?? ''}`}>
{APRVL_STUS_LABEL[cd] ?? cd}
</span>
);
};
return (
<div className="p-4">
<h2 className="text-lg font-semibold mb-4"> </h2>
{/* 검색 */}
<div className="flex flex-wrap gap-2 mb-4 items-center">
<input type="date" value={staYmd.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3')}
onChange={(e) => setStaYmd(e.target.value.replace(/-/g, ''))}
className="border rounded px-2 py-1.5 text-sm"
/>
<span className="text-sm">~</span>
<input type="date" value={endYmd.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3')}
onChange={(e) => setEndYmd(e.target.value.replace(/-/g, ''))}
className="border rounded px-2 py-1.5 text-sm"
/>
<button onClick={handleSearch}
className="px-4 py-1.5 bg-blue-600 text-white text-sm rounded hover:bg-blue-700"
>
</button>
</div>
{/* 일괄처리 바 */}
{selected.size > 0 && (
<div className="flex items-center gap-2 mb-3 p-2 bg-yellow-50 border border-yellow-200 rounded">
<span className="text-sm font-medium text-yellow-800">{selected.size} </span>
<input
type="text"
value={batchComment}
onChange={(e) => setBatchComment(e.target.value)}
placeholder="의견 (반려 시 필수)"
className="flex-1 border rounded px-2 py-1 text-sm"
/>
<button onClick={handleBatchApprove} disabled={isPending}
className="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700 disabled:opacity-50"
>
</button>
<button onClick={handleBatchReject} disabled={isPending}
className="px-3 py-1 bg-red-500 text-white text-sm rounded hover:bg-red-600 disabled:opacity-50"
>
</button>
</div>
)}
{/* 테이블 */}
<table className="w-full text-sm border-collapse">
<thead>
<tr className="bg-gray-100 text-center">
<th className="border px-2 py-2 w-8">
<input type="checkbox"
checked={appringItems.length > 0 && selected.size === appringItems.length}
onChange={toggleAll}
/>
</th>
<th className="border px-3 py-2"></th>
<th className="border px-3 py-2"></th>
<th className="border px-3 py-2"></th>
<th className="border px-3 py-2"></th>
<th className="border px-3 py-2"></th>
<th className="border px-3 py-2 w-16"></th>
<th className="border px-3 py-2 w-24"></th>
</tr>
</thead>
<tbody>
{isLoading ? (
<tr><td colSpan={8} className="text-center py-8 text-gray-400"> ...</td></tr>
) : !data?.list?.length ? (
<tr><td colSpan={8} className="text-center py-8 text-gray-400"> .</td></tr>
) : (
data.list.map((item: ApvreqListItem) => {
const isAppring = item.aprvlStusCd === '0002';
const isChecked = selected.has(item.aprvlDocId);
return (
<tr key={item.aprvlDocId}
className={`hover:bg-gray-50 text-center ${isChecked ? 'bg-blue-50' : ''}`}
>
<td className="border px-2 py-2">
{isAppring && (
<input type="checkbox" checked={isChecked}
onChange={() => toggleOne(item.aprvlDocId)}
/>
)}
</td>
<td className="border px-3 py-2">{item.aplntId}</td>
<td className="border px-3 py-2">{APRVL_KIND_LABEL[item.aprvlKindCd] ?? item.aprvlKindCd}</td>
<td className="border px-3 py-2">{statusBadge(item.aprvlStusCd)}</td>
<td className="border px-3 py-2">{item.offerDt}</td>
<td className="border px-3 py-2">{item.cmplDt || '-'}</td>
<td className="border px-3 py-2">{item.aprvlCmplSno} / {item.finalAprvlSno}</td>
<td className="border px-3 py-2">
<button
onClick={() => router.push(`/tam/0030/${item.aprvlDocId}`)}
className="px-2 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600"
>
</button>
</td>
</tr>
);
})
)}
</tbody>
</table>
{/* 페이지네이션 */}
{totalPages > 1 && (
<div className="flex justify-center gap-1 mt-4">
{Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => (
<button key={p} onClick={() => handlePageChange(p)}
className={`px-3 py-1 text-sm rounded border ${
p === pageNo ? 'bg-blue-600 text-white border-blue-600' : 'hover:bg-gray-100'
}`}
>
{p}
</button>
))}
</div>
)}
<div className="mt-2 text-sm text-gray-500 text-right"> {data?.total ?? 0}</div>
</div>
);
}