Files
sam-react-prod/src/components/material/StockStatus/StockAuditModal.tsx

237 lines
7.7 KiB
TypeScript
Raw Normal View History

'use client';
/**
*
*
* :
* - (, , , , )
* - /
* -
*/
import { useState, useEffect, useCallback } from 'react';
import { Loader2 } from 'lucide-react';
import { ContentSkeleton } from '@/components/ui/skeleton';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { toast } from 'sonner';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { updateStockAudit } from './actions';
import type { StockItem } from './types';
interface StockAuditModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
stocks: StockItem[];
onComplete?: () => void;
}
interface AuditItem {
id: string;
itemCode: string;
itemName: string;
specification: string;
unit: string;
actualQty: number;
newActualQty: number;
}
export function StockAuditModal({
open,
onOpenChange,
stocks,
onComplete,
}: StockAuditModalProps) {
const [auditItems, setAuditItems] = useState<AuditItem[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
// 모달이 열릴 때 데이터 초기화
useEffect(() => {
if (open && stocks.length > 0) {
setAuditItems(
stocks.map((stock) => ({
id: stock.id,
itemCode: stock.itemCode,
itemName: stock.itemName,
specification: stock.specification || '',
unit: stock.unit,
actualQty: stock.actualQty,
newActualQty: stock.actualQty,
}))
);
}
}, [open, stocks]);
// 실제 재고량 변경 핸들러
const handleQtyChange = useCallback((id: string, value: string) => {
const numValue = value === '' ? 0 : parseFloat(value);
if (isNaN(numValue)) return;
setAuditItems((prev) =>
prev.map((item) =>
item.id === id ? { ...item, newActualQty: numValue } : item
)
);
}, []);
// 저장
const handleSubmit = async () => {
// 변경된 항목만 필터링
const changedItems = auditItems.filter(
(item) => item.actualQty !== item.newActualQty
);
if (changedItems.length === 0) {
toast.info('변경된 항목이 없습니다.');
return;
}
setIsSubmitting(true);
try {
const updates = changedItems.map((item) => ({
id: item.id,
actualQty: item.newActualQty,
}));
const result = await updateStockAudit(updates);
if (result.success) {
toast.success(`${changedItems.length}개 항목의 재고가 업데이트되었습니다.`);
onOpenChange(false);
onComplete?.();
} else {
toast.error(result.error || '재고 실사 저장에 실패했습니다.');
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[StockAuditModal] handleSubmit error:', error);
toast.error('재고 실사 저장 중 오류가 발생했습니다.');
} finally {
setIsSubmitting(false);
}
};
// 취소
const handleCancel = () => {
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[95vw] md:max-w-[1200px] w-full p-0 gap-0 max-h-[80vh] flex flex-col">
{/* 헤더 */}
<DialogHeader className="p-6 pb-4 flex-shrink-0">
<DialogTitle className="text-xl font-semibold"> </DialogTitle>
</DialogHeader>
<div className="px-6 pb-6 space-y-6 flex-1 overflow-hidden flex flex-col">
{/* 테이블 */}
{isLoading ? (
<ContentSkeleton type="table" rows={6} />
) : auditItems.length === 0 ? (
<div className="border rounded-lg flex-1">
<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>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell colSpan={5} className="text-center py-12 text-gray-500">
.
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
) : (
<div className="border rounded-lg overflow-auto flex-1">
<Table className="w-full">
<TableHeader className="sticky top-0 bg-gray-50 z-10">
<TableRow className="bg-gray-50">
<TableHead className="text-center font-medium w-[15%]"></TableHead>
<TableHead className="text-center font-medium w-[30%]"></TableHead>
<TableHead className="text-center font-medium w-[20%]"></TableHead>
<TableHead className="text-center font-medium w-[10%]"></TableHead>
<TableHead className="text-center font-medium w-[15%]"> </TableHead>
</TableRow>
</TableHeader>
<TableBody>
{auditItems.map((item) => (
<TableRow key={item.id}>
<TableCell className="text-center font-medium">
{item.itemCode}
</TableCell>
<TableCell className="text-center max-w-[200px] truncate" title={item.itemName}>
{item.itemName}
</TableCell>
<TableCell className="text-center">{item.specification || '-'}</TableCell>
<TableCell className="text-center">{item.unit}</TableCell>
<TableCell className="text-center">
<Input
type="number"
value={item.newActualQty}
onChange={(e) => handleQtyChange(item.id, e.target.value)}
className="w-24 text-center mx-auto"
min={0}
step={1}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
{/* 버튼 영역 */}
<div className="flex gap-3 flex-shrink-0">
<Button
variant="outline"
onClick={handleCancel}
disabled={isSubmitting}
className="flex-1 py-6 text-base font-medium"
>
</Button>
<Button
onClick={handleSubmit}
disabled={isSubmitting}
className="flex-1 py-6 text-base font-medium bg-gray-900 hover:bg-gray-800"
>
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
'저장'
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}