fix(WEB): 자재투입 모달 UX 개선 - 선택 유지/중복투입 차단/버튼 UI
- 자재 투입 후 전체 새로고침 제거, 로컬 오버라이드로 현재 수주 선택 유지 - 자동선택 useEffect에 현재 선택 유효 가드 추가 - API remainingRequiredQty 활용하여 이미 충족된 품목 추가 선택 차단 - 기투입 수량 표시 및 '투입 완료' 뱃지 표시 - 체크박스 → 버튼 형태(선택/선택됨)로 변경 - 수량 소수점 불필요 자릿수 제거 (parseFloat 래핑)
This commit is contained in:
@@ -1,15 +1,14 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 자재투입 모달 (로트 기반)
|
||||
* 자재투입 모달 (로트 선택 기반)
|
||||
*
|
||||
* 입고관리에서 생성된 실제 로트번호 기준으로 자재를 표시합니다.
|
||||
* 컬럼: 로트번호 | 품목명 | 가용수량 | 단위 | 투입 수량 (input, 숫자만)
|
||||
* 하단: 취소 / 투입
|
||||
* 로트를 체크박스로 선택하면 필요수량만큼 FIFO 순서로 자동 배분합니다.
|
||||
* 같은 품목의 여러 로트를 조합하여 필요수량을 충족시킬 수 있습니다.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { Loader2, Check } from 'lucide-react';
|
||||
import { ContentSkeleton } from '@/components/ui/skeleton';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -18,7 +17,7 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -29,7 +28,7 @@ import {
|
||||
} from '@/components/ui/table';
|
||||
import { toast } from 'sonner';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { getMaterialsForWorkOrder, registerMaterialInput, type MaterialForInput } from './actions';
|
||||
import { getMaterialsForWorkOrder, registerMaterialInput, getMaterialsForItem, registerMaterialInputForItem, type MaterialForInput, type MaterialForItemInput } from './actions';
|
||||
import type { WorkOrder } from '../ProductionDashboard/types';
|
||||
import type { MaterialInput } from './types';
|
||||
|
||||
@@ -37,22 +36,39 @@ interface MaterialInputModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
order: WorkOrder | null;
|
||||
workOrderItemId?: number; // 개소(작업지시품목) ID
|
||||
workOrderItemName?: string; // 개소명 (모달 헤더 표시용)
|
||||
onComplete?: () => void;
|
||||
isCompletionFlow?: boolean;
|
||||
onSaveMaterials?: (orderId: string, materials: MaterialInput[]) => void;
|
||||
savedMaterials?: MaterialInput[];
|
||||
}
|
||||
|
||||
interface MaterialGroup {
|
||||
itemId: number;
|
||||
materialName: string;
|
||||
materialCode: string;
|
||||
requiredQty: number;
|
||||
effectiveRequiredQty: number; // 남은 필요수량 (이미 투입분 차감)
|
||||
alreadyInputted: number; // 이미 투입된 수량
|
||||
unit: string;
|
||||
lots: MaterialForInput[];
|
||||
}
|
||||
|
||||
const fmtQty = (v: number) => parseFloat(String(v)).toLocaleString();
|
||||
|
||||
export function MaterialInputModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
order,
|
||||
workOrderItemId,
|
||||
workOrderItemName,
|
||||
onComplete,
|
||||
isCompletionFlow = false,
|
||||
onSaveMaterials,
|
||||
}: MaterialInputModalProps) {
|
||||
const [materials, setMaterials] = useState<MaterialForInput[]>([]);
|
||||
const [inputQuantities, setInputQuantities] = useState<Record<string, string>>({});
|
||||
const [selectedLotKeys, setSelectedLotKeys] = useState<Set<string>>(new Set());
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
@@ -70,6 +86,83 @@ export function MaterialInputModal({
|
||||
fifoRank: i + 1,
|
||||
}));
|
||||
|
||||
// 로트 키 생성
|
||||
const getLotKey = (material: MaterialForInput) =>
|
||||
String(material.stockLotId ?? `item-${material.itemId}`);
|
||||
|
||||
// 품목별 그룹핑
|
||||
const materialGroups: MaterialGroup[] = useMemo(() => {
|
||||
const groups = new Map<number, MaterialForInput[]>();
|
||||
for (const m of materials) {
|
||||
const existing = groups.get(m.itemId) || [];
|
||||
existing.push(m);
|
||||
groups.set(m.itemId, existing);
|
||||
}
|
||||
return Array.from(groups.entries()).map(([itemId, lots]) => {
|
||||
const first = lots[0];
|
||||
const itemInput = first as unknown as MaterialForItemInput;
|
||||
const alreadyInputted = itemInput.alreadyInputted ?? 0;
|
||||
const effectiveRequiredQty = Math.max(0, itemInput.remainingRequiredQty ?? first.requiredQty);
|
||||
return {
|
||||
itemId,
|
||||
materialName: first.materialName,
|
||||
materialCode: first.materialCode,
|
||||
requiredQty: first.requiredQty,
|
||||
effectiveRequiredQty,
|
||||
alreadyInputted,
|
||||
unit: first.unit,
|
||||
lots: lots.sort((a, b) => a.fifoRank - b.fifoRank),
|
||||
};
|
||||
});
|
||||
}, [materials]);
|
||||
|
||||
// 선택된 로트에 FIFO 순서로 자동 배분 계산
|
||||
const allocations = useMemo(() => {
|
||||
const result = new Map<string, number>();
|
||||
for (const group of materialGroups) {
|
||||
let remaining = group.effectiveRequiredQty;
|
||||
for (const lot of group.lots) {
|
||||
const lotKey = getLotKey(lot);
|
||||
if (selectedLotKeys.has(lotKey) && lot.stockLotId && remaining > 0) {
|
||||
const alloc = Math.min(lot.lotAvailableQty, remaining);
|
||||
result.set(lotKey, alloc);
|
||||
remaining -= alloc;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, [materialGroups, selectedLotKeys]);
|
||||
|
||||
// 전체 배정 완료 여부
|
||||
const allGroupsFulfilled = useMemo(() => {
|
||||
if (materialGroups.length === 0) return false;
|
||||
return materialGroups.every((group) => {
|
||||
const allocated = group.lots.reduce(
|
||||
(sum, lot) => sum + (allocations.get(getLotKey(lot)) || 0),
|
||||
0
|
||||
);
|
||||
return group.effectiveRequiredQty <= 0 || allocated >= group.effectiveRequiredQty;
|
||||
});
|
||||
}, [materialGroups, allocations]);
|
||||
|
||||
// 배정된 항목 존재 여부
|
||||
const hasAnyAllocation = useMemo(() => {
|
||||
return Array.from(allocations.values()).some((v) => v > 0);
|
||||
}, [allocations]);
|
||||
|
||||
// 로트 선택/해제
|
||||
const toggleLot = useCallback((lotKey: string) => {
|
||||
setSelectedLotKeys((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(lotKey)) {
|
||||
next.delete(lotKey);
|
||||
} else {
|
||||
next.add(lotKey);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// API로 자재 목록 로드
|
||||
const loadMaterials = useCallback(async () => {
|
||||
if (!order) return;
|
||||
@@ -79,23 +172,17 @@ export function MaterialInputModal({
|
||||
// 목업 아이템인 경우 목업 자재 데이터 사용
|
||||
if (order.id.startsWith('mock-')) {
|
||||
setMaterials(MOCK_MATERIALS);
|
||||
const initialQuantities: Record<string, string> = {};
|
||||
MOCK_MATERIALS.forEach((m) => {
|
||||
initialQuantities[String(m.stockLotId)] = '';
|
||||
});
|
||||
setInputQuantities(initialQuantities);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await getMaterialsForWorkOrder(order.id);
|
||||
// 개소별 API vs 전체 API 분기
|
||||
const result = workOrderItemId
|
||||
? await getMaterialsForItem(order.id, workOrderItemId)
|
||||
: await getMaterialsForWorkOrder(order.id);
|
||||
|
||||
if (result.success) {
|
||||
setMaterials(result.data);
|
||||
const initialQuantities: Record<string, string> = {};
|
||||
result.data.forEach((m) => {
|
||||
initialQuantities[String(m.stockLotId ?? `item-${m.itemId}`)] = '';
|
||||
});
|
||||
setInputQuantities(initialQuantities);
|
||||
} else {
|
||||
toast.error(result.error || '자재 목록 조회에 실패했습니다.');
|
||||
}
|
||||
@@ -106,74 +193,63 @@ export function MaterialInputModal({
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [order]);
|
||||
}, [order, workOrderItemId]);
|
||||
|
||||
// 모달이 열릴 때 데이터 로드
|
||||
// 모달이 열릴 때 데이터 로드 + 선택 초기화
|
||||
useEffect(() => {
|
||||
if (open && order) {
|
||||
loadMaterials();
|
||||
setSelectedLotKeys(new Set());
|
||||
}
|
||||
}, [open, order, loadMaterials]);
|
||||
|
||||
// 투입 수량 변경 핸들러 (숫자만 허용)
|
||||
const handleQuantityChange = (key: string, value: string) => {
|
||||
const numericValue = value.replace(/[^0-9]/g, '');
|
||||
setInputQuantities((prev) => ({
|
||||
...prev,
|
||||
[key]: numericValue,
|
||||
}));
|
||||
};
|
||||
|
||||
// 로트 키 생성
|
||||
const getLotKey = (material: MaterialForInput) =>
|
||||
String(material.stockLotId ?? `item-${material.itemId}`);
|
||||
|
||||
// 투입 등록
|
||||
const handleSubmit = async () => {
|
||||
if (!order) return;
|
||||
|
||||
// 투입 수량이 입력된 항목 필터 (재고 있는 로트만)
|
||||
const materialsWithQuantity = materials.filter((m) => {
|
||||
if (!m.stockLotId) return false;
|
||||
const qty = inputQuantities[getLotKey(m)];
|
||||
return qty && parseInt(qty) > 0;
|
||||
});
|
||||
|
||||
if (materialsWithQuantity.length === 0) {
|
||||
toast.error('투입 수량을 입력해주세요.');
|
||||
return;
|
||||
// 배분된 로트만 추출
|
||||
const inputs: { stock_lot_id: number; qty: number }[] = [];
|
||||
for (const [lotKey, allocQty] of allocations) {
|
||||
if (allocQty > 0) {
|
||||
const material = materials.find((m) => getLotKey(m) === lotKey);
|
||||
if (material?.stockLotId) {
|
||||
inputs.push({ stock_lot_id: material.stockLotId, qty: allocQty });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 가용수량 초과 검증
|
||||
for (const m of materialsWithQuantity) {
|
||||
const inputQty = parseInt(inputQuantities[getLotKey(m)] || '0');
|
||||
if (inputQty > m.lotAvailableQty) {
|
||||
toast.error(`${m.lotNo}: 가용수량(${m.lotAvailableQty})을 초과할 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
if (inputs.length === 0) {
|
||||
toast.error('투입할 로트를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const inputs = materialsWithQuantity.map((m) => ({
|
||||
stock_lot_id: m.stockLotId!,
|
||||
qty: parseInt(inputQuantities[getLotKey(m)] || '0'),
|
||||
}));
|
||||
|
||||
const result = await registerMaterialInput(order.id, inputs);
|
||||
// 개소별 API vs 전체 API 분기
|
||||
const result = workOrderItemId
|
||||
? await registerMaterialInputForItem(order.id, workOrderItemId, inputs)
|
||||
: await registerMaterialInput(order.id, inputs);
|
||||
|
||||
if (result.success) {
|
||||
toast.success('자재 투입이 등록되었습니다.');
|
||||
|
||||
if (onSaveMaterials) {
|
||||
const savedList: MaterialInput[] = materialsWithQuantity.map((m) => ({
|
||||
id: String(m.stockLotId),
|
||||
lotNo: m.lotNo || '',
|
||||
materialName: m.materialName,
|
||||
quantity: m.lotAvailableQty,
|
||||
unit: m.unit,
|
||||
inputQuantity: parseInt(inputQuantities[getLotKey(m)] || '0'),
|
||||
}));
|
||||
const savedList: MaterialInput[] = [];
|
||||
for (const [lotKey, allocQty] of allocations) {
|
||||
if (allocQty > 0) {
|
||||
const material = materials.find((m) => getLotKey(m) === lotKey);
|
||||
if (material) {
|
||||
savedList.push({
|
||||
id: String(material.stockLotId),
|
||||
lotNo: material.lotNo || '',
|
||||
materialName: material.materialName,
|
||||
quantity: material.lotAvailableQty,
|
||||
unit: material.unit,
|
||||
inputQuantity: allocQty,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
onSaveMaterials(order.id, savedList);
|
||||
}
|
||||
|
||||
@@ -194,12 +270,8 @@ export function MaterialInputModal({
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
resetAndClose();
|
||||
};
|
||||
|
||||
const resetAndClose = () => {
|
||||
setInputQuantities({});
|
||||
setSelectedLotKeys(new Set());
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
@@ -210,94 +282,177 @@ export function MaterialInputModal({
|
||||
<DialogContent className="!max-w-3xl p-0 gap-0">
|
||||
{/* 헤더 */}
|
||||
<DialogHeader className="p-6 pb-4">
|
||||
<DialogTitle className="text-xl font-semibold">자재 투입</DialogTitle>
|
||||
<DialogTitle className="text-xl font-semibold">
|
||||
자재 투입{workOrderItemName ? ` - ${workOrderItemName}` : ''}
|
||||
</DialogTitle>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
로트를 선택하면 필요수량만큼 자동 배분됩니다.
|
||||
</p>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="px-6 pb-6 space-y-6">
|
||||
{/* 자재 목록 테이블 */}
|
||||
<div className="px-6 pb-6 space-y-4">
|
||||
{/* 자재 목록 */}
|
||||
{isLoading ? (
|
||||
<ContentSkeleton type="table" rows={4} />
|
||||
) : materials.length === 0 ? (
|
||||
<div className="border rounded-lg">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableHead className="text-center">로트번호</TableHead>
|
||||
<TableHead className="text-center">품목명</TableHead>
|
||||
<TableHead className="text-center">필요수량</TableHead>
|
||||
<TableHead className="text-center">가용수량</TableHead>
|
||||
<TableHead className="text-center">단위</TableHead>
|
||||
<TableHead className="text-center">투입 수량</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-12 text-gray-500">
|
||||
이 공정에 배정된 자재가 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="border rounded-lg py-12 text-center text-sm text-gray-500">
|
||||
이 공정에 배정된 자재가 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableHead className="text-center font-medium">로트번호</TableHead>
|
||||
<TableHead className="text-center font-medium">품목명</TableHead>
|
||||
<TableHead className="text-center font-medium">필요수량</TableHead>
|
||||
<TableHead className="text-center font-medium">가용수량</TableHead>
|
||||
<TableHead className="text-center font-medium">단위</TableHead>
|
||||
<TableHead className="text-center font-medium">투입 수량</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{materials.map((material, index) => {
|
||||
const lotKey = getLotKey(material);
|
||||
const hasStock = material.stockLotId !== null;
|
||||
return (
|
||||
<TableRow key={`mat-${lotKey}-${index}`}>
|
||||
<TableCell className="text-center text-sm">
|
||||
{material.lotNo || (
|
||||
<span className="text-gray-400">재고 없음</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm">
|
||||
{material.materialName}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm">
|
||||
{material.requiredQty.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm">
|
||||
{hasStock ? material.lotAvailableQty.toLocaleString() : (
|
||||
<span className="text-red-500">0</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm">
|
||||
{material.unit}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{hasStock ? (
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
placeholder="0"
|
||||
value={inputQuantities[lotKey] || ''}
|
||||
onChange={(e) =>
|
||||
handleQuantityChange(lotKey, e.target.value)
|
||||
}
|
||||
className="w-20 mx-auto text-center h-8 text-sm"
|
||||
/>
|
||||
<div className="space-y-4 max-h-[60vh] overflow-y-auto">
|
||||
{materialGroups.map((group) => {
|
||||
const groupAllocated = group.lots.reduce(
|
||||
(sum, lot) => sum + (allocations.get(getLotKey(lot)) || 0),
|
||||
0
|
||||
);
|
||||
const isAlreadyComplete = group.effectiveRequiredQty <= 0;
|
||||
const isFulfilled = isAlreadyComplete || groupAllocated >= group.effectiveRequiredQty;
|
||||
|
||||
return (
|
||||
<div key={group.itemId} className="border rounded-lg overflow-hidden">
|
||||
{/* 품목 그룹 헤더 */}
|
||||
<div className="flex items-center justify-between px-4 py-2.5 bg-gray-50 border-b">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-gray-900">
|
||||
{group.materialName}
|
||||
</span>
|
||||
{group.materialCode && (
|
||||
<span className="text-xs text-gray-400">
|
||||
{group.materialCode}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-gray-500">
|
||||
{group.alreadyInputted > 0 ? (
|
||||
<>
|
||||
필요:{' '}
|
||||
<span className="font-semibold text-gray-900">
|
||||
{fmtQty(group.effectiveRequiredQty)}
|
||||
</span>{' '}
|
||||
{group.unit}
|
||||
<span className="ml-1 text-gray-400">
|
||||
(기투입: {fmtQty(group.alreadyInputted)})
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-gray-400 text-sm">-</span>
|
||||
<>
|
||||
필요:{' '}
|
||||
<span className="font-semibold text-gray-900">
|
||||
{fmtQty(group.requiredQty)}
|
||||
</span>{' '}
|
||||
{group.unit}
|
||||
</>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</span>
|
||||
<span
|
||||
className={`text-xs font-semibold px-2 py-0.5 rounded-full flex items-center gap-1 ${
|
||||
isAlreadyComplete
|
||||
? 'bg-emerald-100 text-emerald-700'
|
||||
: isFulfilled
|
||||
? 'bg-emerald-100 text-emerald-700'
|
||||
: groupAllocated > 0
|
||||
? 'bg-amber-100 text-amber-700'
|
||||
: 'bg-gray-100 text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{isAlreadyComplete ? (
|
||||
<>
|
||||
<Check className="h-3 w-3" />
|
||||
투입 완료
|
||||
</>
|
||||
) : isFulfilled ? (
|
||||
<>
|
||||
<Check className="h-3 w-3" />
|
||||
배정 완료
|
||||
</>
|
||||
) : (
|
||||
`${fmtQty(groupAllocated)} / ${fmtQty(group.effectiveRequiredQty)}`
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 로트 테이블 */}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="text-center w-20">선택</TableHead>
|
||||
<TableHead className="text-center">로트번호</TableHead>
|
||||
<TableHead className="text-center">가용수량</TableHead>
|
||||
<TableHead className="text-center">단위</TableHead>
|
||||
<TableHead className="text-center">배정수량</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{group.lots.map((lot, idx) => {
|
||||
const lotKey = getLotKey(lot);
|
||||
const hasStock = lot.stockLotId !== null;
|
||||
const isSelected = selectedLotKeys.has(lotKey);
|
||||
const allocated = allocations.get(lotKey) || 0;
|
||||
const canSelect = hasStock && !isAlreadyComplete && (!isFulfilled || isSelected);
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={`${lotKey}-${idx}`}
|
||||
className={
|
||||
isSelected && allocated > 0
|
||||
? 'bg-blue-50/50'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<TableCell className="text-center">
|
||||
{hasStock ? (
|
||||
<button
|
||||
onClick={() => toggleLot(lotKey)}
|
||||
disabled={!canSelect}
|
||||
className={cn(
|
||||
'min-w-[56px] px-3 py-1.5 rounded-lg text-xs font-semibold transition-all',
|
||||
isSelected
|
||||
? 'bg-blue-600 text-white shadow-sm'
|
||||
: canSelect
|
||||
? 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
: 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
{isSelected ? '선택됨' : '선택'}
|
||||
</button>
|
||||
) : null}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm">
|
||||
{lot.lotNo || (
|
||||
<span className="text-gray-400">
|
||||
재고 없음
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm">
|
||||
{hasStock ? (
|
||||
fmtQty(lot.lotAvailableQty)
|
||||
) : (
|
||||
<span className="text-red-500">0</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm">
|
||||
{lot.unit}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm font-medium">
|
||||
{allocated > 0 ? (
|
||||
<span className="text-blue-600">
|
||||
{fmtQty(allocated)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-300">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -305,7 +460,7 @@ export function MaterialInputModal({
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCancel}
|
||||
onClick={resetAndClose}
|
||||
disabled={isSubmitting}
|
||||
className="flex-1 py-6 text-base font-medium"
|
||||
>
|
||||
@@ -313,7 +468,7 @@ export function MaterialInputModal({
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting}
|
||||
disabled={isSubmitting || !hasAnyAllocation}
|
||||
className="flex-1 py-6 text-base font-medium bg-gray-900 hover:bg-gray-800"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
@@ -330,4 +485,4 @@ export function MaterialInputModal({
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user