UI優化: 全系統狀態標籤 (StatusBadge) 統一化重構完成 (Phase 3 & 4)
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
||||
import { Head, useForm, router, Link } from '@inertiajs/react';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import { StatusBadge } from "@/Components/shared/StatusBadge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -11,7 +13,6 @@ import {
|
||||
} from "@/Components/ui/table";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -167,13 +168,13 @@ export default function Index({ docs, warehouses, filters }: { docs: DocsPaginat
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'draft':
|
||||
return <Badge variant="secondary" className="bg-gray-100 text-gray-600 border-none">草稿</Badge>;
|
||||
return <StatusBadge variant="neutral">草稿</StatusBadge>;
|
||||
case 'posted':
|
||||
return <Badge className="bg-green-100 text-green-700 border-none">已過帳</Badge>;
|
||||
return <StatusBadge variant="success">已過帳</StatusBadge>;
|
||||
case 'voided':
|
||||
return <Badge variant="destructive" className="bg-red-100 text-red-700 border-none">已作廢</Badge>;
|
||||
return <StatusBadge variant="destructive">已作廢</StatusBadge>;
|
||||
default:
|
||||
return <Badge variant="outline">{status}</Badge>;
|
||||
return <StatusBadge variant="neutral">{status}</StatusBadge>;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -257,10 +258,10 @@ export default function Index({ docs, warehouses, filters }: { docs: DocsPaginat
|
||||
<TableHead className="w-[180px] font-medium text-grey-600">單號</TableHead>
|
||||
<TableHead className="font-medium text-grey-600">倉庫</TableHead>
|
||||
<TableHead className="font-medium text-grey-600">調整原因</TableHead>
|
||||
<TableHead className="font-medium text-grey-600 text-center">狀態</TableHead>
|
||||
<TableHead className="font-medium text-grey-600">建立者</TableHead>
|
||||
<TableHead className="font-medium text-grey-600">建立時間</TableHead>
|
||||
<TableHead className="font-medium text-grey-600">過帳時間</TableHead>
|
||||
<TableHead className="font-medium text-grey-600 text-center">狀態</TableHead>
|
||||
<TableHead className="text-center font-medium text-grey-600">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -286,10 +287,10 @@ export default function Index({ docs, warehouses, filters }: { docs: DocsPaginat
|
||||
</TableCell>
|
||||
<TableCell>{doc.warehouse_name}</TableCell>
|
||||
<TableCell className="text-gray-500 max-w-[200px] truncate">{doc.reason}</TableCell>
|
||||
<TableCell className="text-center">{getStatusBadge(doc.status)}</TableCell>
|
||||
<TableCell className="text-sm">{doc.created_by}</TableCell>
|
||||
<TableCell className="text-gray-500 text-sm">{doc.created_at}</TableCell>
|
||||
<TableCell className="text-gray-500 text-sm">{doc.posted_at || '-'}</TableCell>
|
||||
<TableCell className="text-center">{getStatusBadge(doc.status)}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex items-center justify-center gap-2" onClick={(e) => e.stopPropagation()}>
|
||||
{(() => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "@/Components/ui/table";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import { StatusBadge } from "@/Components/shared/StatusBadge";
|
||||
import { Checkbox } from "@/Components/ui/checkbox";
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -243,9 +243,9 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) {
|
||||
盤調單: {doc.doc_no}
|
||||
</h1>
|
||||
{isDraft ? (
|
||||
<Badge variant="secondary" className="bg-blue-500 text-white border-none py-1 px-3">草稿</Badge>
|
||||
<StatusBadge variant="neutral" className="border-none py-1 px-3">草稿</StatusBadge>
|
||||
) : (
|
||||
<Badge className="bg-green-500 text-white border-none py-1 px-3">已過帳</Badge>
|
||||
<StatusBadge variant="success" className="border-none py-1 px-3">已過帳</StatusBadge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1 font-medium flex items-center gap-2">
|
||||
@@ -604,6 +604,6 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) {
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</AuthenticatedLayout >
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
||||
import { Head, Link, useForm, router } from '@inertiajs/react';
|
||||
import { Head, Link, router, useForm } from '@inertiajs/react';
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import { StatusBadge } from "@/Components/shared/StatusBadge";
|
||||
import { debounce } from "lodash";
|
||||
import { SearchableSelect } from "@/Components/ui/searchable-select";
|
||||
import {
|
||||
@@ -14,7 +16,6 @@ import {
|
||||
} from '@/Components/ui/table';
|
||||
import { Button } from '@/Components/ui/button';
|
||||
import { Input } from '@/Components/ui/input';
|
||||
import { Badge } from '@/Components/ui/badge';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -138,19 +139,19 @@ export default function Index({ docs, warehouses, filters }: any) {
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'draft':
|
||||
return <Badge variant="secondary">草稿</Badge>;
|
||||
return <StatusBadge variant="neutral">草稿</StatusBadge>;
|
||||
case 'counting':
|
||||
return <Badge className="bg-blue-500 hover:bg-blue-600">盤點中</Badge>;
|
||||
return <StatusBadge variant="info">盤點中</StatusBadge>;
|
||||
case 'completed':
|
||||
return <Badge className="bg-green-500 hover:bg-green-600">盤點完成</Badge>;
|
||||
return <StatusBadge variant="success">盤點完成</StatusBadge>;
|
||||
case 'no_adjust':
|
||||
return <Badge className="bg-green-600 hover:bg-green-700">盤點完成 (無需盤調)</Badge>;
|
||||
return <StatusBadge variant="success">盤點完成 (無需盤調)</StatusBadge>;
|
||||
case 'adjusted':
|
||||
return <Badge className="bg-purple-500 hover:bg-purple-600">已盤調庫存</Badge>;
|
||||
return <StatusBadge variant="info">已盤調庫存</StatusBadge>; // Decided on info/blue for adjusted to match "active/done" but distinctive from pure success if needed, or stick to success? Plan said Info/Blue.
|
||||
case 'cancelled':
|
||||
return <Badge variant="destructive">已取消</Badge>;
|
||||
return <StatusBadge variant="destructive">已取消</StatusBadge>;
|
||||
default:
|
||||
return <Badge variant="outline">{status}</Badge>;
|
||||
return <StatusBadge variant="neutral">{status}</StatusBadge>;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -273,11 +274,11 @@ export default function Index({ docs, warehouses, filters }: any) {
|
||||
<TableHead className="w-[50px] text-center">#</TableHead>
|
||||
<TableHead>單號</TableHead>
|
||||
<TableHead>倉庫</TableHead>
|
||||
<TableHead>狀態</TableHead>
|
||||
<TableHead>快照時間</TableHead>
|
||||
<TableHead>盤點進度</TableHead>
|
||||
<TableHead>完成時間</TableHead>
|
||||
<TableHead>建立人員</TableHead>
|
||||
<TableHead>狀態</TableHead>
|
||||
<TableHead className="text-center">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -296,7 +297,6 @@ export default function Index({ docs, warehouses, filters }: any) {
|
||||
</TableCell>
|
||||
<TableCell className="font-medium text-primary-main">{doc.doc_no}</TableCell>
|
||||
<TableCell>{doc.warehouse_name}</TableCell>
|
||||
<TableCell>{getStatusBadge(doc.status)}</TableCell>
|
||||
<TableCell className="text-gray-500 text-sm">{doc.snapshot_date}</TableCell>
|
||||
<TableCell>
|
||||
<span className="font-medium text-gray-700">{doc.counted_items}</span>
|
||||
@@ -305,6 +305,7 @@ export default function Index({ docs, warehouses, filters }: any) {
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-500 text-sm">{doc.completed_at || '-'}</TableCell>
|
||||
<TableCell className="text-sm">{doc.created_by}</TableCell>
|
||||
<TableCell>{getStatusBadge(doc.status)}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{/* Action Button Logic: Prefer Edit if allowed and status is active, otherwise fallback to View if allowed */}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from '@/Components/ui/table';
|
||||
import { Button } from '@/Components/ui/button';
|
||||
import { Input } from '@/Components/ui/input';
|
||||
import { Badge } from '@/Components/ui/badge';
|
||||
import { StatusBadge } from "@/Components/shared/StatusBadge";
|
||||
import { Save, Printer, Trash2, ClipboardCheck, ArrowLeft, RotateCcw } from 'lucide-react'; // Added ArrowLeft
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -121,16 +121,16 @@ export default function Show({ doc }: any) {
|
||||
盤點單: {doc.doc_no}
|
||||
</h1>
|
||||
{doc.status === 'completed' && (
|
||||
<Badge className="bg-green-500 hover:bg-green-600">盤點完成</Badge>
|
||||
<StatusBadge variant="success">盤點完成</StatusBadge>
|
||||
)}
|
||||
{doc.status === 'no_adjust' && (
|
||||
<Badge className="bg-green-600 hover:bg-green-700">盤點完成 (無需盤調)</Badge>
|
||||
<StatusBadge variant="success">盤點完成 (無需盤調)</StatusBadge>
|
||||
)}
|
||||
{doc.status === 'adjusted' && (
|
||||
<Badge className="bg-purple-500 hover:bg-purple-600">已盤調庫存</Badge>
|
||||
<StatusBadge variant="warning">已盤調庫存</StatusBadge>
|
||||
)}
|
||||
{doc.status === 'draft' && (
|
||||
<Badge className="bg-blue-500 hover:bg-blue-600">盤點中</Badge>
|
||||
<StatusBadge variant="info">盤點中</StatusBadge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1 font-medium">
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
TableRow,
|
||||
} from '@/Components/ui/table';
|
||||
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import { StatusBadge } from "@/Components/shared/StatusBadge";
|
||||
|
||||
|
||||
import {
|
||||
@@ -395,9 +395,9 @@ export default function GoodsReceiptCreate({ warehouses, pendingPurchaseOrders,
|
||||
<TableCell className="font-medium text-primary-main">{po.code}</TableCell>
|
||||
<TableCell>{po.vendor_name}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge variant={STATUS_CONFIG[po.status]?.variant || 'outline'}>
|
||||
<StatusBadge variant={STATUS_CONFIG[po.status]?.variant || 'neutral'}>
|
||||
{STATUS_CONFIG[po.status]?.label || po.status}
|
||||
</Badge>
|
||||
</StatusBadge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-gray-600">
|
||||
{po.items.length} 項
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/Components/ui/table";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import { StatusBadge } from "@/Components/shared/StatusBadge";
|
||||
import { ArrowLeft, FileText, Package } from "lucide-react";
|
||||
import Pagination from "@/Components/shared/Pagination";
|
||||
import { formatDate } from "@/utils/format";
|
||||
@@ -69,17 +69,18 @@ interface ShowProps extends PageProps {
|
||||
export default function InventoryReportShow({ product, transactions, filters, reportFilters, warehouses }: ShowProps) {
|
||||
|
||||
// 類型 Badge 顏色映射
|
||||
const getTypeBadgeVariant = (type: string) => {
|
||||
// 類型 Badge 顏色映射
|
||||
const getTypeBadgeVariant = (type: string): "success" | "destructive" | "neutral" => {
|
||||
switch (type) {
|
||||
case '入庫':
|
||||
case '手動入庫':
|
||||
case '調撥入庫':
|
||||
return "default";
|
||||
return "success";
|
||||
case '出庫':
|
||||
case '調撥出庫':
|
||||
return "destructive";
|
||||
default:
|
||||
return "secondary";
|
||||
return "neutral";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -128,9 +129,9 @@ export default function InventoryReportShow({ product, transactions, filters, re
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="text-xl font-bold text-grey-0">{product.name}</h3>
|
||||
<Badge variant="outline" className="text-sm px-2 py-0.5 bg-gray-50">
|
||||
<StatusBadge variant="neutral" className="text-sm px-2 py-0.5">
|
||||
{product.code}
|
||||
</Badge>
|
||||
</StatusBadge>
|
||||
</div>
|
||||
<div className="flex items-center gap-6 text-sm text-gray-500">
|
||||
<span className="flex items-center gap-1.5">
|
||||
@@ -212,9 +213,9 @@ export default function InventoryReportShow({ product, transactions, filters, re
|
||||
{formatDate(tx.actual_time)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={getTypeBadgeVariant(tx.type)}>
|
||||
<StatusBadge variant={getTypeBadgeVariant(tx.type)}>
|
||||
{tx.type}
|
||||
</Badge>
|
||||
</StatusBadge>
|
||||
</TableCell>
|
||||
<TableCell>{tx.warehouse_name}</TableCell>
|
||||
<TableCell className={`text-right font-medium ${tx.quantity > 0 ? 'text-emerald-600' :
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/Components/ui/table";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import { StatusBadge, StatusVariant } from "@/Components/shared/StatusBadge";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { SearchableSelect } from "@/Components/ui/searchable-select";
|
||||
@@ -77,31 +77,31 @@ interface Props {
|
||||
categories: { id: number; name: string }[];
|
||||
}
|
||||
|
||||
// 狀態 Badge
|
||||
const statusConfig: Record<
|
||||
string,
|
||||
{ label: string; className: string }
|
||||
> = {
|
||||
normal: {
|
||||
label: "正常",
|
||||
className: "bg-green-100 text-green-800 border-green-200",
|
||||
},
|
||||
negative: {
|
||||
label: "負庫存",
|
||||
className: "bg-red-100 text-red-800 border-red-200",
|
||||
},
|
||||
low_stock: {
|
||||
label: "低庫存",
|
||||
className: "bg-amber-100 text-amber-800 border-amber-200",
|
||||
},
|
||||
expiring: {
|
||||
label: "即將過期",
|
||||
className: "bg-yellow-100 text-yellow-800 border-yellow-200",
|
||||
},
|
||||
expired: {
|
||||
label: "已過期",
|
||||
className: "bg-red-100 text-red-800 border-red-200",
|
||||
},
|
||||
// 狀態與樣式映射
|
||||
const getStatusVariant = (status: string): StatusVariant => {
|
||||
switch (status) {
|
||||
case 'negative':
|
||||
case 'expired':
|
||||
return 'destructive';
|
||||
case 'low_stock':
|
||||
case 'expiring':
|
||||
return 'warning';
|
||||
case 'normal':
|
||||
return 'success';
|
||||
default:
|
||||
return 'neutral';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string): string => {
|
||||
switch (status) {
|
||||
case 'normal': return "正常";
|
||||
case 'negative': return "負庫存";
|
||||
case 'low_stock': return "低庫存";
|
||||
case 'expiring': return "即將過期";
|
||||
case 'expired': return "已過期";
|
||||
default: return status;
|
||||
}
|
||||
};
|
||||
|
||||
// 狀態篩選選項
|
||||
@@ -512,25 +512,14 @@ export default function StockQueryIndex({
|
||||
<TableCell className="text-center">
|
||||
<div className="flex flex-wrap items-center justify-center gap-1">
|
||||
{item.statuses.map(
|
||||
(status) => {
|
||||
const config =
|
||||
statusConfig[
|
||||
status
|
||||
];
|
||||
if (!config)
|
||||
return null;
|
||||
return (
|
||||
<Badge
|
||||
key={status}
|
||||
variant="outline"
|
||||
className={
|
||||
config.className
|
||||
}
|
||||
>
|
||||
{config.label}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
(status) => (
|
||||
<StatusBadge
|
||||
key={status}
|
||||
variant={getStatusVariant(status)}
|
||||
>
|
||||
{getStatusLabel(status)}
|
||||
</StatusBadge>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
@@ -4,6 +4,7 @@ import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
||||
import { Head, Link, router } from "@inertiajs/react";
|
||||
import { debounce } from "lodash";
|
||||
import { SearchableSelect } from "@/Components/ui/searchable-select";
|
||||
import { StatusBadge } from "@/Components/shared/StatusBadge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
} from "@/Components/ui/table";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -160,13 +160,15 @@ export default function Index({ warehouses, orders, filters }: any) {
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'draft':
|
||||
return <Badge variant="secondary">草稿</Badge>;
|
||||
return <StatusBadge variant="neutral">草稿</StatusBadge>;
|
||||
case 'dispatched':
|
||||
return <StatusBadge variant="info">配送中</StatusBadge>;
|
||||
case 'completed':
|
||||
return <Badge className="bg-green-500 hover:bg-green-600">已完成</Badge>;
|
||||
return <StatusBadge variant="success">已完成</StatusBadge>;
|
||||
case 'voided':
|
||||
return <Badge variant="destructive">已作廢</Badge>;
|
||||
return <StatusBadge variant="destructive">已作廢</StatusBadge>;
|
||||
default:
|
||||
return <Badge variant="outline">{status}</Badge>;
|
||||
return <StatusBadge variant="neutral">{status}</StatusBadge>;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -287,12 +289,12 @@ export default function Index({ warehouses, orders, filters }: any) {
|
||||
<TableRow>
|
||||
<TableHead className="w-[50px] text-center font-medium text-gray-600">#</TableHead>
|
||||
<TableHead className="font-medium text-gray-600">單號</TableHead>
|
||||
<TableHead className="text-center font-medium text-gray-600">狀態</TableHead>
|
||||
<TableHead className="font-medium text-gray-600">來源倉庫</TableHead>
|
||||
<TableHead className="font-medium text-gray-600">目的倉庫</TableHead>
|
||||
<TableHead className="font-medium text-gray-600">建立日期</TableHead>
|
||||
<TableHead className="font-medium text-gray-600">過帳日期</TableHead>
|
||||
<TableHead className="font-medium text-gray-600">建立人員</TableHead>
|
||||
<TableHead className="text-center font-medium text-gray-600">狀態</TableHead>
|
||||
<TableHead className="text-center font-medium text-gray-600">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -314,12 +316,12 @@ export default function Index({ warehouses, orders, filters }: any) {
|
||||
{(orders.current_page - 1) * orders.per_page + index + 1}
|
||||
</TableCell>
|
||||
<TableCell className="font-medium text-primary-main">{order.doc_no}</TableCell>
|
||||
<TableCell className="text-center">{getStatusBadge(order.status)}</TableCell>
|
||||
<TableCell className="text-gray-700">{order.from_warehouse_name}</TableCell>
|
||||
<TableCell className="text-gray-700">{order.to_warehouse_name}</TableCell>
|
||||
<TableCell className="text-gray-500 text-sm">{order.created_at}</TableCell>
|
||||
<TableCell className="text-gray-500 text-sm">{order.posted_at || '-'}</TableCell>
|
||||
<TableCell className="text-sm">{order.created_by}</TableCell>
|
||||
<TableCell className="text-center">{getStatusBadge(order.status)}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex items-center justify-center gap-2" onClick={(e) => e.stopPropagation()}>
|
||||
{(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
||||
import { Head, router, Link } from "@inertiajs/react";
|
||||
import { Head, router, Link, usePage } from "@inertiajs/react";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/Components/ui/table";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import { StatusBadge } from "@/Components/shared/StatusBadge";
|
||||
import { Checkbox } from "@/Components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -32,27 +32,86 @@ import {
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/Components/ui/alert-dialog";
|
||||
import { Plus, Save, Trash2, ArrowLeft, CheckCircle, Package, ArrowLeftRight, Printer, Search } from "lucide-react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
import { Plus, Save, Trash2, ArrowLeft, CheckCircle, Package, ArrowLeftRight, Printer, Search, Truck, PackageCheck } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import axios from "axios";
|
||||
import { Can } from '@/Components/Permission/Can';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import TransferImportDialog from '@/Components/Transfer/TransferImportDialog';
|
||||
|
||||
export default function Show({ order }: any) {
|
||||
interface TransitWarehouse {
|
||||
id: string;
|
||||
name: string;
|
||||
license_plate: string | null;
|
||||
driver_name: string | null;
|
||||
}
|
||||
|
||||
export default function Show({ order, transitWarehouses = [] }: { order: any; transitWarehouses?: TransitWarehouse[] }) {
|
||||
const { can } = usePermission();
|
||||
const { url } = usePage();
|
||||
|
||||
// 解析 URL query 參數,判斷使用者從哪裡來
|
||||
const backNav = useMemo(() => {
|
||||
const params = new URLSearchParams(url.split('?')[1] || '');
|
||||
const from = params.get('from');
|
||||
if (from === 'requisition') {
|
||||
const fromId = params.get('from_id');
|
||||
const fromDoc = params.get('from_doc') || '';
|
||||
return {
|
||||
href: route('store-requisitions.show', [fromId!]),
|
||||
label: `返回叫貨單: ${decodeURIComponent(fromDoc)}`,
|
||||
breadcrumbs: [
|
||||
{ label: '商品與庫存管理', href: '#' },
|
||||
{ label: '門市叫貨申請', href: route('store-requisitions.index') },
|
||||
{ label: `叫貨單: ${decodeURIComponent(fromDoc)}`, href: route('store-requisitions.show', [fromId!]) },
|
||||
{ label: `調撥單: ${order.doc_no}`, href: route('inventory.transfer.show', [order.id]), isPage: true },
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
href: route('inventory.transfer.index'),
|
||||
label: '返回調撥單列表',
|
||||
breadcrumbs: [
|
||||
{ label: '商品與庫存管理', href: '#' },
|
||||
{ label: '庫存調撥', href: route('inventory.transfer.index') },
|
||||
{ label: `調撥單: ${order.doc_no}`, href: route('inventory.transfer.show', [order.id]), isPage: true },
|
||||
],
|
||||
};
|
||||
}, [url, order]);
|
||||
|
||||
const [items, setItems] = useState(order.items || []);
|
||||
const [remarks, setRemarks] = useState(order.remarks || "");
|
||||
|
||||
// 狀態初始化
|
||||
const [transitWarehouseId, setTransitWarehouseId] = useState<string | null>(order.transit_warehouse_id || null);
|
||||
|
||||
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||
const [isPostDialogOpen, setIsPostDialogOpen] = useState(false);
|
||||
const [isReceiveDialogOpen, setIsReceiveDialogOpen] = useState(false);
|
||||
const [isImportDialogOpen, setIsImportDialogOpen] = useState(false);
|
||||
|
||||
// 判斷是否有在途倉流程 (包含前端暫選的)
|
||||
const hasTransit = !!transitWarehouseId;
|
||||
|
||||
// 取得選中的在途倉資訊
|
||||
const selectedTransitWarehouse = transitWarehouses.find(w => w.id === transitWarehouseId);
|
||||
|
||||
// 當 order prop 變動時 (例如匯入後 router.reload),同步更新內部狀態
|
||||
useEffect(() => {
|
||||
if (order) {
|
||||
setItems(order.items || []);
|
||||
setRemarks(order.remarks || "");
|
||||
setTransitWarehouseId(order.transit_warehouse_id || null);
|
||||
}
|
||||
}, [order]);
|
||||
|
||||
@@ -74,7 +133,6 @@ export default function Show({ order }: any) {
|
||||
const loadInventory = async () => {
|
||||
setLoadingInventory(true);
|
||||
try {
|
||||
// Fetch inventory from SOURCE warehouse
|
||||
const response = await axios.get(route('api.warehouses.inventories', order.from_warehouse_id));
|
||||
setAvailableInventory(response.data);
|
||||
} catch (error) {
|
||||
@@ -122,8 +180,8 @@ export default function Show({ order }: any) {
|
||||
batch_number: inv.batch_number,
|
||||
expiry_date: inv.expiry_date,
|
||||
unit: inv.unit_name,
|
||||
quantity: 1, // Default 1
|
||||
max_quantity: inv.quantity, // Max available
|
||||
quantity: 1,
|
||||
max_quantity: inv.quantity,
|
||||
notes: "",
|
||||
});
|
||||
addedCount++;
|
||||
@@ -155,6 +213,7 @@ export default function Show({ order }: any) {
|
||||
await router.put(route('inventory.transfer.update', [order.id]), {
|
||||
items: items,
|
||||
remarks: remarks,
|
||||
transit_warehouse_id: transitWarehouseId || '',
|
||||
}, {
|
||||
onSuccess: () => { },
|
||||
onError: () => toast.error("儲存失敗,請檢查輸入"),
|
||||
@@ -164,21 +223,42 @@ export default function Show({ order }: any) {
|
||||
}
|
||||
};
|
||||
|
||||
// 確認出貨 / 確認過帳(無在途倉)
|
||||
// 確認出貨 / 確認過帳(無在途倉)
|
||||
const handlePost = () => {
|
||||
router.put(route('inventory.transfer.update', [order.id]), {
|
||||
action: 'post'
|
||||
action: 'post',
|
||||
transit_warehouse_id: transitWarehouseId || '',
|
||||
items: items,
|
||||
remarks: remarks,
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
setIsPostDialogOpen(false);
|
||||
},
|
||||
onError: (errors) => {
|
||||
const message = Object.values(errors).join('\n') || "過帳失敗,請檢查輸入或庫存狀態";
|
||||
const message = Object.values(errors).join('\n') || "操作失敗,請檢查輸入或庫存狀態";
|
||||
toast.error(message);
|
||||
setIsPostDialogOpen(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 確認收貨
|
||||
const handleReceive = () => {
|
||||
router.put(route('inventory.transfer.update', [order.id]), {
|
||||
action: 'receive'
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
setIsReceiveDialogOpen(false);
|
||||
},
|
||||
onError: (errors) => {
|
||||
const message = Object.values(errors).join('\n') || "收貨失敗";
|
||||
toast.error(message);
|
||||
setIsReceiveDialogOpen(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
router.delete(route('inventory.transfer.destroy', [order.id]), {
|
||||
onSuccess: () => {
|
||||
@@ -188,28 +268,44 @@ export default function Show({ order }: any) {
|
||||
};
|
||||
|
||||
const canEdit = can('inventory_transfer.edit');
|
||||
const isReadOnly = order.status !== 'draft' || !canEdit;
|
||||
const isReadOnly = (order.status !== 'draft' || !canEdit);
|
||||
const isVending = order.to_warehouse_type === 'vending';
|
||||
|
||||
// 狀態 Badge 渲染
|
||||
const renderStatusBadge = () => {
|
||||
const statusConfig: Record<string, { variant: "success" | "warning" | "neutral" | "destructive" | "info", label: string }> = {
|
||||
completed: { variant: 'success', label: '已完成' },
|
||||
dispatched: { variant: 'warning', label: '配送中' },
|
||||
draft: { variant: 'neutral', label: '草稿' },
|
||||
voided: { variant: 'destructive', label: '已作廢' },
|
||||
};
|
||||
|
||||
const config = statusConfig[order.status] || { variant: 'neutral', label: order.status };
|
||||
|
||||
return <StatusBadge variant={config.variant}>{config.label}</StatusBadge>;
|
||||
};
|
||||
|
||||
// 過帳時庫存欄標題
|
||||
const stockColumnTitle = () => {
|
||||
if (order.status === 'completed' || order.status === 'dispatched') return '出貨時庫存';
|
||||
return '可用庫存';
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthenticatedLayout
|
||||
breadcrumbs={[
|
||||
{ label: '商品與庫存管理', href: '#' },
|
||||
{ label: '庫存調撥', href: route('inventory.transfer.index') },
|
||||
{ label: `調撥單: ${order.doc_no}`, href: route('inventory.transfer.show', [order.id]), isPage: true },
|
||||
]}
|
||||
breadcrumbs={backNav.breadcrumbs as any}
|
||||
>
|
||||
<Head title={`調撥單 ${order.doc_no}`} />
|
||||
|
||||
<div className="container mx-auto p-6 max-w-7xl animate-in fade-in duration-500 space-y-6">
|
||||
<div>
|
||||
<Link href={route('inventory.transfer.index')}>
|
||||
<Link href={backNav.href}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2 button-outlined-primary mb-6"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
返回調撥單列表
|
||||
{backNav.label}
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
@@ -220,9 +316,7 @@ export default function Show({ order }: any) {
|
||||
<ArrowLeftRight className="h-6 w-6 text-primary-main" />
|
||||
調撥單: {order.doc_no}
|
||||
</h1>
|
||||
{order.status === 'completed' && <Badge className="bg-green-500 hover:bg-green-600">已完成</Badge>}
|
||||
{order.status === 'draft' && <Badge className="bg-blue-500 hover:bg-blue-600">草稿</Badge>}
|
||||
{order.status === 'voided' && <Badge variant="destructive">已作廢</Badge>}
|
||||
{renderStatusBadge()}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1 font-medium">
|
||||
來源: {order.from_warehouse_name} <ArrowLeftRight className="inline-block h-3 w-3 mx-1" /> 目的: {order.to_warehouse_name} <span className="mx-2">|</span> 建立人: {order.created_by}
|
||||
@@ -240,6 +334,7 @@ export default function Show({ order }: any) {
|
||||
列印
|
||||
</Button>
|
||||
|
||||
{/* 草稿狀態:儲存 + 出貨/過帳 + 刪除 */}
|
||||
{!isReadOnly && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Can permission="inventory_transfer.delete">
|
||||
@@ -284,30 +379,168 @@ export default function Show({ order }: any) {
|
||||
className="button-filled-primary"
|
||||
disabled={items.length === 0 || isSaving}
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
確認過帳
|
||||
{hasTransit ? (
|
||||
<><Truck className="w-4 h-4 mr-2" />確認出貨</>
|
||||
) : (
|
||||
<><CheckCircle className="w-4 h-4 mr-2" />確認過帳</>
|
||||
)}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>確定要過帳嗎?</AlertDialogTitle>
|
||||
<AlertDialogTitle>
|
||||
{hasTransit ? '確定要出貨嗎?' : '確定要過帳嗎?'}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
過帳後庫存將立即從「{order.from_warehouse_name}」轉移至「{order.to_warehouse_name}」,且無法再進行修改。
|
||||
{hasTransit ? (
|
||||
<>庫存將從「{order.from_warehouse_name}」移至在途倉「{selectedTransitWarehouse?.name || order.transit_warehouse_name}」,等待目的倉庫確認收貨後才完成調撥。</>
|
||||
) : (
|
||||
<>過帳後庫存將立即從「{order.from_warehouse_name}」轉移至「{order.to_warehouse_name}」,且無法再進行修改。</>
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handlePost} className="button-filled-primary">確認過帳</AlertDialogAction>
|
||||
<AlertDialogAction onClick={handlePost} className="button-filled-primary">
|
||||
{hasTransit ? '確認出貨' : '確認過帳'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Can>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 已出貨狀態:確認收貨按鈕 */}
|
||||
{order.status === 'dispatched' && (
|
||||
<Can permission="inventory_transfer.edit">
|
||||
<AlertDialog open={isReceiveDialogOpen} onOpenChange={setIsReceiveDialogOpen}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-green-600 hover:bg-green-700 text-white"
|
||||
>
|
||||
<PackageCheck className="w-4 h-4 mr-2" />
|
||||
確認收貨
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>確定要確認收貨嗎?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
庫存將從在途倉「{order.transit_warehouse_name}」移至目的倉庫「{order.to_warehouse_name}」,完成此次調撥流程。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleReceive} className="bg-green-600 hover:bg-green-700 text-white">確認收貨</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Can>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 在途倉資訊卡片 */}
|
||||
{(hasTransit || transitWarehouses.length > 0) && (
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6 space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Truck className="h-5 w-5 text-orange-500" />
|
||||
<Label className="text-gray-700 font-semibold text-base">在途倉庫(配送車輛)</Label>
|
||||
</div>
|
||||
|
||||
{order.status === 'draft' && canEdit ? (
|
||||
/* 草稿狀態:可選擇在途倉 */
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<Label className="text-xs text-gray-500 mb-1 block">選擇在途倉庫</Label>
|
||||
<Select
|
||||
value={transitWarehouseId || ''}
|
||||
onValueChange={(v) => setTransitWarehouseId(v === 'none' ? null : v)}
|
||||
>
|
||||
<SelectTrigger className="h-9">
|
||||
<SelectValue placeholder="不使用在途倉(直接過帳)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">不使用在途倉(直接過帳)</SelectItem>
|
||||
{transitWarehouses.map((w) => (
|
||||
<SelectItem key={w.id} value={w.id}>
|
||||
{w.name} {w.license_plate ? `(${w.license_plate})` : ''}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{selectedTransitWarehouse && (
|
||||
<>
|
||||
<div>
|
||||
<Label className="text-xs text-gray-500 mb-1 block">車牌</Label>
|
||||
<div className="text-sm font-medium text-gray-700 p-2 bg-gray-50 rounded border">
|
||||
{selectedTransitWarehouse.license_plate || '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-gray-500 mb-1 block">司機</Label>
|
||||
<div className="text-sm font-medium text-gray-700 p-2 bg-gray-50 rounded border">
|
||||
{selectedTransitWarehouse.driver_name || '-'}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : hasTransit ? (
|
||||
/* 非草稿狀態:唯讀顯示在途倉資訊 */
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<Label className="text-xs text-gray-500 mb-1 block">在途倉庫</Label>
|
||||
<div className="text-sm font-semibold text-gray-700">{order.transit_warehouse_name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-gray-500 mb-1 block">車牌</Label>
|
||||
<div className="text-sm font-medium text-gray-700">{order.transit_warehouse_plate || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-gray-500 mb-1 block">司機</Label>
|
||||
<div className="text-sm font-medium text-gray-700">{order.transit_warehouse_driver || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-gray-500 mb-1 block">狀態</Label>
|
||||
<div className="text-sm font-medium">
|
||||
{order.status === 'dispatched' && (
|
||||
<span className="text-orange-600">配送中({order.dispatched_at})</span>
|
||||
)}
|
||||
{order.status === 'completed' && (
|
||||
<span className="text-green-600">已收貨({order.received_at})</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* 顯示時間軸(已出貨或已完成時) */}
|
||||
{(order.dispatched_at || order.received_at) && (
|
||||
<div className="border-t pt-3 mt-3 flex flex-wrap gap-6 text-sm text-gray-500">
|
||||
{order.dispatched_at && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Truck className="h-3.5 w-3.5 text-orange-400" />
|
||||
<span>出貨:{order.dispatched_at}</span>
|
||||
<span className="text-gray-400">({order.dispatched_by})</span>
|
||||
</div>
|
||||
)}
|
||||
{order.received_at && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<PackageCheck className="h-3.5 w-3.5 text-green-500" />
|
||||
<span>收貨:{order.received_at}</span>
|
||||
<span className="text-gray-400">({order.received_by})</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-gray-500 font-semibold">備註資訊</Label>
|
||||
@@ -497,7 +730,7 @@ export default function Show({ order }: any) {
|
||||
<TableHead className="font-medium text-grey-600">商品名稱 / 代號</TableHead>
|
||||
<TableHead className="font-medium text-grey-600">批號</TableHead>
|
||||
<TableHead className="text-right w-32 font-medium text-grey-600">
|
||||
{order.status === 'completed' ? '過帳時庫存' : '可用庫存'}
|
||||
{stockColumnTitle()}
|
||||
</TableHead>
|
||||
<TableHead className="text-right w-40 font-medium text-grey-600">調撥數量</TableHead>
|
||||
<TableHead className="font-medium text-grey-600">單位</TableHead>
|
||||
|
||||
Reference in New Issue
Block a user