Files
star-erp/resources/js/Pages/Integration/SalesOrders/Show.tsx
sky121113 2f30a78118
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 56s
feat(integration): 實作並測試 POS 與販賣機訂單同步 API
主要變更:
- 實作 POS 與販賣機訂單同步邏輯,支援多租戶與 Sanctum 驗證。
- 修正多租戶識別中間件與 Sanctum 驗證順序問題。
- 切換快取驅動至 Redis 以支援 Tenancy 標籤功能。
- 新增商品同步 API (Upsert) 及相關單元測試。
- 新增手動測試腳本 tests/manual/test_integration_api.sh。
- 前端新增銷售訂單來源篩選與欄位顯示。
2026-02-23 13:27:12 +08:00

216 lines
11 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 { ArrowLeft, TrendingUp, Package, CreditCard, Calendar, FileJson } from "lucide-react";
import { Button } from "@/Components/ui/button";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
import { Head, Link } from "@inertiajs/react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/Components/ui/table";
import { StatusBadge, StatusVariant } from "@/Components/shared/StatusBadge";
import { formatDate } from "@/lib/date";
import { formatNumber } from "@/utils/format";
import CopyButton from "@/Components/shared/CopyButton";
interface SalesOrderItem {
id: number;
product_name: string;
quantity: string;
price: string;
total: string;
}
interface SalesOrder {
id: number;
external_order_id: string;
status: string;
payment_method: string;
total_amount: string;
sold_at: string;
created_at: string;
raw_payload: any;
items: SalesOrderItem[];
source: string;
source_label: string | null;
}
const getSourceDisplay = (source: string, sourceLabel: string | null): string => {
const base = source === 'pos' ? 'POS 收銀機'
: source === 'vending' ? '販賣機'
: source === 'manual_import' ? '手動匯入'
: source;
return sourceLabel ? `${base} (${sourceLabel})` : base;
};
interface Props {
order: SalesOrder;
}
const getStatusVariant = (status: string): StatusVariant => {
switch (status) {
case 'completed': return 'success';
case 'pending': return 'warning';
case 'cancelled': return 'destructive';
default: return 'neutral';
}
};
const getStatusLabel = (status: string): string => {
switch (status) {
case 'completed': return "已完成";
case 'pending': return "待處理";
case 'cancelled': return "已取消";
default: return status;
}
};
export default function SalesOrderShow({ order }: Props) {
return (
<AuthenticatedLayout
breadcrumbs={[
{ label: "銷售管理", href: "#" },
{ label: "銷售訂單管理", href: route("integration.sales-orders.index") },
{ label: `訂單詳情 (#${order.external_order_id})`, href: "#", isPage: true },
]}
>
<Head title={`銷售訂單詳情 - ${order.external_order_id}`} />
<div className="container mx-auto p-6 max-w-7xl">
{/* Header */}
<div className="mb-6">
<Link href={route("integration.sales-orders.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">
<TrendingUp className="h-6 w-6 text-primary-main" />
</h1>
<p className="text-gray-500 mt-1">{order.external_order_id}</p>
</div>
<StatusBadge variant={getStatusVariant(order.status)}>
{getStatusLabel(order.status)}
</StatusBadge>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 左側:基本資訊與明細 */}
<div className="lg:col-span-2 space-y-6">
{/* 基本資訊卡片 */}
<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 flex items-center gap-2">
<Package className="h-5 w-5 text-primary-main" />
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<span className="text-sm text-gray-500 block mb-1"></span>
<div className="flex items-center gap-1.5">
<span className="font-mono font-medium text-gray-900">{order.external_order_id}</span>
<CopyButton text={order.external_order_id} label="複製單號" />
</div>
</div>
<div>
<span className="text-sm text-gray-500 block mb-1"></span>
<div className="flex items-center gap-1.5 font-medium text-gray-900">
<Calendar className="h-4 w-4 text-gray-400" />
{formatDate(order.sold_at)}
</div>
</div>
<div>
<span className="text-sm text-gray-500 block mb-1"></span>
<div className="flex items-center gap-1.5 font-medium text-gray-900">
<CreditCard className="h-4 w-4 text-gray-400" />
{order.payment_method || "—"}
</div>
</div>
<div>
<span className="text-sm text-gray-500 block mb-1"></span>
<span className="font-medium text-gray-900">{formatDate(order.created_at as any)}</span>
</div>
<div>
<span className="text-sm text-gray-500 block mb-1"></span>
<span className="font-medium text-gray-900">{getSourceDisplay(order.source, order.source_label)}</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">
<h2 className="text-lg font-bold text-gray-900 mb-0"></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-[50px] text-center">#</TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{order.items.map((item, index) => (
<TableRow key={item.id}>
<TableCell className="text-gray-500 text-center">{index + 1}</TableCell>
<TableCell className="font-medium">{item.product_name}</TableCell>
<TableCell className="text-right font-medium">{formatNumber(parseFloat(item.quantity))}</TableCell>
<TableCell className="text-right text-gray-600">${formatNumber(parseFloat(item.price))}</TableCell>
<TableCell className="text-right font-bold text-primary-main">${formatNumber(parseFloat(item.total))}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<div className="mt-6 flex justify-end">
<div className="w-full max-w-sm bg-primary-lightest/30 px-6 py-4 rounded-xl border border-primary-light/20 flex flex-col gap-3">
<div className="flex justify-between items-end w-full">
<span className="text-sm text-gray-500 font-medium mb-1"></span>
<span className="text-2xl font-black text-primary-main">
${formatNumber(parseFloat(order.total_amount))}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
{/* 右側:原始資料 (Raw Payload) */}
<div className="space-y-6">
<div className="bg-white rounded-xl border border-gray-200 shadow-sm p-6">
<h2 className="text-lg font-bold text-gray-900 mb-4 flex items-center gap-2">
<FileJson className="h-5 w-5 text-primary-main" />
API
</h2>
<p className="text-sm text-gray-500 mb-4">
JSON
</p>
<div className="bg-slate-900 rounded-lg p-4 overflow-auto max-h-[600px]">
<pre className="text-xs text-slate-300 font-mono">
{JSON.stringify(order.raw_payload, null, 2)}
</pre>
</div>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
);
}