o2o-negosium-original/negodata/front/src/pages/partners.tsx

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>
);
}