609 lines
16 KiB
TypeScript
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>
|
|
);
|
|
} |