ERP-node/frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx

266 lines
8.2 KiB
TypeScript
Raw Normal View History

"use client";
import React, { useState, useEffect } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { Search, Loader2 } from "lucide-react";
import { useEntitySearch } from "../entity-search-input/useEntitySearch";
import { ItemSelectionModalProps } from "./types";
export function ItemSelectionModal({
open,
onOpenChange,
sourceTable,
sourceColumns,
sourceSearchFields = [],
multiSelect = true,
filterCondition = {},
modalTitle,
alreadySelected = [],
uniqueField,
onSelect,
}: ItemSelectionModalProps) {
const [localSearchText, setLocalSearchText] = useState("");
const [selectedItems, setSelectedItems] = useState<any[]>([]);
const { results, loading, error, search, clearSearch } = useEntitySearch({
tableName: sourceTable,
searchFields: sourceSearchFields.length > 0 ? sourceSearchFields : sourceColumns,
filterCondition,
});
// 모달 열릴 때 초기 검색
useEffect(() => {
if (open) {
search("", 1); // 빈 검색어로 전체 목록 조회
setSelectedItems([]);
} else {
clearSearch();
setLocalSearchText("");
setSelectedItems([]);
}
}, [open]);
const handleSearch = () => {
search(localSearchText, 1);
};
const handleToggleItem = (item: any) => {
if (!multiSelect) {
setSelectedItems([item]);
return;
}
const isSelected = selectedItems.some((selected) =>
uniqueField
? selected[uniqueField] === item[uniqueField]
: selected === item
);
if (isSelected) {
setSelectedItems(
selectedItems.filter((selected) =>
uniqueField
? selected[uniqueField] !== item[uniqueField]
: selected !== item
)
);
} else {
setSelectedItems([...selectedItems, item]);
}
};
const handleConfirm = () => {
onSelect(selectedItems);
onOpenChange(false);
};
// 이미 추가된 항목인지 확인
const isAlreadyAdded = (item: any): boolean => {
if (!uniqueField) return false;
return alreadySelected.some(
(selected) => selected[uniqueField] === item[uniqueField]
);
};
// 선택된 항목인지 확인
const isSelected = (item: any): boolean => {
return selectedItems.some((selected) =>
uniqueField
? selected[uniqueField] === item[uniqueField]
: selected === item
);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[95vw] sm:max-w-[900px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg">{modalTitle}</DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
{multiSelect && " (다중 선택 가능)"}
</DialogDescription>
</DialogHeader>
{/* 검색 입력 */}
<div className="flex gap-2">
<Input
placeholder="검색어를 입력하세요"
value={localSearchText}
onChange={(e) => setLocalSearchText(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleSearch();
}
}}
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
<Button
onClick={handleSearch}
disabled={loading}
className="h-8 text-xs sm:h-10 sm:text-sm"
>
{loading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Search className="h-4 w-4" />
)}
<span className="ml-2"></span>
</Button>
</div>
{/* 선택된 항목 수 */}
{selectedItems.length > 0 && (
<div className="text-sm text-primary">
{selectedItems.length}
</div>
)}
{/* 오류 메시지 */}
{error && (
<div className="text-sm text-destructive bg-destructive/10 p-3 rounded-md">
{error}
</div>
)}
{/* 검색 결과 테이블 */}
<div className="border rounded-md overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-xs sm:text-sm">
<thead className="bg-muted">
<tr>
{multiSelect && (
<th className="px-4 py-2 text-left font-medium text-muted-foreground w-12">
</th>
)}
{sourceColumns.map((col) => (
<th
key={col}
className="px-4 py-2 text-left font-medium text-muted-foreground"
>
{col}
</th>
))}
</tr>
</thead>
<tbody>
{loading && results.length === 0 ? (
<tr>
<td
colSpan={sourceColumns.length + (multiSelect ? 1 : 0)}
className="px-4 py-8 text-center"
>
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
<p className="mt-2 text-muted-foreground"> ...</p>
</td>
</tr>
) : results.length === 0 ? (
<tr>
<td
colSpan={sourceColumns.length + (multiSelect ? 1 : 0)}
className="px-4 py-8 text-center text-muted-foreground"
>
</td>
</tr>
) : (
results.map((item, index) => {
const alreadyAdded = isAlreadyAdded(item);
const selected = isSelected(item);
return (
<tr
key={index}
className={`border-t transition-colors ${
alreadyAdded
? "bg-muted/50 opacity-50"
: selected
? "bg-primary/10"
: "hover:bg-accent cursor-pointer"
}`}
onClick={() => {
if (!alreadyAdded) {
handleToggleItem(item);
}
}}
>
{multiSelect && (
<td className="px-4 py-2">
<Checkbox
checked={selected}
disabled={alreadyAdded}
onCheckedChange={() => {
if (!alreadyAdded) {
handleToggleItem(item);
}
}}
/>
</td>
)}
{sourceColumns.map((col) => (
<td key={col} className="px-4 py-2">
{item[col] || "-"}
</td>
))}
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={() => onOpenChange(false)}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</Button>
<Button
onClick={handleConfirm}
disabled={selectedItems.length === 0}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
({selectedItems.length})
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}