Files
star-erp/resources/js/Pages/Inventory/Count/Show.tsx

277 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, useForm } from '@inertiajs/react';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/Components/ui/table';
import { Button } from '@/Components/ui/button';
import { Input } from '@/Components/ui/input';
import { Badge } from '@/Components/ui/badge';
import { Save, CheckCircle, Printer, Trash2, ClipboardCheck } from 'lucide-react';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/Components/ui/alert-dialog"
import { Can } from '@/Components/Permission/Can';
export default function Show({ doc }: any) {
// Transform items to form data structure
const { data, setData, put, delete: destroy, processing, transform } = useForm({
items: doc.items.map((item: any) => ({
id: item.id,
counted_qty: item.counted_qty,
notes: item.notes || '',
})),
action: 'save', // 'save' or 'complete'
});
// Helper to update local form data
const updateItem = (index: number, field: string, value: any) => {
const newItems = [...data.items];
newItems[index][field] = value;
setData('items', newItems);
};
const handleSubmit = (action: string) => {
transform((data) => ({
...data,
action: action,
}));
put(route('inventory.count.update', [doc.id]));
};
const handleDelete = () => {
destroy(route('inventory.count.destroy', [doc.id]));
};
const isCompleted = doc.status === 'completed';
// Calculate progress
const totalItems = doc.items.length;
const countedItems = data.items.filter((i: any) => i.counted_qty !== '' && i.counted_qty !== null).length;
const progress = Math.round((countedItems / totalItems) * 100) || 0;
return (
<AuthenticatedLayout
breadcrumbs={[
{ label: '商品與庫存管理', href: '#' },
{ label: '庫存盤點', href: route('inventory.count.index') },
{ label: `盤點單: ${doc.doc_no}`, href: route('inventory.count.show', [doc.id]), isPage: true },
]}
>
<Head title={`盤點單 ${doc.doc_no}`} />
<div className="container mx-auto p-6 max-w-7xl animate-in fade-in duration-500">
<div className="space-y-6">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex items-center gap-4">
<div>
<div className="flex items-center gap-2">
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
<ClipboardCheck className="h-6 w-6 text-primary-main" />
: {doc.doc_no}
</h1>
{doc.status === 'completed' ? (
<Badge className="bg-green-500 hover:bg-green-600"></Badge>
) : (
<Badge className="bg-blue-500 hover:bg-blue-600"></Badge>
)}
</div>
<p className="text-sm text-gray-500 mt-1 font-medium">
: {doc.warehouse_name} <span className="mx-2">|</span> : {doc.created_by} <span className="mx-2">|</span> : {doc.snapshot_date}
</p>
</div>
</div>
<div className="flex items-center gap-2">
{!isCompleted && (
<div className="flex items-center gap-2">
<Can permission="inventory.view">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" size="sm" disabled={processing} className="button-outlined-error">
<Trash2 className="w-4 h-4 mr-2" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={handleDelete} className="bg-red-600 hover:bg-red-700"></AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<Button
variant="outline"
size="sm"
className="button-outlined-primary"
onClick={() => handleSubmit('save')}
disabled={processing}
>
<Save className="w-4 h-4 mr-2" />
</Button>
<Button
size="sm"
className="button-filled-primary"
onClick={() => handleSubmit('complete')}
disabled={processing}
>
<CheckCircle className="w-4 h-4 mr-2" />
</Button>
</Can>
</div>
)}
{isCompleted && (
<Button variant="outline" size="sm" onClick={() => window.print()}>
<Printer className="w-4 h-4 mr-2" />
</Button>
)}
</div>
</div>
{!isCompleted && (
<div className="bg-white rounded-lg shadow-sm border p-6 space-y-4">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-500 font-medium">: {countedItems} / {totalItems} </span>
<span className="font-bold text-primary-main">{progress}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div className="bg-primary-main h-2 rounded-full transition-all duration-300" style={{ width: `${progress}%` }}></div>
</div>
</div>
)}
<div className="bg-white rounded-lg shadow-sm border p-6 space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="font-semibold text-lg text-grey-900"></h3>
<p className="text-sm text-grey-500">
</p>
</div>
</div>
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader className="bg-gray-50">
<TableRow>
<TableHead className="w-[50px] text-center font-medium text-grey-600">#</TableHead>
<TableHead className="font-medium text-grey-600"> / </TableHead>
<TableHead className="font-medium text-grey-600"></TableHead>
<TableHead className="text-right font-medium text-grey-600"></TableHead>
<TableHead className="text-right w-32 font-medium text-grey-600"></TableHead>
<TableHead className="text-right font-medium text-grey-600"></TableHead>
<TableHead className="font-medium text-grey-600"></TableHead>
<TableHead className="font-medium text-grey-600"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{doc.items.map((item: any, index: number) => {
const formItem = data.items[index];
const diff = formItem.counted_qty !== '' && formItem.counted_qty !== null
? (parseFloat(formItem.counted_qty) - item.system_qty)
: 0;
const hasDiff = Math.abs(diff) > 0.0001;
return (
<TableRow key={item.id} className={hasDiff && formItem.counted_qty !== '' ? "bg-red-50/30" : ""}>
<TableCell className="text-gray-500 font-medium text-center">
{index + 1}
</TableCell>
<TableCell className="py-3">
<div className="flex flex-col">
<span className="font-semibold text-gray-900">{item.product_name}</span>
<span className="text-xs text-gray-500 font-mono">{item.product_code}</span>
</div>
</TableCell>
<TableCell className="text-sm font-mono">{item.batch_number || '-'}</TableCell>
<TableCell className="text-right font-medium">{item.system_qty.toFixed(2)}</TableCell>
<TableCell className="text-right px-1 py-3">
{isCompleted ? (
<span className="font-semibold mr-2">{item.counted_qty}</span>
) : (
<Input
type="number"
step="0.01"
value={formItem.counted_qty ?? ''}
onChange={(e) => updateItem(index, 'counted_qty', e.target.value)}
onWheel={(e: any) => e.target.blur()}
disabled={processing}
className="h-9 text-right font-medium focus:ring-primary-main"
placeholder="盤點..."
/>
)}
</TableCell>
<TableCell className="text-right">
<span className={`font-bold ${!hasDiff
? 'text-gray-400'
: diff > 0
? 'text-green-600'
: 'text-red-600'
}`}>
{formItem.counted_qty !== '' && formItem.counted_qty !== null
? diff.toFixed(2)
: '-'}
</span>
</TableCell>
<TableCell className="text-sm text-gray-500">{item.unit || item.unit_name}</TableCell>
<TableCell className="px-1">
{isCompleted ? (
<span className="text-sm text-gray-600">{item.notes}</span>
) : (
<Input
value={formItem.notes}
onChange={(e) => updateItem(index, 'notes', e.target.value)}
disabled={processing}
className="h-9 text-sm"
placeholder="備註..."
/>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
</div>
<div className="bg-gray-50/80 border border-dashed border-grey-200 rounded-lg p-4 flex items-start gap-3">
<div className="bg-blue-100 p-2 rounded-lg">
<Save className="w-5 h-5 text-blue-600" />
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-1 text-sm"></h4>
<p className="text-xs text-gray-500 leading-relaxed">
</p>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
);
}