171 lines
6.7 KiB
TypeScript
171 lines
6.7 KiB
TypeScript
import { useState } from 'react';
|
|
import { Plus, Upload, Download, FileSpreadsheet, ChevronDown, Trash2 } from 'lucide-react';
|
|
import { useOverlayRouter } from '@/lib/useOverlayRouter';
|
|
import { showToast } from '@/lib/notify';
|
|
import { confirm } from '@/lib/confirm';
|
|
import { PageContainer } from '@/components/layout/PageContainer';
|
|
import { PageToolbar, SearchInput } from '@/components/layout/PageToolbar';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { useAuthStore, canManage } from '@/stores/auth';
|
|
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
|
import { useServerList } from '@/lib/useServerList';
|
|
import { usePartners } from '@/features/partners/hooks/usePartners';
|
|
import type { ListSuppliersParams } from '@/api/generated/model/listSuppliersParams';
|
|
import { PartnerTable } from '@/features/partners/components/PartnerTable';
|
|
import { PartnerFormSheet } from '@/features/partners/components/PartnerFormSheet';
|
|
import { ExcelUploadModal, downloadPartnerTemplate } from '@/features/partners/components/ExcelUploadModal';
|
|
import { type Partner } from '@/features/partners/types';
|
|
|
|
export default function PartnersPage() {
|
|
// 검색/페이지 상태(재사용 훅) → 서버 쿼리 파라미터로 변환.
|
|
const list = useServerList({ pageSize: 10 });
|
|
const params: ListSuppliersParams = {
|
|
search: list.debouncedSearch || undefined,
|
|
page: list.page,
|
|
size: list.pageSize,
|
|
};
|
|
|
|
const { partners, total, allPartners, createPartner, updatePartner, deletePartner, bulkCreate } =
|
|
usePartners(params);
|
|
const totalPages = list.totalPages(total);
|
|
|
|
// 오버레이(폼/엑셀)를 쿼리스트링으로 → 딥링크·뒤로가기·새로고침 지원.
|
|
// ?detail=<id> 직접 접근 시 데이터 로드 후 수정 폼이 자동으로 열린다.
|
|
const overlay = useOverlayRouter(['new', 'detail', 'modal']);
|
|
const editId = overlay.get('detail');
|
|
const modal = overlay.get('modal'); // 'excel' | null
|
|
const editing = editId ? allPartners.find((p) => p.supplier_id === editId) ?? null : null;
|
|
const formMode: 'create' | 'edit' = editId ? 'edit' : 'create';
|
|
const isFormOpen = overlay.has('new') || !!editing;
|
|
|
|
// 협력사 삭제는 최고관리자 전용(단건 삭제와 동일 규칙) — 일괄삭제 버튼도 최고관리자에게만 노출.
|
|
const isSuperAdmin = useAuthStore((st) => canManage(st.user?.role));
|
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
|
|
|
const handleBulkDelete = async () => {
|
|
if (selectedIds.length === 0) return;
|
|
if (!(await confirm({ title: '선택 협력사 일괄 삭제', description: `선택한 ${selectedIds.length}개 협력사를 명부에서 삭제하시겠습니까?`, confirmText: '삭제', destructive: true }))) return;
|
|
let ok = 0;
|
|
let fail = 0;
|
|
for (const id of selectedIds) {
|
|
try {
|
|
await deletePartner(id);
|
|
ok += 1;
|
|
} catch {
|
|
fail += 1;
|
|
}
|
|
}
|
|
setSelectedIds([]);
|
|
showToast(fail === 0 ? `${ok}개 협력사가 삭제되었습니다.` : `${ok}개 삭제 · ${fail}개 실패`, fail === 0 ? 'info' : 'error');
|
|
};
|
|
|
|
const openCreate = () => overlay.open('new');
|
|
const openEdit = (part: Partner) => overlay.open('detail', part.supplier_id);
|
|
|
|
const handleDeletePartner = async (id: string, partnerName: string) => {
|
|
if (await confirm({ title: '협력사 삭제', description: `[${partnerName}] 파트너사를 협력사 목록에서 삭제하시겠습니까?`, confirmText: '삭제', destructive: true })) {
|
|
try {
|
|
await deletePartner(id);
|
|
showToast('협력사 정보가 명부에서 제거되었습니다.', 'info');
|
|
} catch (err) {
|
|
showToast(err instanceof Error ? err.message : '협력사 삭제 실패', 'error');
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<PageContainer>
|
|
{/* 검색/액션 바 + 테이블을 한 카드로 붙인다(포털형). */}
|
|
<div className="overflow-hidden rounded-lg border border-border bg-card">
|
|
<PageToolbar
|
|
className="rounded-none border-0 border-b border-border"
|
|
actions={
|
|
<>
|
|
{isSuperAdmin && (
|
|
<Button
|
|
variant="outline"
|
|
disabled={selectedIds.length === 0}
|
|
onClick={handleBulkDelete}
|
|
className="text-rose-600 hover:text-rose-700"
|
|
>
|
|
<Trash2 />
|
|
선택 삭제
|
|
{selectedIds.length > 0 && <Badge variant="destructive">{selectedIds.length}</Badge>}
|
|
</Button>
|
|
)}
|
|
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger render={<Button variant="outline" />}>
|
|
<FileSpreadsheet />
|
|
엑셀업로드
|
|
<ChevronDown />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent>
|
|
<DropdownMenuItem onClick={() => overlay.open('modal', 'excel')}>
|
|
<Upload />
|
|
일괄 업로드
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={downloadPartnerTemplate}>
|
|
<Download />
|
|
양식 다운로드
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
<Button onClick={openCreate}>
|
|
<Plus />
|
|
신규 협력사 등록
|
|
</Button>
|
|
</>
|
|
}
|
|
>
|
|
<SearchInput
|
|
id="partner-search"
|
|
value={list.search}
|
|
onChange={(e) => list.setSearch(e.target.value)}
|
|
onKeyDown={(e) => e.key === 'Enter' && list.submitSearch()}
|
|
onClear={list.clearSearch}
|
|
placeholder="협력사명, 코드 또는 담당자명으로 추적 검색..."
|
|
/>
|
|
</PageToolbar>
|
|
|
|
<PartnerTable
|
|
className="rounded-none border-0"
|
|
data={partners}
|
|
selectedIds={isSuperAdmin ? selectedIds : undefined}
|
|
onSelectionChange={isSuperAdmin ? setSelectedIds : undefined}
|
|
onRowClick={openEdit}
|
|
page={list.page}
|
|
totalPages={totalPages}
|
|
totalCount={total}
|
|
pageSize={list.pageSize}
|
|
onPageChange={list.setPage}
|
|
/>
|
|
</div>
|
|
|
|
{isFormOpen && (
|
|
<PartnerFormSheet
|
|
key={`${formMode}-${editing?.supplier_id ?? 'new'}`}
|
|
open
|
|
mode={formMode}
|
|
partner={editing}
|
|
onCreate={createPartner}
|
|
onUpdate={updatePartner}
|
|
onDelete={handleDeletePartner}
|
|
onClose={overlay.close}
|
|
/>
|
|
)}
|
|
|
|
{modal === 'excel' && (
|
|
<ExcelUploadModal
|
|
open
|
|
partners={allPartners}
|
|
onConfirm={bulkCreate}
|
|
onClose={overlay.close}
|
|
/>
|
|
)}
|
|
</PageContainer>
|
|
);
|
|
}
|