Files
2025-12-30 15:03:19 +08:00

609 lines
16 KiB
TypeScript

import { useState } from "react";
import { Toaster, toast } from "sonner@2.0.3";
import PurchaseOrderManagement from "./components/PurchaseOrderManagement";
import CreatePurchaseOrderPage from "./components/CreatePurchaseOrderPage";
import InspectionPage from "./components/InspectionPage";
import ViewPurchaseOrderPage from "./components/ViewPurchaseOrderPage";
import NavigationSidebar from "./components/NavigationSidebar";
import type { PurchaseOrder, PurchaseOrderStatus, Supplier, PurchaseOrderItem, PaymentInfo } from "./types/purchase-order";
import type { Store, Warehouse } from "./types/requester";
import { STATUS_CONFIG } from "./constants/purchase-order";
// Mock suppliers data
const mockSuppliers: Supplier[] = [
{
id: "sup-1",
name: "美食材料供應商",
contact: "王小明",
phone: "02-2345-6789",
email: "supplier1@example.com",
commonProducts: [
{ productId: "prod-1", productName: "高筋麵粉", unit: "公斤", lastPrice: 45 },
{ productId: "prod-2", productName: "無鹽奶油", unit: "公斤", lastPrice: 280 },
{ productId: "prod-3", productName: "細砂糖", unit: "公斤", lastPrice: 35 },
{ productId: "prod-4", productName: "雞蛋", unit: "打", lastPrice: 65 },
],
},
{
id: "sup-2",
name: "優質乳製品公司",
contact: "李美玲",
phone: "02-3456-7890",
email: "dairy@example.com",
commonProducts: [
{ productId: "prod-5", productName: "鮮奶油", unit: "公升", lastPrice: 180 },
{ productId: "prod-6", productName: "馬斯卡彭起司", unit: "公斤", lastPrice: 450 },
{ productId: "prod-7", productName: "鮮奶", unit: "公升", lastPrice: 55 },
],
},
{
id: "sup-3",
name: "進口巧克力專賣店",
contact: "張大華",
phone: "02-4567-8901",
email: "chocolate@example.com",
commonProducts: [
{ productId: "prod-8", productName: "70%黑巧克力", unit: "公斤", lastPrice: 520 },
{ productId: "prod-9", productName: "白巧克力", unit: "公斤", lastPrice: 480 },
{ productId: "prod-10", productName: "可可粉", unit: "公斤", lastPrice: 380 },
],
},
];
// Mock stores data
const mockStores: Store[] = [
{
id: "store-1",
name: "總店",
address: "台北市大安區信義路四段100號",
manager: "王小華",
phone: "02-2700-1000",
},
{
id: "store-2",
name: "A 店(東區店)",
address: "台北市大安區忠孝東路四段200號",
manager: "李美玲",
phone: "02-2771-2000",
},
{
id: "store-3",
name: "B 店(西門店)",
address: "台北市萬華區成都路50號",
manager: "張大明",
phone: "02-2388-3000",
},
{
id: "store-4",
name: "C 店(天母店)",
address: "台北市士林區中山北路七段150號",
manager: "周小姐",
phone: "02-2876-4000",
},
];
// Mock warehouses data
const mockWarehouses: Warehouse[] = [
{
id: "warehouse-1",
name: "中央倉庫",
address: "新北市新莊區中正路500號",
manager: "劉主管",
phone: "02-2990-5000",
},
{
id: "warehouse-2",
name: "南區倉庫",
address: "台中市西屯區工業區一路200號",
manager: "黃主管",
phone: "04-2350-6000",
},
];
const initialPurchaseOrders: PurchaseOrder[] = [
{
id: "po-1",
poNumber: "PO202411001",
supplierId: "sup-1",
supplierName: "美食材料供應商",
expectedDate: "2024-11-25",
status: "pending_confirm",
createdBy: "王小華",
requesterType: "store",
requesterId: "store-1",
requesterName: "總店",
department: "總店",
items: [
{
productId: "prod-1",
productName: "高筋麵粉",
quantity: 50,
unit: "公斤",
unitPrice: 45,
previousPrice: 45,
subtotal: 2250,
},
{
productId: "prod-3",
productName: "細砂糖",
quantity: 30,
unit: "公斤",
unitPrice: 35,
previousPrice: 35,
subtotal: 1050,
},
],
totalAmount: 3300,
createdAt: "2024-11-18",
reviewInfo: {
reviewedBy: "張主管",
reviewedAt: "2024-11-19 10:30",
},
},
{
id: "po-2",
poNumber: "PO202411002",
supplierId: "sup-2",
supplierName: "優質乳製品公司",
expectedDate: "2024-11-26",
status: "shipping",
createdBy: "李美玲",
requesterType: "store",
requesterId: "store-2",
requesterName: "A 店(東區店)",
department: "A 店",
items: [
{
productId: "prod-5",
productName: "鮮奶油",
quantity: 20,
unit: "公升",
unitPrice: 180,
previousPrice: 180,
subtotal: 3600,
},
],
totalAmount: 3600,
createdAt: "2024-11-19",
reviewInfo: {
reviewedBy: "陳主管",
reviewedAt: "2024-11-20 14:15",
},
},
{
id: "po-3",
poNumber: "PO202411003",
supplierId: "sup-3",
supplierName: "進口巧克力專賣店",
expectedDate: "2024-11-28",
status: "draft",
createdBy: "張大明",
requesterType: "store",
requesterId: "store-3",
requesterName: "B 店(西門店)",
department: "B 店",
items: [
{
productId: "prod-8",
productName: "70%黑巧克力",
quantity: 10,
unit: "公斤",
unitPrice: 520,
previousPrice: 500,
subtotal: 5200,
},
],
totalAmount: 5200,
createdAt: "2024-11-20",
},
{
id: "po-4",
poNumber: "PO202411004",
supplierId: "sup-1",
supplierName: "美食材料供應商",
expectedDate: "2024-11-30",
status: "review_pending",
createdBy: "陳雅婷",
requesterType: "store",
requesterId: "store-1",
requesterName: "總店",
department: "總店",
items: [
{
productId: "prod-2",
productName: "無鹽奶油",
quantity: 15,
unit: "公斤",
unitPrice: 280,
previousPrice: 280,
subtotal: 4200,
},
{
productId: "prod-4",
productName: "雞蛋",
quantity: 10,
unit: "打",
unitPrice: 65,
previousPrice: 65,
subtotal: 650,
},
],
totalAmount: 4850,
createdAt: "2024-11-21",
},
{
id: "po-5",
poNumber: "PO202411005",
supplierId: "sup-2",
supplierName: "優質乳製品公司",
expectedDate: "2024-12-02",
status: "completed",
createdBy: "林志豪",
requesterType: "store",
requesterId: "store-2",
requesterName: "A 店(東區店)",
department: "A 店",
items: [
{
productId: "prod-6",
productName: "馬斯卡彭起司",
quantity: 5,
unit: "公斤",
unitPrice: 450,
previousPrice: 450,
subtotal: 2250,
},
],
totalAmount: 2250,
createdAt: "2024-11-15",
reviewInfo: {
reviewedBy: "王主管",
reviewedAt: "2024-11-16 09:00",
},
},
{
id: "po-6",
poNumber: "PO202411006",
supplierId: "sup-1",
supplierName: "美食材料供應商",
expectedDate: "2024-11-27",
status: "processing",
createdBy: "劉主管",
requesterType: "warehouse",
requesterId: "warehouse-1",
requesterName: "中央倉庫",
department: "中央倉庫",
items: [
{
productId: "prod-1",
productName: "高筋麵粉",
quantity: 200,
unit: "公斤",
unitPrice: 45,
previousPrice: 45,
subtotal: 9000,
},
{
productId: "prod-3",
productName: "細砂糖",
quantity: 100,
unit: "公斤",
unitPrice: 35,
previousPrice: 35,
subtotal: 3500,
},
],
totalAmount: 12500,
createdAt: "2024-11-19",
reviewInfo: {
reviewedBy: "李主管",
reviewedAt: "2024-11-20 11:30",
},
},
{
id: "po-7",
poNumber: "PO202411007",
supplierId: "sup-3",
supplierName: "進口巧克力專賣店",
expectedDate: "2024-11-29",
status: "rejected",
createdBy: "黃主管",
requesterType: "warehouse",
requesterId: "warehouse-2",
requesterName: "南區倉庫",
department: "南區倉庫",
items: [
{
productId: "prod-8",
productName: "70%黑巧克力",
quantity: 30,
unit: "公斤",
unitPrice: 520,
previousPrice: 500,
subtotal: 15600,
},
{
productId: "prod-10",
productName: "可可粉",
quantity: 20,
unit: "公斤",
unitPrice: 380,
previousPrice: 380,
subtotal: 7600,
},
],
totalAmount: 23200,
createdAt: "2024-11-20",
reviewInfo: {
reviewedBy: "陳主管",
reviewedAt: "2024-11-21 16:45",
rejectionReason: "數量過多,超出本月預算",
},
},
{
id: "po-8",
poNumber: "PO202411008",
supplierId: "sup-2",
supplierName: "優質乳製品公司",
expectedDate: "2024-12-01",
status: "pending_confirm",
createdBy: "周小姐",
requesterType: "store",
requesterId: "store-4",
requesterName: "C 店(天母店)",
department: "C 店",
items: [
{
productId: "prod-5",
productName: "鮮奶油",
quantity: 10,
unit: "公升",
unitPrice: 180,
previousPrice: 180,
subtotal: 1800,
},
],
totalAmount: 1800,
createdAt: "2024-11-22",
reviewInfo: {
reviewedBy: "張主管",
reviewedAt: "2024-11-23 10:00",
},
},
{
id: "po-9",
poNumber: "PO202411009",
supplierId: "sup-1",
supplierName: "美食材料供應商",
expectedDate: "2024-12-03",
status: "completed",
createdBy: "吳經理",
requesterType: "warehouse",
requesterId: "warehouse-1",
requesterName: "中央倉庫",
department: "中央倉庫",
items: [
{
productId: "prod-2",
productName: "無鹽奶油",
quantity: 25,
unit: "公斤",
unitPrice: 280,
previousPrice: 280,
subtotal: 7000,
},
],
totalAmount: 7000,
createdAt: "2024-11-16",
reviewInfo: {
reviewedBy: "林主管",
reviewedAt: "2024-11-17 09:30",
},
paymentInfo: {
paymentMethod: "bank_transfer",
paymentDate: "2024-11-28",
actualAmount: 7000,
paidBy: "財務部 陳小姐",
paidAt: "2024-11-28 14:30",
hasInvoice: true,
invoice: {
invoiceNumber: "AB-12345678",
invoiceAmount: 7000,
invoiceDate: "2024-11-28",
invoiceType: "triplicate",
companyName: "甜點連鎖股份有限公司",
taxId: "12345678",
},
},
},
];
export default function App() {
const [currentPage, setCurrentPage] = useState("purchase-order-management");
const [purchaseOrders, setPurchaseOrders] = useState<PurchaseOrder[]>(initialPurchaseOrders);
const [inspectingOrder, setInspectingOrder] = useState<PurchaseOrder | undefined>();
const [editingOrder, setEditingOrder] = useState<PurchaseOrder | undefined>();
const [viewingOrder, setViewingOrder] = useState<PurchaseOrder | undefined>();
const handleNavigate = (path: string) => {
setCurrentPage(path);
};
const handleNavigateToInspection = (order: PurchaseOrder) => {
setInspectingOrder(order);
setCurrentPage("inspection");
};
const handleNavigateToCreateOrder = () => {
setEditingOrder(undefined);
setCurrentPage("create-purchase-order");
};
const handleNavigateToEditOrder = (order: PurchaseOrder) => {
setEditingOrder(order);
setCurrentPage("create-purchase-order");
};
const handleNavigateToViewOrder = (order: PurchaseOrder) => {
setViewingOrder(order);
setCurrentPage("view-purchase-order");
};
const handleSaveOrder = (order: PurchaseOrder) => {
if (editingOrder) {
setPurchaseOrders((prev) =>
prev.map((o) => (o.id === order.id ? order : o))
);
toast.success("採購單已更新");
} else {
setPurchaseOrders((prev) => [...prev, order]);
toast.success("採購單已建立");
}
setCurrentPage("purchase-order-management");
setEditingOrder(undefined);
};
const handleDeleteOrder = (id: string) => {
setPurchaseOrders((prev) => prev.filter((o) => o.id !== id));
toast.success("採購單已刪除");
};
const handleStatusChange = (id: string, newStatus: PurchaseOrderStatus, reviewInfo?: any) => {
setPurchaseOrders((prev) =>
prev.map((o) => {
if (o.id === id) {
const updatedOrder = { ...o, status: newStatus };
if (reviewInfo) {
updatedOrder.reviewInfo = reviewInfo;
}
return updatedOrder;
}
return o;
})
);
toast.success(`採購單狀態已更新為:${STATUS_CONFIG[newStatus].label}`);
};
const handleCancelCreateOrder = () => {
setCurrentPage("purchase-order-management");
setEditingOrder(undefined);
};
const handleCompleteInspection = (orderId: string, items: PurchaseOrderItem[]) => {
// 驗收完成後自動轉為「待確認」狀態
setPurchaseOrders((prev) =>
prev.map((o) =>
o.id === orderId ? { ...o, status: "pending_confirm" as const, items } : o
)
);
toast.success("驗收完成!已自動產生應付帳款記錄,請進行付款作業");
setCurrentPage("purchase-order-management");
setInspectingOrder(undefined);
};
const handlePayment = (orderId: string, paymentInfo: PaymentInfo) => {
setPurchaseOrders((prev) =>
prev.map((o) => {
if (o.id === orderId) {
// 付款完成後自動轉為「已完成」狀態
return {
...o,
status: "completed" as const,
paymentInfo,
};
}
return o;
})
);
toast.success("付款完成!採購單已完成所有作業");
};
const handleCancelInspection = () => {
setCurrentPage("purchase-order-management");
setInspectingOrder(undefined);
};
const renderPage = () => {
switch (currentPage) {
case "purchase-order-management":
return (
<PurchaseOrderManagement
orders={purchaseOrders}
onNavigateToInspection={handleNavigateToInspection}
onNavigateToCreateOrder={handleNavigateToCreateOrder}
onNavigateToEditOrder={handleNavigateToEditOrder}
onNavigateToViewOrder={handleNavigateToViewOrder}
onDeleteOrder={handleDeleteOrder}
onStatusChange={handleStatusChange}
onPayment={handlePayment}
/>
);
case "create-purchase-order":
return (
<CreatePurchaseOrderPage
order={editingOrder}
onSave={handleSaveOrder}
onCancel={handleCancelCreateOrder}
onDelete={handleDeleteOrder}
suppliers={mockSuppliers}
stores={mockStores}
warehouses={mockWarehouses}
/>
);
case "inspection":
return (
<InspectionPage
order={inspectingOrder}
onComplete={handleCompleteInspection}
onCancel={handleCancelInspection}
/>
);
case "view-purchase-order":
return (
<ViewPurchaseOrderPage
order={viewingOrder}
onBack={() => setCurrentPage("purchase-order-management")}
/>
);
default:
return (
<PurchaseOrderManagement
orders={purchaseOrders}
onNavigateToInspection={handleNavigateToInspection}
onNavigateToCreateOrder={handleNavigateToCreateOrder}
onNavigateToEditOrder={handleNavigateToEditOrder}
onNavigateToViewOrder={handleNavigateToViewOrder}
onDeleteOrder={handleDeleteOrder}
onStatusChange={handleStatusChange}
onPayment={handlePayment}
/>
);
}
};
const hidesSidebarOnMobile =
currentPage === "inspection" ||
currentPage === "create-purchase-order";
return (
<div className="flex min-h-screen" style={{ backgroundColor: 'var(--bg-page)' }}>
{/* Sidebar Navigation - 某些頁面在手機版隱藏 */}
<div className={hidesSidebarOnMobile ? "hidden md:block" : ""}>
<NavigationSidebar
currentPath={currentPage}
onNavigate={handleNavigate}
/>
</div>
{/* Main Content */}
<main className="flex-1 overflow-auto" style={{ backgroundColor: 'var(--bg-page)' }}>
{renderPage()}
</main>
<Toaster />
</div>
);
}