first commit
This commit is contained in:
91
resources/js/utils/format.ts
Normal file
91
resources/js/utils/format.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 格式化相關工具函式
|
||||
*/
|
||||
|
||||
/**
|
||||
* 格式化數字為千分位格式
|
||||
*/
|
||||
export const formatNumber = (num: number): string => {
|
||||
return num.toLocaleString();
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化貨幣(NT$)
|
||||
*/
|
||||
export const formatCurrency = (num: number): string => {
|
||||
return `NT$ ${num.toLocaleString()}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
export const formatDate = (date: string): string => {
|
||||
if (!date) return "-";
|
||||
return new Date(date).toLocaleDateString("zh-TW");
|
||||
};
|
||||
|
||||
/**
|
||||
* 獲取當前日期(YYYY-MM-DD 格式)
|
||||
*/
|
||||
export const getCurrentDate = (): string => {
|
||||
return new Date().toISOString().split("T")[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成唯一 ID
|
||||
*/
|
||||
export const generateId = (): string => {
|
||||
return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成撥補單號
|
||||
*/
|
||||
export const generateOrderNumber = (): string => {
|
||||
return `TO${Date.now().toString().slice(-8)}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成批號
|
||||
* 格式:{倉庫代碼}-{日期YYYYMMDD}-{流水號}
|
||||
* 例如:WH1-20251128-001
|
||||
*/
|
||||
export const generateBatchNumber = (
|
||||
warehouseId: string,
|
||||
date?: string,
|
||||
sequence?: number
|
||||
): string => {
|
||||
const targetDate = date || getCurrentDate();
|
||||
const dateStr = targetDate.replace(/-/g, "");
|
||||
const seq = sequence || Math.floor(Math.random() * 1000);
|
||||
const seqStr = seq.toString().padStart(3, "0");
|
||||
return `WH${warehouseId}-${dateStr}-${seqStr}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 獲取當前日期時間(YYYY-MM-DDTHH:mm 格式,用於 datetime-local input)
|
||||
*/
|
||||
export const getCurrentDateTime = (): string => {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(now.getDate()).padStart(2, "0");
|
||||
const hours = String(now.getHours()).padStart(2, "0");
|
||||
const minutes = String(now.getMinutes()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化日期時間顯示
|
||||
*/
|
||||
export const formatDateTime = (datetime: string): string => {
|
||||
if (!datetime) return "-";
|
||||
return new Date(datetime).toLocaleString("zh-TW", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
});
|
||||
};
|
||||
165
resources/js/utils/inventory.ts
Normal file
165
resources/js/utils/inventory.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* 庫存計算相關工具函式
|
||||
*/
|
||||
|
||||
import { WarehouseInventory, WarehouseStats, SafetyStockSetting, SafetyStockStatus } from "../types/warehouse";
|
||||
|
||||
/**
|
||||
* 計算倉庫的總庫存數量
|
||||
*/
|
||||
export const calculateTotalQuantity = (
|
||||
inventories: WarehouseInventory[],
|
||||
warehouseId: string
|
||||
): number => {
|
||||
return inventories
|
||||
.filter((inv) => inv.warehouseId === warehouseId)
|
||||
.reduce((sum, inv) => sum + inv.quantity, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* 按商品分組計算總庫存
|
||||
*/
|
||||
export const calculateProductTotalStock = (
|
||||
inventories: WarehouseInventory[],
|
||||
productId: string
|
||||
): number => {
|
||||
return inventories
|
||||
.filter((inv) => inv.productId === productId)
|
||||
.reduce((sum, inv) => sum + inv.quantity, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* 計算安全庫存狀態
|
||||
*/
|
||||
export const getSafetyStockStatus = (
|
||||
currentStock: number,
|
||||
safetyStock: number | null | undefined
|
||||
): SafetyStockStatus => {
|
||||
if (!safetyStock || safetyStock === 0) return "正常";
|
||||
const ratio = currentStock / safetyStock;
|
||||
if (ratio >= 1.2) return "正常";
|
||||
if (ratio >= 1.0) return "接近";
|
||||
return "低於";
|
||||
};
|
||||
|
||||
/**
|
||||
* 檢查商品是否低於安全庫存
|
||||
*/
|
||||
export const isProductLowStock = (
|
||||
inventories: WarehouseInventory[],
|
||||
productId: string,
|
||||
safetyStockSettings: SafetyStockSetting[]
|
||||
): boolean => {
|
||||
const setting = safetyStockSettings.find((s) => s.productId === productId);
|
||||
if (!setting) return false;
|
||||
|
||||
const totalStock = calculateProductTotalStock(inventories, productId);
|
||||
return totalStock < setting.safetyStock;
|
||||
};
|
||||
|
||||
/**
|
||||
* 計算低庫存警告數量(按商品計算)
|
||||
*/
|
||||
export const calculateLowStockCount = (
|
||||
inventories: WarehouseInventory[],
|
||||
warehouseId: string,
|
||||
safetyStockSettings: SafetyStockSetting[]
|
||||
): number => {
|
||||
// 取得該倉庫的所有庫存
|
||||
const warehouseInventories = inventories.filter(
|
||||
(inv) => String(inv.warehouseId) === String(warehouseId)
|
||||
);
|
||||
|
||||
// 取得該倉庫的安全庫存設定
|
||||
const warehouseSettings = safetyStockSettings.filter(
|
||||
(s) => String(s.warehouseId) === String(warehouseId)
|
||||
);
|
||||
|
||||
// 計算有多少商品低於安全庫存
|
||||
let lowStockCount = 0;
|
||||
warehouseSettings.forEach((setting) => {
|
||||
// 找出該設定對應的商品庫存
|
||||
// 注意:這裡假設一個商品在同一個倉庫只有一種庫存紀錄 (warehouse_inventory table)
|
||||
// 如果有批號區分,則需要加總所有該商品的數量
|
||||
const productTotalStock = warehouseInventories
|
||||
.filter(inv => String(inv.productId) === String(setting.productId))
|
||||
.reduce((sum, inv) => sum + inv.quantity, 0);
|
||||
|
||||
// 只有當安全庫存設定存在時才進行判斷 (後端已過濾掉 null)
|
||||
// 若設定為 0,則表示允許庫存為 0,不會觸發警告 (除非庫存為負)
|
||||
if (productTotalStock < setting.safetyStock) {
|
||||
lowStockCount++;
|
||||
}
|
||||
});
|
||||
|
||||
return lowStockCount;
|
||||
};
|
||||
|
||||
/**
|
||||
* 計算待撥補需求數量(按商品計算)
|
||||
*/
|
||||
export const calculateReplenishmentNeeded = (
|
||||
inventories: WarehouseInventory[],
|
||||
warehouseId: string,
|
||||
safetyStockSettings: SafetyStockSetting[]
|
||||
): number => {
|
||||
// 取得該倉庫的所有庫存
|
||||
const warehouseInventories = inventories.filter(
|
||||
(inv) => inv.warehouseId === warehouseId
|
||||
);
|
||||
|
||||
// 取得該倉庫的安全庫存設定
|
||||
const warehouseSettings = safetyStockSettings.filter(
|
||||
(s) => s.warehouseId === warehouseId
|
||||
);
|
||||
|
||||
// 計算需要撥補的總量
|
||||
let replenishmentNeeded = 0;
|
||||
warehouseSettings.forEach((setting) => {
|
||||
const productTotalStock = calculateProductTotalStock(
|
||||
warehouseInventories,
|
||||
setting.productId
|
||||
);
|
||||
if (productTotalStock < setting.safetyStock) {
|
||||
replenishmentNeeded += setting.safetyStock - productTotalStock;
|
||||
}
|
||||
});
|
||||
|
||||
return replenishmentNeeded;
|
||||
};
|
||||
|
||||
/**
|
||||
* 計算倉庫統計資訊
|
||||
*/
|
||||
export const calculateWarehouseStats = (
|
||||
inventories: WarehouseInventory[],
|
||||
warehouseId: string,
|
||||
safetyStockSettings: SafetyStockSetting[]
|
||||
): WarehouseStats => {
|
||||
return {
|
||||
totalQuantity: calculateTotalQuantity(inventories, warehouseId),
|
||||
lowStockCount: calculateLowStockCount(inventories, warehouseId, safetyStockSettings),
|
||||
replenishmentNeeded: calculateReplenishmentNeeded(inventories, warehouseId, safetyStockSettings),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 檢查倉庫是否有庫存警告
|
||||
*/
|
||||
export const hasWarehouseWarning = (
|
||||
inventories: WarehouseInventory[],
|
||||
warehouseId: string,
|
||||
safetyStockSettings: SafetyStockSetting[]
|
||||
): boolean => {
|
||||
return calculateLowStockCount(inventories, warehouseId, safetyStockSettings) > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* 過濾倉庫的庫存
|
||||
*/
|
||||
export const filterWarehouseInventories = (
|
||||
inventories: WarehouseInventory[],
|
||||
warehouseId: string
|
||||
): WarehouseInventory[] => {
|
||||
return inventories.filter((inv) => inv.warehouseId === warehouseId);
|
||||
};
|
||||
83
resources/js/utils/purchase-order.ts
Normal file
83
resources/js/utils/purchase-order.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 採購單相關工具函式
|
||||
*/
|
||||
|
||||
import type { PurchaseOrderItem } from "@/types/purchase-order";
|
||||
import { PRICE_ALERT_THRESHOLD } from "@/constants/purchase-order";
|
||||
|
||||
/**
|
||||
* 格式化金額
|
||||
*/
|
||||
export function formatCurrency(amount: number): string {
|
||||
return `$${amount.toLocaleString()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 計算項目小計
|
||||
*/
|
||||
export function calculateSubtotal(quantity: number, unitPrice: number): number {
|
||||
return quantity * unitPrice;
|
||||
}
|
||||
|
||||
/**
|
||||
* 計算總金額
|
||||
*/
|
||||
export function calculateTotalAmount(items: PurchaseOrderItem[]): number {
|
||||
return items.reduce((sum, item) => sum + Number(item.subtotal || 0), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 檢查價格是否警示(超過閾值)
|
||||
*/
|
||||
export function isPriceAlert(currentPrice: number, previousPrice?: number): boolean {
|
||||
if (!previousPrice || previousPrice === 0) return false;
|
||||
const increase = ((currentPrice - previousPrice) / previousPrice) * 100;
|
||||
return increase > PRICE_ALERT_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* 計算價格漲幅百分比
|
||||
*/
|
||||
export function calculatePriceIncrease(currentPrice: number, previousPrice?: number): number {
|
||||
if (!previousPrice || previousPrice === 0) return 0;
|
||||
return ((currentPrice - previousPrice) / previousPrice) * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成採購單編號
|
||||
*/
|
||||
export function generatePONumber(): string {
|
||||
const year = new Date().getFullYear();
|
||||
const sequence = (Date.now() % 100000).toString().padStart(5, "0");
|
||||
return `PO${year}${sequence}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得今天日期(YYYY-MM-DD 格式)
|
||||
*/
|
||||
export function getTodayDate(): string {
|
||||
return new Date().toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 驗證採購單表單
|
||||
*/
|
||||
export function validatePurchaseOrder(
|
||||
supplierId: string,
|
||||
expectedDate: string,
|
||||
items: PurchaseOrderItem[]
|
||||
): boolean {
|
||||
return !!(
|
||||
supplierId &&
|
||||
expectedDate &&
|
||||
items.length > 0 &&
|
||||
items.some((item) => item.quantity > 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 過濾有效項目(數量大於 0)
|
||||
*/
|
||||
export function filterValidItems(items: PurchaseOrderItem[]): PurchaseOrderItem[] {
|
||||
return items.filter((item) => item.quantity > 0);
|
||||
}
|
||||
72
resources/js/utils/validation.ts
Normal file
72
resources/js/utils/validation.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 驗證相關工具函式
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 驗證撥補單表單資料
|
||||
*/
|
||||
export const validateTransferOrder = (formData: {
|
||||
sourceWarehouseId: string;
|
||||
targetWarehouseId: string;
|
||||
productId: string;
|
||||
quantity: number;
|
||||
}): { isValid: boolean; error?: string } => {
|
||||
if (!formData.sourceWarehouseId) {
|
||||
return { isValid: false, error: "請選擇來源倉庫" };
|
||||
}
|
||||
|
||||
if (!formData.targetWarehouseId) {
|
||||
return { isValid: false, error: "請選擇目標倉庫" };
|
||||
}
|
||||
|
||||
if (formData.sourceWarehouseId === formData.targetWarehouseId) {
|
||||
return { isValid: false, error: "來源倉庫與目標倉庫不能相同" };
|
||||
}
|
||||
|
||||
if (!formData.productId) {
|
||||
return { isValid: false, error: "請選擇撥補商品" };
|
||||
}
|
||||
|
||||
if (formData.quantity <= 0) {
|
||||
return { isValid: false, error: "撥補數量必須大於0" };
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
};
|
||||
|
||||
/**
|
||||
* 驗證撥補數量是否超過可用庫存
|
||||
*/
|
||||
export const validateTransferQuantity = (
|
||||
quantity: number,
|
||||
availableQuantity: number
|
||||
): { isValid: boolean; error?: string } => {
|
||||
if (quantity > availableQuantity) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `撥補數量不能超過可用庫存 (${availableQuantity})`,
|
||||
};
|
||||
}
|
||||
return { isValid: true };
|
||||
};
|
||||
|
||||
/**
|
||||
* 驗證倉庫表單資料
|
||||
*/
|
||||
export const validateWarehouse = (formData: {
|
||||
code: string;
|
||||
name: string;
|
||||
address: string;
|
||||
}): { isValid: boolean; error?: string } => {
|
||||
if (!formData.name.trim()) {
|
||||
return { isValid: false, error: "倉庫名稱為必填欄位" };
|
||||
}
|
||||
|
||||
if (!formData.address.trim()) {
|
||||
return { isValid: false, error: "倉庫地址為必填欄位" };
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
};
|
||||
Reference in New Issue
Block a user