- 수입검사: InspectionCreate/Detail/List 대폭 개선, OrderSelectModal/문서 컴포넌트 신규 추가 - 수입검사: actions/types/mockData/inspectionConfig 전면 리팩토링 - QMS: InspectionModalV2/ImportInspectionDocument 개선 - 캘린더: DayTimeView 신규 추가, CalendarHeader/ScheduleCalendar/utils 확장 - 출고: ShipmentDetail/List/actions 개선, ShipmentOrderDocument/ShippingSlip 수정 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
221 lines
6.9 KiB
TypeScript
221 lines
6.9 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* 수주 선택 모달
|
|
*
|
|
* 기획서 기반 신규 생성:
|
|
* - 검색 입력
|
|
* - 체크박스 테이블 (수주번호, 현장명, 납품일, 개소)
|
|
* - 취소/선택 버튼
|
|
*/
|
|
|
|
import { useState, useCallback, useEffect } from 'react';
|
|
import { Search, Loader2 } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import { toast } from 'sonner';
|
|
import { getOrderSelectList } from './actions';
|
|
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
|
import type { OrderSelectItem } from './types';
|
|
|
|
interface OrderSelectModalProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
onSelect: (items: OrderSelectItem[]) => void;
|
|
/** 이미 선택된 항목 ID 목록 (중복 선택 방지) */
|
|
excludeIds?: string[];
|
|
}
|
|
|
|
export function OrderSelectModal({
|
|
open,
|
|
onOpenChange,
|
|
onSelect,
|
|
excludeIds = [],
|
|
}: OrderSelectModalProps) {
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [items, setItems] = useState<OrderSelectItem[]>([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
|
|
|
// 데이터 로드
|
|
const loadItems = useCallback(async (q?: string) => {
|
|
setIsLoading(true);
|
|
try {
|
|
const result = await getOrderSelectList({ q: q || undefined });
|
|
if (result.success) {
|
|
// 이미 선택된 항목 제외
|
|
const filtered = result.data.filter((item) => !excludeIds.includes(item.id));
|
|
setItems(filtered);
|
|
} else {
|
|
toast.error(result.error || '수주 목록을 불러오는데 실패했습니다.');
|
|
}
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[OrderSelectModal] loadItems error:', error);
|
|
toast.error('수주 목록 로드 중 오류가 발생했습니다.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [excludeIds]);
|
|
|
|
// 모달 열릴 때 데이터 로드 & 상태 초기화
|
|
useEffect(() => {
|
|
if (open) {
|
|
setSearchTerm('');
|
|
setSelectedIds(new Set());
|
|
loadItems();
|
|
}
|
|
}, [open, loadItems]);
|
|
|
|
// 검색
|
|
const handleSearch = useCallback(() => {
|
|
loadItems(searchTerm);
|
|
}, [searchTerm, loadItems]);
|
|
|
|
const handleSearchKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter') {
|
|
handleSearch();
|
|
}
|
|
}, [handleSearch]);
|
|
|
|
// 체크박스 토글
|
|
const handleToggle = useCallback((id: string) => {
|
|
setSelectedIds((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(id)) {
|
|
next.delete(id);
|
|
} else {
|
|
next.add(id);
|
|
}
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
// 전체 선택/해제
|
|
const handleToggleAll = useCallback(() => {
|
|
setSelectedIds((prev) => {
|
|
if (prev.size === items.length) {
|
|
return new Set();
|
|
}
|
|
return new Set(items.map((item) => item.id));
|
|
});
|
|
}, [items]);
|
|
|
|
// 선택 확인
|
|
const handleConfirm = useCallback(() => {
|
|
const selectedItems = items.filter((item) => selectedIds.has(item.id));
|
|
onSelect(selectedItems);
|
|
onOpenChange(false);
|
|
}, [items, selectedIds, onSelect, onOpenChange]);
|
|
|
|
const isAllSelected = items.length > 0 && selectedIds.size === items.length;
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle>수주 선택</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
{/* 검색 */}
|
|
<div className="flex gap-2">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
<Input
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
onKeyDown={handleSearchKeyDown}
|
|
placeholder="수주번호, 현장명 검색..."
|
|
className="pl-9"
|
|
/>
|
|
</div>
|
|
<Button variant="outline" onClick={handleSearch}>
|
|
검색
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 테이블 */}
|
|
<div className="max-h-[400px] overflow-y-auto border rounded-md">
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
|
</div>
|
|
) : (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-10">
|
|
<Checkbox
|
|
checked={isAllSelected}
|
|
onCheckedChange={handleToggleAll}
|
|
/>
|
|
</TableHead>
|
|
<TableHead>수주번호</TableHead>
|
|
<TableHead>현장명</TableHead>
|
|
<TableHead className="text-center">납품일</TableHead>
|
|
<TableHead className="text-center">개소</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{items.map((item) => (
|
|
<TableRow
|
|
key={item.id}
|
|
className="cursor-pointer hover:bg-muted/50"
|
|
onClick={() => handleToggle(item.id)}
|
|
>
|
|
<TableCell onClick={(e) => e.stopPropagation()}>
|
|
<Checkbox
|
|
checked={selectedIds.has(item.id)}
|
|
onCheckedChange={() => handleToggle(item.id)}
|
|
/>
|
|
</TableCell>
|
|
<TableCell>{item.orderNumber}</TableCell>
|
|
<TableCell>{item.siteName}</TableCell>
|
|
<TableCell className="text-center">{item.deliveryDate}</TableCell>
|
|
<TableCell className="text-center">{item.locationCount}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
{items.length === 0 && (
|
|
<TableRow>
|
|
<TableCell colSpan={5} className="text-center text-muted-foreground py-8">
|
|
{searchTerm ? '검색 결과가 없습니다.' : '수주 데이터가 없습니다.'}
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
취소
|
|
</Button>
|
|
<Button
|
|
onClick={handleConfirm}
|
|
disabled={selectedIds.size === 0}
|
|
>
|
|
선택 ({selectedIds.size}건)
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|