fix: [자재투입] 품목 검색 추가 - 재고 없는 품목도 검색 + 개별 강제입고 버튼
This commit is contained in:
@@ -34,7 +34,7 @@ import {
|
||||
} from '@/components/ui/table';
|
||||
import { toast } from 'sonner';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { getMaterialsForWorkOrder, registerMaterialInput, getMaterialsForItem, registerMaterialInputForItem, searchStockByCode, forceCreateReceiving, type MaterialForInput, type MaterialForItemInput, type StockSearchResult } from './actions';
|
||||
import { getMaterialsForWorkOrder, registerMaterialInput, getMaterialsForItem, registerMaterialInputForItem, searchStockByCode, forceCreateReceiving, searchItems, type MaterialForInput, type MaterialForItemInput, type StockSearchResult, type ItemSearchResult } from './actions';
|
||||
import type { WorkOrder } from '../ProductionDashboard/types';
|
||||
import type { MaterialInput } from './types';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
@@ -95,11 +95,22 @@ export function MaterialInputModal({
|
||||
const [searchResults, setSearchResults] = useState<StockSearchResult[]>([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
|
||||
// 품목 검색 결과 (재고 없는 품목 포함)
|
||||
const [itemSearchResults, setItemSearchResults] = useState<ItemSearchResult[]>([]);
|
||||
|
||||
const handleStockSearch = useCallback(async (query: string) => {
|
||||
if (!query.trim()) { setSearchResults([]); return; }
|
||||
if (!query.trim()) { setSearchResults([]); setItemSearchResults([]); return; }
|
||||
setIsSearching(true);
|
||||
const result = await searchStockByCode(query.trim());
|
||||
if (result.success) setSearchResults(result.data);
|
||||
const [stockResult, itemResult] = await Promise.all([
|
||||
searchStockByCode(query.trim()),
|
||||
searchItems(query.trim()),
|
||||
]);
|
||||
if (stockResult.success) setSearchResults(stockResult.data);
|
||||
if (itemResult.success) {
|
||||
// 재고 검색 결과에 이미 있는 품목은 제외
|
||||
const stockItemIds = new Set(stockResult.data?.map(s => s.itemId) || []);
|
||||
setItemSearchResults(itemResult.data.filter(i => !stockItemIds.has(i.itemId)));
|
||||
}
|
||||
setIsSearching(false);
|
||||
}, []);
|
||||
|
||||
@@ -591,23 +602,33 @@ export function MaterialInputModal({
|
||||
</div>
|
||||
) : isSearching ? (
|
||||
<p className="text-[11px] text-gray-400 flex items-center gap-1"><Loader2 className="h-3 w-3 animate-spin" />검색 중...</p>
|
||||
) : searchQuery && (
|
||||
) : searchQuery && searchResults.length === 0 && itemSearchResults.length === 0 ? (
|
||||
<p className="text-[11px] text-red-500">검색 결과 없음</p>
|
||||
)}
|
||||
{searchQuery && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="w-full h-8 text-xs border-orange-300 text-orange-600 hover:bg-orange-50"
|
||||
disabled={isForceCreating}
|
||||
onClick={() => {
|
||||
const firstResult = searchResults[0];
|
||||
if (firstResult) handleForceReceiving(firstResult.itemId, firstResult.itemCode);
|
||||
}}
|
||||
>
|
||||
{isForceCreating ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : <Zap className="h-3 w-3 mr-1" />}
|
||||
[DEV] 입고자료 강제생성
|
||||
</Button>
|
||||
) : null}
|
||||
{/* 재고 없는 품목 목록 (items 테이블에만 존재) */}
|
||||
{itemSearchResults.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] text-orange-600 font-medium">재고 없는 품목 {itemSearchResults.length}건:</p>
|
||||
{itemSearchResults.map((item, idx) => (
|
||||
<div key={`item-${item.itemId}-${idx}`} className="text-[11px] px-2 py-1.5 rounded border bg-orange-50 border-orange-200 flex items-center justify-between">
|
||||
<div>
|
||||
<span className="font-medium">{item.itemCode}</span>
|
||||
<span className="text-gray-500 ml-1">{item.itemName}</span>
|
||||
<span className="text-[10px] text-gray-400 ml-1">({item.itemType})</span>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-6 text-[10px] px-2 border-orange-300 text-orange-600 hover:bg-orange-100"
|
||||
disabled={isForceCreating}
|
||||
onClick={() => handleForceReceiving(item.itemId, item.itemCode)}
|
||||
>
|
||||
{isForceCreating ? <Loader2 className="h-3 w-3 animate-spin" /> : <Zap className="h-3 w-3 mr-0.5" />}
|
||||
강제입고
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -939,4 +939,36 @@ export async function searchStockByCode(
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 품목(items) 검색 (재고 없는 품목도 검색) =====
|
||||
export interface ItemSearchResult {
|
||||
itemId: number;
|
||||
itemCode: string;
|
||||
itemName: string;
|
||||
itemType: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export async function searchItems(
|
||||
search: string
|
||||
): Promise<{ success: boolean; data: ItemSearchResult[]; error?: string }> {
|
||||
const result = await executeServerAction<{
|
||||
data: Array<{ id: number; code: string; name: string; item_type: string; unit: string }>;
|
||||
}>({
|
||||
url: buildApiUrl('/api/v1/items', { search, per_page: 10 }),
|
||||
errorMessage: '품목 검색에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data) return { success: false, data: [], error: result.error };
|
||||
const items = Array.isArray(result.data) ? result.data : (result.data.data || []);
|
||||
return {
|
||||
success: true,
|
||||
data: items.map((i: { id: number; code: string; name: string; item_type: string; unit: string }) => ({
|
||||
itemId: i.id,
|
||||
itemCode: i.code,
|
||||
itemName: i.name,
|
||||
itemType: i.item_type,
|
||||
unit: i.unit,
|
||||
})),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user