Files
star-erp/resources/js/Pages/Sales/Import/Show.tsx
sky121113 b6fe9ad9f3
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 1m8s
feat: 實作銷售單匯入管理、貨道扣庫優化及 UI 細節調整
2026-02-09 14:36:47 +08:00

339 lines
18 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, Link, useForm, router } from '@inertiajs/react'; // Add router import
import { useState, useEffect } from "react";
import { SearchableSelect } from "@/Components/ui/searchable-select";
import { Button } from '@/Components/ui/button';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/Components/ui/table';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/Components/ui/alert-dialog";
import { Badge } from "@/Components/ui/badge";
import { ArrowLeft, CheckCircle, Trash2, Printer } from 'lucide-react';
import { format } from 'date-fns';
import Pagination from "@/Components/shared/Pagination";
interface ImportItem {
id: number;
transaction_serial: string;
machine_id: string;
slot: string | null;
product_code: string;
product_id: number | null;
product?: {
name: string;
};
quantity: number;
amount: number;
transaction_at: string;
original_status: string;
warehouse?: {
name: string;
};
}
interface ImportBatch {
id: number;
import_date: string;
status: 'pending' | 'confirmed';
total_quantity: number;
total_amount: number;
items: ImportItem[]; // Note: items might be paginated in props, handled below
created_at: string;
confirmed_at?: string;
}
interface Props {
import: ImportBatch;
items: {
data: ImportItem[];
links: any[];
current_page: number;
per_page: number;
total: number;
};
filters?: {
per_page?: string;
};
flash?: {
success?: string;
error?: string;
};
}
export default function SalesImportShow({ import: batch, items, filters = {} }: Props) {
const { post, processing } = useForm({});
const [perPage, setPerPage] = useState(filters?.per_page?.toString() || "10");
// Sync state with prop if it changes via navigation
useEffect(() => {
if (filters?.per_page) {
setPerPage(filters.per_page.toString());
}
}, [filters?.per_page]);
const handlePerPageChange = (value: string) => {
setPerPage(value);
router.get(
route("sales-imports.show", batch.id),
{ per_page: value },
{ preserveState: true, preserveScroll: true, replace: true }
);
};
const handleConfirm = () => {
post(route('sales-imports.confirm', batch.id));
};
const handleDelete = () => {
router.delete(route('sales-imports.destroy', batch.id));
};
return (
<AuthenticatedLayout
breadcrumbs={[
{ label: '銷售管理', href: '#' },
{ label: '銷售單匯入', href: route('sales-imports.index') },
{ label: '匯入明細', href: '#', isPage: true },
]}
>
<Head title={`匯入批次 #${batch.id}`} />
<div className="container mx-auto p-6 max-w-7xl">
{/* Header */}
<div className="mb-6">
<Link href={route('sales-imports.index')}>
<Button
variant="outline"
className="gap-2 button-outlined-primary mb-6"
>
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
<CheckCircle className="h-6 w-6 text-primary-main" />
</h1>
<p className="text-gray-500 mt-1">#{batch.id} | {format(new Date(batch.created_at), 'yyyy/MM/dd HH:mm')}</p>
</div>
<div className="flex items-center gap-3">
<Badge variant={batch.status === 'confirmed' ? 'default' : 'secondary'}>
{batch.status === 'confirmed' ? '已確認' : '待確認'}
</Badge>
{batch.status === 'pending' && (
<>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="outline"
className="gap-2 button-outlined-error"
>
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
#{batch.id}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
className="bg-red-600 hover:bg-red-700"
onClick={handleDelete}
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
className="button-filled-primary gap-2"
disabled={processing}
>
<CheckCircle className="h-4 w-4" />
{processing ? '處理中...' : '確認並扣庫'}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
className="button-filled-primary"
onClick={handleConfirm}
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)}
{batch.status === 'confirmed' && (
<Button variant="outline" className="gap-2 button-outlined-primary">
<Printer className="h-4 w-4" />
</Button>
)}
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-8">
{/* 統計資訊卡片 */}
<div className="bg-white rounded-xl border border-gray-200 shadow-sm p-6">
<h2 className="text-lg font-bold text-gray-900 mb-6 border-b pb-4"></h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-x-8 gap-y-6">
<div>
<span className="text-sm text-gray-500 block mb-1 font-medium"></span>
<span className="text-2xl font-bold text-gray-900">{Math.floor(batch.total_quantity || 0).toLocaleString()}</span>
</div>
<div>
<span className="text-sm text-gray-500 block mb-1 font-medium"></span>
<span className="text-2xl font-bold text-primary-main">
NT$ {Number(batch.total_amount || 0).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 })}
</span>
</div>
<div>
<span className="text-sm text-gray-500 block mb-1 font-medium"></span>
<span className="text-2xl font-bold text-gray-900">
{batch.confirmed_at ? format(new Date(batch.confirmed_at), 'yyyy/MM/dd HH:mm') : '--'}
</span>
</div>
</div>
</div>
{/* 匯入明細清單 */}
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
<div className="p-6 border-b border-gray-100 bg-gray-50/30">
<h2 className="text-lg font-bold text-gray-900"></h2>
</div>
<div className="p-6">
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-gray-50 hover:bg-gray-50">
<TableHead className="w-[80px] text-center">#</TableHead>
<TableHead> / </TableHead>
<TableHead> ()</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="w-[120px] text-center"> / </TableHead>
<TableHead className="text-center"></TableHead>
<TableHead className="text-right w-[100px]"></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.data.length === 0 ? (
<TableRow>
<TableCell colSpan={9} className="h-24 text-center text-gray-500">
</TableCell>
</TableRow>
) : (
items.data.map((item, index) => (
<TableRow key={item.id}>
<TableCell className="text-center text-gray-500">
{(items.current_page - 1) * items.per_page + index + 1}
</TableCell>
<TableCell>
<div className="flex flex-col">
<span className="font-mono text-sm font-bold text-gray-900">{item.transaction_serial}</span>
<span className="text-[10px] text-gray-400">
{format(new Date(item.transaction_at), 'yyyy/MM/dd HH:mm:ss')}
</span>
</div>
</TableCell>
<TableCell>
<div className="flex flex-col">
<span className="text-sm font-medium text-gray-900">{item.warehouse?.name || '--'}</span>
<span className="font-mono text-[10px] text-gray-400">{item.machine_id}</span>
</div>
</TableCell>
<TableCell>
<div className="font-mono text-sm font-bold text-gray-900">{item.product_code}</div>
</TableCell>
<TableCell>
<div className="text-sm text-gray-600 truncate max-w-[200px]" title={item.product?.name}>
{item.product?.name || '--'}
</div>
</TableCell>
<TableCell className="text-center font-bold">
{item.slot || '--'}
</TableCell>
<TableCell className="text-center">
<Badge variant="outline" className={item.original_status === '已出貨' ? "text-green-600 border-green-200 bg-green-50" : "text-gray-500"}>
{item.original_status}
</Badge>
</TableCell>
<TableCell className="text-right font-medium">{Math.floor(item.quantity)}</TableCell>
<TableCell className="text-right font-bold text-primary">
NT$ {Number(item.amount).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 })}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* Pagination */}
<div className="mt-6 flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="flex items-center gap-2 text-sm text-gray-500">
<span></span>
<SearchableSelect
value={perPage}
onValueChange={handlePerPageChange}
options={[
{ label: "10", value: "10" },
{ label: "20", value: "20" },
{ label: "50", value: "50" },
{ label: "100", value: "100" },
]}
className="w-[100px] h-8"
showSearch={false}
/>
<span></span>
</div>
<Pagination links={items.links} />
</div>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
);
}