feat(Inventory): 實作批號溯源完整功能與 UI 呈現,包含文字敘述卡片與更完整的關聯屬性

This commit is contained in:
2026-02-26 10:39:24 +08:00
parent 63e4f88a14
commit f960aaaeb2
16 changed files with 1085 additions and 694 deletions

View File

@@ -0,0 +1,154 @@
import React, { useState } from 'react';
import { Head, router } from '@inertiajs/react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { PageProps } from '@/types/global';
import { Card, CardContent, CardHeader, CardTitle } from '@/Components/ui/card';
import { Button } from '@/Components/ui/button';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
import { TrendingUp, Search, RotateCcw } from 'lucide-react';
import { RadioGroup, RadioGroupItem } from '@/Components/ui/radio-group';
import { cn } from '@/lib/utils';
import TreeView, { TraceabilityNode } from './Components/TreeView';
import { TraceabilitySummary } from './Components/TraceabilitySummary';
import { Can } from '@/Components/Permission/Can';
interface Props extends PageProps {
search: {
batch_number: string | null;
direction: 'backward' | 'forward';
};
result: TraceabilityNode | null;
}
export default function TraceabilityIndex({ search, result }: Props) {
const [batchNumber, setBatchNumber] = useState(search.batch_number || '');
const [direction, setDirection] = useState<'backward' | 'forward'>(search.direction || 'backward');
const [isSearching, setIsSearching] = useState(false);
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
if (!batchNumber.trim()) return;
setIsSearching(true);
router.get(
route('inventory.traceability.index'),
{ batch_number: batchNumber.trim(), direction },
{
preserveState: true,
preserveScroll: true,
onFinish: () => setIsSearching(false)
}
);
};
return (
<AuthenticatedLayout
breadcrumbs={[
{ label: '報表管理', href: '#' },
{ label: '批號溯源', href: route('inventory.traceability.index'), isPage: true },
]}
>
<Head title="批號溯源 - Star ERP" />
<div className="container mx-auto p-6 max-w-7xl">
<div className="mb-6">
<div className="mb-4">
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
<TrendingUp className="h-6 w-6 text-primary-main" />
</h1>
<p className="text-gray-500 mt-1">
</p>
</div>
</div>
<Can permission="inventory.traceability.view">
<Card className="mb-6 bg-white shadow-sm border-gray-200">
<CardHeader className="pb-3 border-b border-gray-100 bg-gray-50/50">
<CardTitle className="text-lg flex items-center gap-2 text-gray-800">
<Search className="h-5 w-5 text-primary-main" />
</CardTitle>
</CardHeader>
<CardContent className="pt-6">
<form onSubmit={handleSearch} className="flex flex-col md:flex-row gap-6 items-start md:items-end">
<div className="flex-1 w-full space-y-1.5">
<Label htmlFor="batchNumber" className="text-sm font-medium text-grey-1"></Label>
<Input
id="batchNumber"
type="text"
placeholder="請輸入欲查詢的批號 (例如PROD-TW-20240101-01)"
value={batchNumber}
onChange={(e) => setBatchNumber(e.target.value)}
className="max-w-md w-full"
/>
</div>
<div className="space-y-3">
<Label className="text-gray-700 font-medium"></Label>
<RadioGroup
value={direction}
onValueChange={(val: 'backward' | 'forward') => setDirection(val)}
className="flex space-x-6"
>
<div
className={cn(
"flex items-center space-x-2 px-4 py-2 rounded-lg border cursor-pointer transition-colors",
direction === 'backward' ? "bg-primary-lightest border-primary-light" : "bg-gray-50 border-gray-200 hover:bg-gray-100"
)}
>
<RadioGroupItem value="backward" id="backward" />
<Label htmlFor="backward" className="cursor-pointer flex items-center gap-1.5 font-medium">
<RotateCcw className="h-4 w-4 text-primary-main" />
( )
</Label>
</div>
<div
className={cn(
"flex items-center space-x-2 px-4 py-2 rounded-lg border cursor-pointer transition-colors",
direction === 'forward' ? "bg-primary-lightest border-primary-light" : "bg-gray-50 border-gray-200 hover:bg-gray-100"
)}
>
<RadioGroupItem value="forward" id="forward" />
<Label htmlFor="forward" className="cursor-pointer flex items-center gap-1.5 font-medium">
<TrendingUp className="h-4 w-4 text-primary-main" />
( )
</Label>
</div>
</RadioGroup>
</div>
<Button
type="submit"
disabled={isSearching || !batchNumber.trim()}
className="button-filled-primary min-w-[120px]"
>
{isSearching ? '查詢中...' : '開始查詢'}
</Button>
</form>
</CardContent>
</Card>
{search.batch_number && (
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden p-6 md:p-8">
{result ? (
<>
<TraceabilitySummary data={result} direction={search.direction || 'backward'} />
<TreeView data={result} />
</>
) : (
<div className="py-16 flex flex-col items-center justify-center text-gray-500">
<Search className="h-12 w-12 text-gray-300 mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-1"></h3>
<p>{search.batch_number}</p>
</div>
)}
</div>
)}
</Can>
</div>
</AuthenticatedLayout>
);
}