diff --git a/app/Modules/Core/Controllers/ActivityLogController.php b/app/Modules/Core/Controllers/ActivityLogController.php index b7a8011..794fcbe 100644 --- a/app/Modules/Core/Controllers/ActivityLogController.php +++ b/app/Modules/Core/Controllers/ActivityLogController.php @@ -36,7 +36,11 @@ class ActivityLogController extends Controller public function index(Request $request) { - $perPage = $request->input('per_page', 10); + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = $request->input('per_page', $defaultPerPage); + if (!in_array((int)$perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; + } $sortBy = $request->input('sort_by', 'created_at'); $sortOrder = $request->input('sort_order', 'desc'); diff --git a/app/Modules/Core/Controllers/SystemSettingController.php b/app/Modules/Core/Controllers/SystemSettingController.php new file mode 100644 index 0000000..ef555f2 --- /dev/null +++ b/app/Modules/Core/Controllers/SystemSettingController.php @@ -0,0 +1,46 @@ +groupBy('group'); + + return Inertia::render('Admin/Setting/Index', [ + 'settings' => $settings, + ]); + } + + /** + * 更新系統設定 + */ + public function update(Request $request) + { + $validated = $request->validate([ + 'settings' => 'required|array', + 'settings.*.key' => 'required|string|exists:system_settings,key', + 'settings.*.value' => 'nullable', + ]); + + foreach ($validated['settings'] as $item) { + SystemSetting::where('key', $item['key'])->update([ + 'value' => $item['value'] + ]); + } + + // 清除記憶體快取,確保後續讀取拿到最新值 + SystemSetting::clearCache(); + + return redirect()->back()->with('success', '系統設定已更新'); + } +} diff --git a/app/Modules/Core/Controllers/UserController.php b/app/Modules/Core/Controllers/UserController.php index eafef39..e894c96 100644 --- a/app/Modules/Core/Controllers/UserController.php +++ b/app/Modules/Core/Controllers/UserController.php @@ -18,7 +18,12 @@ class UserController extends Controller */ public function index(Request $request) { - $perPage = $request->input('per_page', 10); + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = $request->input('per_page', $defaultPerPage); + + if (!in_array((int)$perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; + } $sortBy = $request->input('sort_by', 'id'); $sortOrder = $request->input('sort_order', 'asc'); $search = $request->input('search'); diff --git a/app/Modules/Core/Models/SystemSetting.php b/app/Modules/Core/Models/SystemSetting.php new file mode 100644 index 0000000..92e8bf4 --- /dev/null +++ b/app/Modules/Core/Models/SystemSetting.php @@ -0,0 +1,61 @@ +first(); + + if (!$setting) { + static::$cache[$key] = $default; + return $default; + } + + $value = $setting->value; + + // 根據 type 進行類別轉換 + $resolved = match ($setting->type) { + 'integer', 'number' => (int) $value, + 'boolean', 'bool' => filter_var($value, FILTER_VALIDATE_BOOLEAN), + 'json', 'array' => json_decode($value, true), + default => $value, + }; + + static::$cache[$key] = $resolved; + + return $resolved; + } + + /** + * 清除記憶體快取(儲存設定後應呼叫) + */ + public static function clearCache(): void + { + static::$cache = []; + } +} diff --git a/app/Modules/Core/Routes/web.php b/app/Modules/Core/Routes/web.php index e612703..1996d9c 100644 --- a/app/Modules/Core/Routes/web.php +++ b/app/Modules/Core/Routes/web.php @@ -7,6 +7,7 @@ use App\Modules\Core\Controllers\ProfileController; use App\Modules\Core\Controllers\RoleController; use App\Modules\Core\Controllers\UserController; use App\Modules\Core\Controllers\ActivityLogController; +use App\Modules\Core\Controllers\SystemSettingController; // 登入/登出路由 Route::get('/login', [LoginController::class, 'show'])->name('login'); @@ -56,5 +57,10 @@ Route::middleware('auth')->group(function () { Route::get('/activity-logs', [ActivityLogController::class, 'index'])->name('activity-logs.index'); }); + Route::middleware('permission:system.settings.view')->group(function () { + Route::get('/settings', [SystemSettingController::class, 'index'])->name('settings.index'); + Route::post('/settings', [SystemSettingController::class, 'update'])->name('settings.update'); + }); + }); }); diff --git a/app/Modules/Finance/Controllers/AccountPayableController.php b/app/Modules/Finance/Controllers/AccountPayableController.php index 2ab5916..9afa218 100644 --- a/app/Modules/Finance/Controllers/AccountPayableController.php +++ b/app/Modules/Finance/Controllers/AccountPayableController.php @@ -63,7 +63,11 @@ class AccountPayableController extends Controller $query->where('due_date', '<=', $request->date_end); } - $perPage = $request->input('per_page', 10); + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = $request->input('per_page', $defaultPerPage); + if (!in_array((int)$perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; + } $payables = $query->latest()->paginate($perPage)->withQueryString(); // Manual Hydration for Vendors diff --git a/app/Modules/Finance/Controllers/AccountingReportController.php b/app/Modules/Finance/Controllers/AccountingReportController.php index 6f66869..75452e5 100644 --- a/app/Modules/Finance/Controllers/AccountingReportController.php +++ b/app/Modules/Finance/Controllers/AccountingReportController.php @@ -27,7 +27,11 @@ class AccountingReportController extends Controller $allRecords = $reportData['records']; // 3. Manual Pagination - $perPage = $request->input('per_page', 10); + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = $request->input('per_page', $defaultPerPage); + if (!in_array((int)$perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; + } $page = $request->input('page', 1); $offset = ($page - 1) * $perPage; diff --git a/app/Modules/Finance/Services/AccountPayableService.php b/app/Modules/Finance/Services/AccountPayableService.php index 1ae3dc8..6ec6eb2 100644 --- a/app/Modules/Finance/Services/AccountPayableService.php +++ b/app/Modules/Finance/Services/AccountPayableService.php @@ -50,8 +50,8 @@ class AccountPayableService 'total_amount' => collect($receiptData['items'] ?? [])->sum('total_amount'), 'tax_amount' => 0, // 假設後續會實作稅額計算,目前預設為 0 'status' => AccountPayable::STATUS_PENDING, - // 設定應付日期,預設為進貨後 30 天 (可依據供應商設定調整) - 'due_date' => now()->addDays(30)->toDateString(), + // 設定應付日期,預設為進貨後天數 (由系統設定決定,預設 30 天) + 'due_date' => now()->addDays(\App\Modules\Core\Models\SystemSetting::getVal('finance.ap_payment_days', 30))->toDateString(), 'created_by' => $userId, 'remarks' => "由進貨單 {$receiptData['code']} 自動生成", ]); diff --git a/app/Modules/Finance/Services/FinanceService.php b/app/Modules/Finance/Services/FinanceService.php index 913d838..45ab42d 100644 --- a/app/Modules/Finance/Services/FinanceService.php +++ b/app/Modules/Finance/Services/FinanceService.php @@ -94,7 +94,13 @@ class FinanceService implements FinanceServiceInterface $sortDirection = $filters['sort_direction'] ?? 'desc'; $query->orderBy($sortField, $sortDirection); - return $query->paginate($filters['per_page'] ?? 10); + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = (int) ($filters['per_page'] ?? $defaultPerPage); + if (!in_array($perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; + } + + return $query->paginate($perPage); } public function getUniqueCategories(): Collection diff --git a/app/Modules/Integration/Controllers/SalesOrderController.php b/app/Modules/Integration/Controllers/SalesOrderController.php index 3eccaa1..ced9ec7 100644 --- a/app/Modules/Integration/Controllers/SalesOrderController.php +++ b/app/Modules/Integration/Controllers/SalesOrderController.php @@ -29,7 +29,13 @@ class SalesOrderController extends Controller // 排序 $query->orderBy('sold_at', 'desc'); - $orders = $query->paginate($request->input('per_page', 10)) + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = (int) $request->input('per_page', $defaultPerPage); + if (!in_array($perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; + } + + $orders = $query->paginate($perPage) ->withQueryString(); return Inertia::render('Integration/SalesOrders/Index', [ diff --git a/app/Modules/Inventory/Contracts/InventoryServiceInterface.php b/app/Modules/Inventory/Contracts/InventoryServiceInterface.php index 6f45a46..1976883 100644 --- a/app/Modules/Inventory/Contracts/InventoryServiceInterface.php +++ b/app/Modules/Inventory/Contracts/InventoryServiceInterface.php @@ -131,7 +131,7 @@ interface InventoryServiceInterface * @param int $perPage 每頁筆數 * @return array */ - public function getStockQueryData(array $filters = [], int $perPage = 10): array; + public function getStockQueryData(array $filters = [], ?int $perPage = null): array; /** * Get statistics for the dashboard. diff --git a/app/Modules/Inventory/Controllers/AdjustDocController.php b/app/Modules/Inventory/Controllers/AdjustDocController.php index 66300f7..51926ca 100644 --- a/app/Modules/Inventory/Controllers/AdjustDocController.php +++ b/app/Modules/Inventory/Controllers/AdjustDocController.php @@ -39,7 +39,11 @@ class AdjustDocController extends Controller $query->where('warehouse_id', $request->warehouse_id); } - $perPage = $request->input('per_page', 10); + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = $request->input('per_page', $defaultPerPage); + if (!in_array((int)$perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; + } $docs = $query->orderByDesc('created_at') ->paginate($perPage) ->withQueryString() diff --git a/app/Modules/Inventory/Controllers/CountDocController.php b/app/Modules/Inventory/Controllers/CountDocController.php index 90d3853..16c555d 100644 --- a/app/Modules/Inventory/Controllers/CountDocController.php +++ b/app/Modules/Inventory/Controllers/CountDocController.php @@ -35,9 +35,11 @@ class CountDocController extends Controller }); } - $perPage = $request->input('per_page', 10); - if (!in_array($perPage, [10, 20, 50, 100])) { - $perPage = 10; + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = $request->input('per_page', $defaultPerPage); + + if (!in_array((int)$perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; } $countQuery = function ($query) { diff --git a/app/Modules/Inventory/Controllers/GoodsReceiptController.php b/app/Modules/Inventory/Controllers/GoodsReceiptController.php index 37fe351..9a8b38a 100644 --- a/app/Modules/Inventory/Controllers/GoodsReceiptController.php +++ b/app/Modules/Inventory/Controllers/GoodsReceiptController.php @@ -63,7 +63,11 @@ class GoodsReceiptController extends Controller } // 每頁筆數 - $perPage = $request->input('per_page', 10); + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = $request->input('per_page', $defaultPerPage); + if (!in_array((int)$perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; + } $receipts = $query->orderBy('created_at', 'desc') ->paginate($perPage) diff --git a/app/Modules/Inventory/Controllers/InventoryAnalysisController.php b/app/Modules/Inventory/Controllers/InventoryAnalysisController.php index 9ce0ed4..369eb7a 100644 --- a/app/Modules/Inventory/Controllers/InventoryAnalysisController.php +++ b/app/Modules/Inventory/Controllers/InventoryAnalysisController.php @@ -24,7 +24,12 @@ class InventoryAnalysisController extends Controller 'warehouse_id', 'category_id', 'search', 'per_page', 'sort_by', 'sort_order', 'status' ]); - $analysisData = $this->turnoverService->getAnalysisData($filters, $request->input('per_page', 10)); + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = (int) $request->input('per_page', $defaultPerPage); + if (!in_array($perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; + } + $analysisData = $this->turnoverService->getAnalysisData($filters, $perPage); $kpis = $this->turnoverService->getKPIs($filters); return Inertia::render('Inventory/Analysis/Index', [ diff --git a/app/Modules/Inventory/Controllers/InventoryReportController.php b/app/Modules/Inventory/Controllers/InventoryReportController.php index 0e065a5..c2032ea 100644 --- a/app/Modules/Inventory/Controllers/InventoryReportController.php +++ b/app/Modules/Inventory/Controllers/InventoryReportController.php @@ -35,7 +35,12 @@ class InventoryReportController extends Controller $filters['date_to'] = date('Y-m-d'); } - $reportData = $this->reportService->getReportData($filters, $request->input('per_page', 10)); + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = (int) $request->input('per_page', $defaultPerPage); + if (!in_array($perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; + } + $reportData = $this->reportService->getReportData($filters, $perPage); $summary = $this->reportService->getSummary($filters); return Inertia::render('Inventory/Report/Index', [ diff --git a/app/Modules/Inventory/Controllers/ProductController.php b/app/Modules/Inventory/Controllers/ProductController.php index a1d9a30..def05c6 100644 --- a/app/Modules/Inventory/Controllers/ProductController.php +++ b/app/Modules/Inventory/Controllers/ProductController.php @@ -37,9 +37,11 @@ class ProductController extends Controller $query->where('category_id', $request->category_id); } - $perPage = $request->input('per_page', 10); - if (!in_array($perPage, [10, 20, 50, 100])) { - $perPage = 10; + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = $request->input('per_page', $defaultPerPage); + + if (!in_array((int)$perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; } $sortField = $request->input('sort_field', 'id'); diff --git a/app/Modules/Inventory/Controllers/StockQueryController.php b/app/Modules/Inventory/Controllers/StockQueryController.php index b797b8e..3816244 100644 --- a/app/Modules/Inventory/Controllers/StockQueryController.php +++ b/app/Modules/Inventory/Controllers/StockQueryController.php @@ -24,7 +24,12 @@ class StockQueryController extends Controller public function index(Request $request) { $filters = $request->only(['warehouse_id', 'category_id', 'search', 'status', 'sort_by', 'sort_order', 'per_page']); - $perPage = (int) ($filters['per_page'] ?? 10); + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = (int) ($filters['per_page'] ?? $defaultPerPage); + + if (!in_array($perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; + } $result = $this->inventoryService->getStockQueryData($filters, $perPage); diff --git a/app/Modules/Inventory/Controllers/StoreRequisitionController.php b/app/Modules/Inventory/Controllers/StoreRequisitionController.php index 8507b47..3ce7dc7 100644 --- a/app/Modules/Inventory/Controllers/StoreRequisitionController.php +++ b/app/Modules/Inventory/Controllers/StoreRequisitionController.php @@ -65,7 +65,11 @@ class StoreRequisitionController extends Controller $query->orderBy('id', 'desc'); } - $perPage = $request->input('per_page', 10); + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = $request->input('per_page', $defaultPerPage); + if (!in_array((int)$perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; + } $requisitions = $query->paginate($perPage)->withQueryString(); // 水和倉庫名稱與使用者名稱 diff --git a/app/Modules/Inventory/Controllers/TransferOrderController.php b/app/Modules/Inventory/Controllers/TransferOrderController.php index 5832401..2f6d7f5 100644 --- a/app/Modules/Inventory/Controllers/TransferOrderController.php +++ b/app/Modules/Inventory/Controllers/TransferOrderController.php @@ -42,7 +42,11 @@ class TransferOrderController extends Controller }); } - $perPage = $request->input('per_page', 10); + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = $request->input('per_page', $defaultPerPage); + if (!in_array((int)$perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; + } $orders = $query->orderByDesc('created_at') ->paginate($perPage) ->withQueryString() diff --git a/app/Modules/Inventory/Controllers/WarehouseController.php b/app/Modules/Inventory/Controllers/WarehouseController.php index 6b65297..9e6c217 100644 --- a/app/Modules/Inventory/Controllers/WarehouseController.php +++ b/app/Modules/Inventory/Controllers/WarehouseController.php @@ -24,9 +24,11 @@ class WarehouseController extends Controller }); } - $perPage = $request->input('per_page', 10); - if (!in_array($perPage, [10, 20, 50, 100])) { - $perPage = 10; + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = $request->input('per_page', $defaultPerPage); + + if (!in_array((int)$perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; } $warehouses = $query->withSum('inventories as book_stock', 'quantity') // 帳面庫存 = 所有庫存總和 diff --git a/app/Modules/Inventory/Services/InventoryReportService.php b/app/Modules/Inventory/Services/InventoryReportService.php index 152d605..73a176d 100644 --- a/app/Modules/Inventory/Services/InventoryReportService.php +++ b/app/Modules/Inventory/Services/InventoryReportService.php @@ -23,8 +23,9 @@ class InventoryReportService * @param int|null $perPage 每頁筆數 * @return \Illuminate\Pagination\LengthAwarePaginator|\Illuminate\Support\Collection */ - public function getReportData(array $filters, ?int $perPage = 10) + public function getReportData(array $filters, ?int $perPage = null) { + $perPage = $perPage ?? \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); $dateFrom = $filters['date_from'] ?? null; $dateTo = $filters['date_to'] ?? null; $warehouseId = $filters['warehouse_id'] ?? null; @@ -197,8 +198,9 @@ class InventoryReportService /** * 取得特定商品的庫存異動明細 */ - public function getProductDetails($productId, array $filters, ?int $perPage = 20) + public function getProductDetails($productId, array $filters, ?int $perPage = null) { + $perPage = $perPage ?? \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); $dateFrom = $filters['date_from'] ?? null; $dateTo = $filters['date_to'] ?? null; $warehouseId = $filters['warehouse_id'] ?? null; diff --git a/app/Modules/Inventory/Services/InventoryService.php b/app/Modules/Inventory/Services/InventoryService.php index bf8b1fa..defd7a6 100644 --- a/app/Modules/Inventory/Services/InventoryService.php +++ b/app/Modules/Inventory/Services/InventoryService.php @@ -258,10 +258,12 @@ class InventoryService implements InventoryServiceInterface /** * 即時庫存查詢:統計卡片 + 分頁明細 */ - public function getStockQueryData(array $filters = [], int $perPage = 10): array + public function getStockQueryData(array $filters = [], ?int $perPage = null): array { + $perPage = $perPage ?? \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); $today = now()->toDateString(); - $expiryThreshold = now()->addDays(30)->toDateString(); + $expiryDays = \App\Modules\Core\Models\SystemSetting::getVal('inventory.expiry_warning_days', 30); + $expiryThreshold = now()->addDays($expiryDays)->toDateString(); // 基礎查詢 $query = Inventory::query() @@ -492,7 +494,8 @@ class InventoryService implements InventoryServiceInterface public function getDashboardStats(): array { $today = now()->toDateString(); - $expiryThreshold = now()->addDays(30)->toDateString(); + $expiryDays = \App\Modules\Core\Models\SystemSetting::getVal('inventory.expiry_warning_days', 30); + $expiryThreshold = now()->addDays($expiryDays)->toDateString(); // 1. 庫存品項數 (明細總數) $totalItems = DB::table('inventories') diff --git a/app/Modules/Inventory/Services/TurnoverService.php b/app/Modules/Inventory/Services/TurnoverService.php index f74916c..88f3637 100644 --- a/app/Modules/Inventory/Services/TurnoverService.php +++ b/app/Modules/Inventory/Services/TurnoverService.php @@ -12,8 +12,9 @@ class TurnoverService /** * Get inventory turnover analysis data */ - public function getAnalysisData(array $filters, int $perPage = 20) + public function getAnalysisData(array $filters, ?int $perPage = null) { + $perPage = $perPage ?? \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); $warehouseId = $filters['warehouse_id'] ?? null; $categoryId = $filters['category_id'] ?? null; $search = $filters['search'] ?? null; @@ -60,7 +61,8 @@ class TurnoverService // Given potentially large data, subquery per row might be slow, but for pagination it's okay-ish. // Better approach: Join with a subquery of aggregated transactions. - $thirtyDaysAgo = Carbon::now()->subDays(30); + $analysisDays = \App\Modules\Core\Models\SystemSetting::getVal('turnover.analysis_period_days', 30); + $thirtyDaysAgo = Carbon::now()->subDays($analysisDays); // Subquery for 30-day sales $salesSubquery = InventoryTransaction::query() @@ -111,7 +113,7 @@ class TurnoverService // Turnover Days Calculation in SQL: (stock / (sales_30d / 30)) => (stock * 30) / sales_30d // Handle division by zero: if sales_30d is 0, turnover is 'Inf' (or very high number like 9999) $turnoverDaysSql = "CASE WHEN COALESCE(sales_30d.sales_qty_30d, 0) > 0 - THEN (COALESCE(SUM(inventories.quantity), 0) * 30) / sales_30d.sales_qty_30d + THEN (COALESCE(SUM(inventories.quantity), 0) * $analysisDays) / sales_30d.sales_qty_30d ELSE 9999 END"; $query->addSelect(DB::raw("$turnoverDaysSql as turnover_days")); @@ -125,7 +127,8 @@ class TurnoverService // For dead stock, definitive IS stock > 0. if ($statusFilter === 'dead') { - $ninetyDaysAgo = Carbon::now()->subDays(90); + $deadStockDays = \App\Modules\Core\Models\SystemSetting::getVal('turnover.dead_stock_days', 90); + $ninetyDaysAgo = Carbon::now()->subDays($deadStockDays); $query->havingRaw("current_stock > 0 AND (last_sale_date < ? OR last_sale_date IS NULL)", [$ninetyDaysAgo]); } @@ -146,10 +149,13 @@ class TurnoverService $lastSale = $item->last_sale_date ? Carbon::parse($item->last_sale_date) : null; $daysSinceSale = $lastSale ? $lastSale->diffInDays(Carbon::now()) : 9999; - if ($item->current_stock > 0 && $daysSinceSale > 90) { + $deadStockDays = \App\Modules\Core\Models\SystemSetting::getVal('turnover.dead_stock_days', 90); + $slowMovingDays = \App\Modules\Core\Models\SystemSetting::getVal('turnover.slow_moving_days', 60); + + if ($item->current_stock > 0 && $daysSinceSale > $deadStockDays) { $item->status = 'dead'; // 滯銷 $item->status_label = '滯銷'; - } elseif ($item->current_stock > 0 && $item->turnover_days > 60) { + } elseif ($item->current_stock > 0 && $item->turnover_days > $slowMovingDays) { $item->status = 'slow'; // 週轉慢 $item->status_label = '週轉慢'; } elseif ($item->current_stock == 0) { @@ -187,8 +193,8 @@ class TurnoverService // 2. Dead Stock Value (No sale in 90 days) // Need last sale date for each product-location or just product? // Assuming dead stock is product-level logic for simplicity. - - $ninetyDaysAgo = Carbon::now()->subDays(90); + $deadStockDays = \App\Modules\Core\Models\SystemSetting::getVal('turnover.dead_stock_days', 90); + $ninetyDaysAgo = Carbon::now()->subDays($deadStockDays); // Get IDs of products sold in last 90 days $soldProductIds = InventoryTransaction::query() @@ -225,17 +231,17 @@ class TurnoverService // Simplified: (Total Stock / Total Sales 30d) * 30 $totalStock = (clone $buildInvQuery())->sum('inventories.quantity'); - - $totalSales30d = DB::table('inventory_transactions') - ->join('inventories', 'inventory_transactions.inventory_id', '=', 'inventories.id') - ->join('products', 'inventories.product_id', '=', 'products.id') - ->where('inventory_transactions.type', '出庫') - ->where('inventory_transactions.actual_time', '>=', Carbon::now()->subDays(30)) - ->when($warehouseId, fn($q) => $q->where('inventories.warehouse_id', $warehouseId)) - ->when($categoryId, fn($q) => $q->where('products.category_id', $categoryId)) - ->sum(DB::raw('ABS(inventory_transactions.quantity)')); - - $avgTurnoverDays = $totalSales30d > 0 ? ($totalStock * 30) / $totalSales30d : 0; + $analysisDays = \App\Modules\Core\Models\SystemSetting::getVal('turnover.analysis_period_days', 30); + $totalSales30d = DB::table('inventory_transactions') + ->join('inventories', 'inventory_transactions.inventory_id', '=', 'inventories.id') + ->join('products', 'inventories.product_id', '=', 'products.id') + ->where('inventory_transactions.type', '出庫') + ->where('inventory_transactions.actual_time', '>=', Carbon::now()->subDays($analysisDays)) + ->when($warehouseId, fn($q) => $q->where('inventories.warehouse_id', $warehouseId)) + ->when($categoryId, fn($q) => $q->where('products.category_id', $categoryId)) + ->sum(DB::raw('ABS(inventory_transactions.quantity)')); + + $avgTurnoverDays = $totalSales30d > 0 ? ($totalStock * $analysisDays) / $totalSales30d : 0; return [ 'total_stock_value' => $totalValue, diff --git a/app/Modules/Procurement/Controllers/PurchaseOrderController.php b/app/Modules/Procurement/Controllers/PurchaseOrderController.php index 281291e..348c38b 100644 --- a/app/Modules/Procurement/Controllers/PurchaseOrderController.php +++ b/app/Modules/Procurement/Controllers/PurchaseOrderController.php @@ -66,7 +66,11 @@ class PurchaseOrderController extends Controller $query->orderBy($sortField, $sortDirection); } - $perPage = $request->input('per_page', 10); + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = $request->input('per_page', $defaultPerPage); + if (!in_array((int)$perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; + } $orders = $query->paginate($perPage)->withQueryString(); // 2. 手動注入倉庫與使用者資料 diff --git a/app/Modules/Procurement/Controllers/PurchaseReturnController.php b/app/Modules/Procurement/Controllers/PurchaseReturnController.php index 42e8301..c54d0e6 100644 --- a/app/Modules/Procurement/Controllers/PurchaseReturnController.php +++ b/app/Modules/Procurement/Controllers/PurchaseReturnController.php @@ -35,11 +35,12 @@ class PurchaseReturnController extends Controller $query->where('status', $request->status); } - $purchaseReturns = $query->paginate(15)->withQueryString(); + $perPage = $request->input('per_page', 15); + $purchaseReturns = $query->paginate($perPage)->withQueryString(); return Inertia::render('PurchaseReturn/Index', [ 'purchaseReturns' => $purchaseReturns, - 'filters' => $request->only(['search', 'status']), + 'filters' => $request->only(['search', 'status', 'per_page']), ]); } diff --git a/app/Modules/Procurement/Controllers/ShippingOrderController.php b/app/Modules/Procurement/Controllers/ShippingOrderController.php index f817e8a..26caf6a 100644 --- a/app/Modules/Procurement/Controllers/ShippingOrderController.php +++ b/app/Modules/Procurement/Controllers/ShippingOrderController.php @@ -48,7 +48,11 @@ class ShippingOrderController extends Controller $query->where('status', $request->status); } - $perPage = $request->input('per_page', 10); + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = $request->input('per_page', $defaultPerPage); + if (!in_array((int)$perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; + } $orders = $query->orderBy('id', 'desc')->paginate($perPage)->withQueryString(); // 水和倉庫與使用者 diff --git a/app/Modules/Procurement/Controllers/VendorController.php b/app/Modules/Procurement/Controllers/VendorController.php index b8521ce..1d598b5 100644 --- a/app/Modules/Procurement/Controllers/VendorController.php +++ b/app/Modules/Procurement/Controllers/VendorController.php @@ -44,7 +44,11 @@ class VendorController extends Controller $sortDirection = 'desc'; } - $perPage = $request->input('per_page', 10); + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = $request->input('per_page', $defaultPerPage); + if (!in_array((int)$perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; + } $vendors = $query->orderBy($sortField, $sortDirection) ->paginate($perPage) diff --git a/app/Modules/Production/Controllers/ProductionOrderController.php b/app/Modules/Production/Controllers/ProductionOrderController.php index fe69c93..a1bebf9 100644 --- a/app/Modules/Production/Controllers/ProductionOrderController.php +++ b/app/Modules/Production/Controllers/ProductionOrderController.php @@ -62,7 +62,11 @@ class ProductionOrderController extends Controller $query->orderBy($request->input('sort_field', 'created_at'), $request->input('sort_direction', 'desc')); // 分頁 - $perPage = $request->input('per_page', 10); + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = $request->input('per_page', $defaultPerPage); + if (!in_array((int)$perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; + } $productionOrders = $query->paginate($perPage)->withQueryString(); // --- 手動資料水和 (Manual Hydration) --- diff --git a/app/Modules/Production/Controllers/RecipeController.php b/app/Modules/Production/Controllers/RecipeController.php index 10dff11..59d1de3 100644 --- a/app/Modules/Production/Controllers/RecipeController.php +++ b/app/Modules/Production/Controllers/RecipeController.php @@ -40,7 +40,13 @@ class RecipeController extends Controller $query->orderBy($request->input('sort_field', 'created_at'), $request->input('sort_direction', 'desc')); - $recipes = $query->paginate($request->input('per_page', 10))->withQueryString(); + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = (int) $request->input('per_page', $defaultPerPage); + if (!in_array($perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; + } + + $recipes = $query->paginate($perPage)->withQueryString(); // Manual Hydration $productIds = $recipes->pluck('product_id')->unique()->filter()->toArray(); diff --git a/app/Modules/Sales/Controllers/SalesImportController.php b/app/Modules/Sales/Controllers/SalesImportController.php index c7a7e4f..01d2d35 100644 --- a/app/Modules/Sales/Controllers/SalesImportController.php +++ b/app/Modules/Sales/Controllers/SalesImportController.php @@ -15,7 +15,11 @@ class SalesImportController extends Controller { public function index(Request $request) { - $perPage = $request->input('per_page', 10); + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = $request->input('per_page', $defaultPerPage); + if (!in_array((int)$perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; + } $search = $request->input('search'); $batches = SalesImportBatch::with('importer') @@ -65,7 +69,11 @@ class SalesImportController extends Controller { $import->load(['items', 'importer']); - $perPage = $request->input('per_page', 10); + $defaultPerPage = \App\Modules\Core\Models\SystemSetting::getVal('display.per_page', 10); + $perPage = $request->input('per_page', $defaultPerPage); + if (!in_array((int)$perPage, [10, 20, 50, 100])) { + $perPage = $defaultPerPage; + } $paginatedItems = $import->items()->paginate($perPage)->withQueryString(); // Manual Hydration for Products and Warehouses diff --git a/database/migrations/tenant/2026_02_25_151106_create_system_settings_table.php b/database/migrations/tenant/2026_02_25_151106_create_system_settings_table.php new file mode 100644 index 0000000..5e6e4ea --- /dev/null +++ b/database/migrations/tenant/2026_02_25_151106_create_system_settings_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('group')->index()->comment('設定分組'); + $table->string('key')->unique()->comment('設定鍵名'); + $table->text('value')->nullable()->comment('設定值'); + $table->string('type')->default('string')->comment('資料型別'); + $table->string('description')->nullable()->comment('功能說明'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('system_settings'); + } +}; diff --git a/database/seeders/PermissionSeeder.php b/database/seeders/PermissionSeeder.php index 467a949..ea50e80 100644 --- a/database/seeders/PermissionSeeder.php +++ b/database/seeders/PermissionSeeder.php @@ -124,6 +124,10 @@ class PermissionSeeder extends Seeder // 系統日誌 'system.view_logs' => '檢視日誌', + // 系統設定 + 'system.settings.view' => '檢視設定', + 'system.settings.edit' => '編輯設定', + // 公共事業費管理 'utility_fees.view' => '檢視', 'utility_fees.create' => '建立', diff --git a/database/seeders/SystemSettingSeeder.php b/database/seeders/SystemSettingSeeder.php new file mode 100644 index 0000000..8be5324 --- /dev/null +++ b/database/seeders/SystemSettingSeeder.php @@ -0,0 +1,68 @@ + 'finance', + 'key' => 'finance.ap_payment_days', + 'value' => '30', + 'type' => 'integer', + 'description' => '應付帳款預設付款天數', + ], + // 📦 庫存管理 + [ + 'group' => 'inventory', + 'key' => 'inventory.expiry_warning_days', + 'value' => '30', + 'type' => 'integer', + 'description' => '商品到期預警天數', + ], + // 📊 周轉率分析 + [ + 'group' => 'turnover', + 'key' => 'turnover.analysis_period_days', + 'value' => '30', + 'type' => 'integer', + 'description' => '周轉率分析:銷售統計期間(天)', + ], + [ + 'group' => 'turnover', + 'key' => 'turnover.dead_stock_days', + 'value' => '90', + 'type' => 'integer', + 'description' => '周轉率分析:滯銷判定天數', + ], + [ + 'group' => 'turnover', + 'key' => 'turnover.slow_moving_days', + 'value' => '60', + 'type' => 'integer', + 'description' => '周轉率分析:週轉慢判定天數', + ], + // 🖥️ 顯示設定 + [ + 'group' => 'display', + 'key' => 'display.per_page', + 'value' => '10', + 'type' => 'integer', + 'description' => '每頁預設筆數', + ], + ]; + + foreach ($settings as $setting) { + \App\Modules\Core\Models\SystemSetting::updateOrCreate( + ['key' => $setting['key']], + $setting + ); + } + } +} diff --git a/resources/js/Layouts/AuthenticatedLayout.tsx b/resources/js/Layouts/AuthenticatedLayout.tsx index 39e7511..d387bf1 100644 --- a/resources/js/Layouts/AuthenticatedLayout.tsx +++ b/resources/js/Layouts/AuthenticatedLayout.tsx @@ -307,6 +307,13 @@ export default function AuthenticatedLayout({ route: "/admin/activity-logs", permission: "system.view_logs", }, + { + id: "system-settings", + label: "系統設定", + icon: , + route: "/admin/settings", + permission: "system.settings.view", + }, { id: "manual", label: "操作手冊", diff --git a/resources/js/Pages/AccountPayable/Index.tsx b/resources/js/Pages/AccountPayable/Index.tsx index 31ca26e..0157867 100644 --- a/resources/js/Pages/AccountPayable/Index.tsx +++ b/resources/js/Pages/AccountPayable/Index.tsx @@ -260,7 +260,7 @@ export default function AccountPayableIndex({ payables, filters, vendors }: any) -
+
每頁顯示 @@ -278,7 +278,7 @@ export default function AccountPayableIndex({ payables, filters, vendors }: any) />
- 共 {payables.total} 筆紀錄 + 共 {payables.total} 筆資料
diff --git a/resources/js/Pages/Accounting/Report.tsx b/resources/js/Pages/Accounting/Report.tsx index 68b3ce8..404bd7d 100644 --- a/resources/js/Pages/Accounting/Report.tsx +++ b/resources/js/Pages/Accounting/Report.tsx @@ -365,21 +365,24 @@ export default function AccountingReport({ records, summary, filters }: PageProp {/* Pagination Footer */}
-
- 每頁顯示 - - +
+
+ 每頁顯示 + + +
+ 共 {records.total} 筆資料
diff --git a/resources/js/Pages/Admin/ActivityLog/Index.tsx b/resources/js/Pages/Admin/ActivityLog/Index.tsx index 87d686e..ef9962a 100644 --- a/resources/js/Pages/Admin/ActivityLog/Index.tsx +++ b/resources/js/Pages/Admin/ActivityLog/Index.tsx @@ -318,22 +318,25 @@ export default function ActivityLogIndex({ activities, filters, subject_types, u from={activities.from} /> -
-
- 每頁顯示 - - +
+
+
+ 每頁顯示 + + +
+ 共 {activities.total} 筆資料
diff --git a/resources/js/Pages/Admin/Setting/Index.tsx b/resources/js/Pages/Admin/Setting/Index.tsx new file mode 100644 index 0000000..83aa498 --- /dev/null +++ b/resources/js/Pages/Admin/Setting/Index.tsx @@ -0,0 +1,178 @@ +import React from "react"; +import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; +import { Head, useForm } from "@inertiajs/react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle +} from "@/Components/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/Components/ui/tabs"; +import { Input } from "@/Components/ui/input"; +import { Label } from "@/Components/ui/label"; +import { Button } from "@/Components/ui/button"; +import { + Coins, + Package, + RefreshCcw, + Monitor, + Save, + Settings +} from "lucide-react"; +import { toast } from "sonner"; + +interface Setting { + key: string; + value: string; + description: string; +} + +interface PageProps { + settings: Record; +} + +export default function SettingIndex({ settings }: PageProps) { + const { data, setData, post, processing } = useForm({ + settings: Object.values(settings).flat().map(s => ({ + key: s.key, + value: s.value + })) + }); + + const handleValueChange = (key: string, value: string) => { + const newSettings = data.settings.map(s => + s.key === key ? { ...s, value } : s + ); + setData('settings', newSettings); + }; + + const submit = (e: React.FormEvent) => { + e.preventDefault(); + post(route('settings.update'), { + onSuccess: () => toast.success("系統設定已更新"), + }); + }; + + const renderSettingRow = (setting: Setting) => { + const currentVal = data.settings.find(s => s.key === setting.key)?.value || ''; + + return ( +
+
+ +

{setting.key}

+
+
+ handleValueChange(setting.key, e.target.value)} + className="max-w-xs" + /> +
+
+ ); + }; + + return ( + + + +
+
+

+ + 系統設定 +

+

+ 管理全系統的預設值與業務規則。 +

+
+ +
+ + + + 財務設定 + + + 庫存管理 + + + 周轉率分析 + + + 顯示設定 + + + + + + + 財務設定 + 管理應付帳款與稅務相關的預設規則。 + + + {settings.finance?.map(renderSettingRow)} + + + + + + + + 庫存管理 + 管理商品效期、預警等庫存核心設定。 + + + {settings.inventory?.map(renderSettingRow)} + + + + + + + + 周轉率分析 + 調整商品周轉率計算與滯銷判定的天數標準。 + + + {settings.turnover?.map(renderSettingRow)} + + + + + + + + 顯示設定 + 設定全系統清單頁面的顯示偏好。 + + + {settings.display?.map(renderSettingRow)} + + + + +
+ +
+
+
+
+
+ ); +} diff --git a/resources/js/Pages/Admin/User/Index.tsx b/resources/js/Pages/Admin/User/Index.tsx index 27931a2..27db872 100644 --- a/resources/js/Pages/Admin/User/Index.tsx +++ b/resources/js/Pages/Admin/User/Index.tsx @@ -56,6 +56,7 @@ interface Props { users: { data: User[]; from: number; + total: number; links: PaginationLinks[]; }; filters: { @@ -394,22 +395,25 @@ export default function UserIndex({ users, roles, filters }: Props) {
{/* 分頁元件 - 統一樣式 */} -
-
- 每頁顯示 - - +
+
+
+ 每頁顯示 + + +
+ 共 {users.total} 筆資料
diff --git a/resources/js/Pages/Integration/SalesOrders/Index.tsx b/resources/js/Pages/Integration/SalesOrders/Index.tsx index a490c56..9a168a0 100644 --- a/resources/js/Pages/Integration/SalesOrders/Index.tsx +++ b/resources/js/Pages/Integration/SalesOrders/Index.tsx @@ -280,9 +280,7 @@ export default function SalesOrderIndex({ orders, filters }: Props) { />
- - 共 {orders.total} 筆紀錄 - + 共 {orders.total} 筆資料
diff --git a/resources/js/Pages/Inventory/Adjust/Index.tsx b/resources/js/Pages/Inventory/Adjust/Index.tsx index 2ddbfef..c878cbb 100644 --- a/resources/js/Pages/Inventory/Adjust/Index.tsx +++ b/resources/js/Pages/Inventory/Adjust/Index.tsx @@ -353,7 +353,7 @@ export default function Index({ docs, warehouses, filters }: { docs: DocsPaginat
-
+
每頁顯示 @@ -371,7 +371,7 @@ export default function Index({ docs, warehouses, filters }: { docs: DocsPaginat />
- 共 {docs?.total || 0} 筆紀錄 + 共 {docs.total} 筆資料
diff --git a/resources/js/Pages/Inventory/Analysis/Index.tsx b/resources/js/Pages/Inventory/Analysis/Index.tsx index 11ae8e6..6f7d2d2 100644 --- a/resources/js/Pages/Inventory/Analysis/Index.tsx +++ b/resources/js/Pages/Inventory/Analysis/Index.tsx @@ -415,22 +415,25 @@ export default function InventoryAnalysisIndex({ analysisData, kpis, warehouses,
{/* Pagination Footer */} -
-
- 每頁顯示 - - +
+
+
+ 每頁顯示 + + +
+ 共 {analysisData.total} 筆資料
diff --git a/resources/js/Pages/Inventory/Count/Index.tsx b/resources/js/Pages/Inventory/Count/Index.tsx index 45c97e0..f6394ca 100644 --- a/resources/js/Pages/Inventory/Count/Index.tsx +++ b/resources/js/Pages/Inventory/Count/Index.tsx @@ -368,7 +368,7 @@ export default function Index({ docs, warehouses, filters }: any) {
-
+
每頁顯示 @@ -386,7 +386,7 @@ export default function Index({ docs, warehouses, filters }: any) { />
- 共 {docs.total} 筆紀錄 + 共 {docs.total} 筆資料
diff --git a/resources/js/Pages/Inventory/GoodsReceipt/Index.tsx b/resources/js/Pages/Inventory/GoodsReceipt/Index.tsx index a9613d1..afdf69f 100644 --- a/resources/js/Pages/Inventory/GoodsReceipt/Index.tsx +++ b/resources/js/Pages/Inventory/GoodsReceipt/Index.tsx @@ -45,7 +45,7 @@ export default function GoodsReceiptIndex({ receipts, filters, warehouses }: Pro const [warehouseId, setWarehouseId] = useState(filters.warehouse_id || 'all'); const [dateStart, setDateStart] = useState(filters.date_start || ''); const [dateEnd, setDateEnd] = useState(filters.date_end || ''); - const [perPage, setPerPage] = useState(filters.per_page || '10'); + const [perPage, setPerPage] = useState(filters.per_page || receipts.per_page?.toString() || '10'); const [dateRangeType, setDateRangeType] = useState('custom'); // Advanced Filter Toggle @@ -58,7 +58,7 @@ export default function GoodsReceiptIndex({ receipts, filters, warehouses }: Pro setWarehouseId(filters.warehouse_id || 'all'); setDateStart(filters.date_start || ''); setDateEnd(filters.date_end || ''); - setPerPage(filters.per_page || '10'); + setPerPage(filters.per_page || receipts.per_page?.toString() || '10'); }, [filters]); const handleFilter = () => { @@ -285,22 +285,25 @@ export default function GoodsReceiptIndex({ receipts, filters, warehouses }: Pro {/* Pagination */} -
-
- 每頁顯示 - - +
+
+
+ 每頁顯示 + + +
+ 共 {receipts.total} 筆資料
diff --git a/resources/js/Pages/Inventory/Report/Index.tsx b/resources/js/Pages/Inventory/Report/Index.tsx index 077b58b..fd8dfeb 100644 --- a/resources/js/Pages/Inventory/Report/Index.tsx +++ b/resources/js/Pages/Inventory/Report/Index.tsx @@ -581,22 +581,25 @@ export default function InventoryReportIndex({ reportData, summary, warehouses,
{/* Pagination Footer */} -
-
- 每頁顯示 - - +
+
+
+ 每頁顯示 + + +
+ 共 {reportData.total} 筆資料
diff --git a/resources/js/Pages/Inventory/StockQuery/Index.tsx b/resources/js/Pages/Inventory/StockQuery/Index.tsx index 2bcd737..f0108ab 100644 --- a/resources/js/Pages/Inventory/StockQuery/Index.tsx +++ b/resources/js/Pages/Inventory/StockQuery/Index.tsx @@ -123,7 +123,7 @@ export default function StockQueryIndex({ }: Props) { const [search, setSearch] = useState(filters.search || ""); const [perPage, setPerPage] = useState( - filters.per_page || "10" + filters.per_page || inventories.per_page?.toString() || "10" ); // 執行篩選 diff --git a/resources/js/Pages/Inventory/Transfer/Index.tsx b/resources/js/Pages/Inventory/Transfer/Index.tsx index 281b540..d548a71 100644 --- a/resources/js/Pages/Inventory/Transfer/Index.tsx +++ b/resources/js/Pages/Inventory/Transfer/Index.tsx @@ -386,7 +386,7 @@ export default function Index({ warehouses, orders, filters }: any) {
-
+
每頁顯示 @@ -404,7 +404,7 @@ export default function Index({ warehouses, orders, filters }: any) { />
- 共 {orders.total} 筆紀錄 + 共 {orders.total} 筆資料
diff --git a/resources/js/Pages/Product/Index.tsx b/resources/js/Pages/Product/Index.tsx index e8ce9ad..e8ac593 100644 --- a/resources/js/Pages/Product/Index.tsx +++ b/resources/js/Pages/Product/Index.tsx @@ -51,6 +51,9 @@ interface PageProps { data: Product[]; links: any[]; // Todo: pagination types from: number; + per_page: number; + current_page: number; + total: number; }; categories: Category[]; units: Unit[]; @@ -67,7 +70,7 @@ export default function ProductManagement({ products, categories, units, filters const { branding } = usePage().props; const [searchTerm, setSearchTerm] = useState(filters.search || ""); const [typeFilter, setTypeFilter] = useState(filters.category_id || "all"); - const [perPage, setPerPage] = useState(filters.per_page || "10"); + const [perPage, setPerPage] = useState(filters.per_page || products.per_page?.toString() || "10"); const [sortField, setSortField] = useState(filters.sort_field || null); const [sortDirection, setSortDirection] = useState<"asc" | "desc" | null>(filters.sort_direction as "asc" | "desc" || null); const [isCategoryDialogOpen, setIsCategoryDialogOpen] = useState(false); @@ -78,12 +81,10 @@ export default function ProductManagement({ products, categories, units, filters useEffect(() => { setSearchTerm(filters.search || ""); setTypeFilter(filters.category_id || "all"); - setSearchTerm(filters.search || ""); - setTypeFilter(filters.category_id || "all"); - setPerPage(filters.per_page || "10"); + setPerPage(filters.per_page || products.per_page?.toString() || "10"); setSortField(filters.sort_field || null); setSortDirection(filters.sort_direction as "asc" | "desc" || null); - }, [filters]); + }, [filters, products.per_page]); const handleSort = (field: string) => { let newField: string | null = field; @@ -270,22 +271,25 @@ export default function ProductManagement({ products, categories, units, filters /> {/* 分頁元件 */} -
-
- 每頁顯示 - - +
+
+
+ 每頁顯示 + + +
+ 共 {products.total} 筆資料
diff --git a/resources/js/Pages/Production/Index.tsx b/resources/js/Pages/Production/Index.tsx index cbda4b6..f93b918 100644 --- a/resources/js/Pages/Production/Index.tsx +++ b/resources/js/Pages/Production/Index.tsx @@ -43,6 +43,7 @@ interface Props { data: ProductionOrder[]; links: any[]; total: number; + per_page: number; from: number; to: number; }; @@ -66,12 +67,12 @@ const statusOptions = [ export default function ProductionIndex({ productionOrders, filters }: Props) { const [search, setSearch] = useState(filters.search || ""); const [status, setStatus] = useState(filters.status || "all"); - const [perPage, setPerPage] = useState(filters.per_page || "10"); + const [perPage, setPerPage] = useState(filters.per_page || productionOrders.per_page?.toString() || "10"); useEffect(() => { setSearch(filters.search || ""); setStatus(filters.status || "all"); - setPerPage(filters.per_page || "10"); + setPerPage(filters.per_page || productionOrders.per_page?.toString() || "10"); }, [filters]); const handleFilter = () => { @@ -302,22 +303,25 @@ export default function ProductionIndex({ productionOrders, filters }: Props) {
{/* 分頁 */} -
-
- 每頁顯示 - - +
+
+
+ 每頁顯示 + + +
+ 共 {productionOrders.total} 筆資料
diff --git a/resources/js/Pages/Production/Recipe/Index.tsx b/resources/js/Pages/Production/Recipe/Index.tsx index c8759dd..9fd2bf1 100644 --- a/resources/js/Pages/Production/Recipe/Index.tsx +++ b/resources/js/Pages/Production/Recipe/Index.tsx @@ -306,22 +306,25 @@ export default function RecipeIndex({ recipes, filters }: Props) {
{/* 分頁 */} -
-
- 每頁顯示 - - +
+
+
+ 每頁顯示 + + +
+ 共 {recipes.total} 筆資料
diff --git a/resources/js/Pages/PurchaseOrder/Index.tsx b/resources/js/Pages/PurchaseOrder/Index.tsx index 9b692ed..ef71597 100644 --- a/resources/js/Pages/PurchaseOrder/Index.tsx +++ b/resources/js/Pages/PurchaseOrder/Index.tsx @@ -30,6 +30,7 @@ interface Props { data: PurchaseOrder[]; links: any[]; total: number; + per_page: number; from: number; to: number; }; @@ -53,7 +54,7 @@ export default function PurchaseOrderIndex({ orders, filters, warehouses }: Prop const [warehouseId, setWarehouseId] = useState(filters.warehouse_id || "all"); const [dateStart, setDateStart] = useState(filters.date_start || ""); const [dateEnd, setDateEnd] = useState(filters.date_end || ""); - const [perPage, setPerPage] = useState(filters.per_page || "10"); + const [perPage, setPerPage] = useState(filters.per_page || orders.per_page?.toString() || "10"); const [dateRangeType, setDateRangeType] = useState('custom'); // Advanced Filter Toggle @@ -66,7 +67,7 @@ export default function PurchaseOrderIndex({ orders, filters, warehouses }: Prop setWarehouseId(filters.warehouse_id || "all"); setDateStart(filters.date_start || ""); setDateEnd(filters.date_end || ""); - setPerPage(filters.per_page || "10"); + setPerPage(filters.per_page || orders.per_page?.toString() || "10"); }, [filters]); const handleFilter = () => { @@ -295,22 +296,25 @@ export default function PurchaseOrderIndex({ orders, filters, warehouses }: Prop /> {/* 分頁元件 - 統一樣式 */} -
-
- 每頁顯示 - - +
+
+
+ 每頁顯示 + + +
+ 共 {orders.total} 筆資料
diff --git a/resources/js/Pages/PurchaseReturn/Index.tsx b/resources/js/Pages/PurchaseReturn/Index.tsx index 3664913..21283de 100644 --- a/resources/js/Pages/PurchaseReturn/Index.tsx +++ b/resources/js/Pages/PurchaseReturn/Index.tsx @@ -14,6 +14,7 @@ import { getBreadcrumbs } from "@/utils/breadcrumb"; import { Can } from "@/Components/Permission/Can"; import { Input } from "@/Components/ui/input"; import { Label } from "@/Components/ui/label"; +import { SearchableSelect } from "@/Components/ui/searchable-select"; import { Select, SelectContent, @@ -34,17 +35,20 @@ interface Props { filters: { search?: string; status?: string; + per_page?: string; }; } export default function PurchaseReturnIndex({ purchaseReturns, filters }: Props) { const [search, setSearch] = useState(filters.search || ""); const [status, setStatus] = useState(filters.status || "all"); + const [perPage, setPerPage] = useState(filters.per_page || "15"); // 同步 URL 參數 useEffect(() => { setSearch(filters.search || ""); setStatus(filters.status || "all"); + setPerPage(filters.per_page || "15"); }, [filters]); const handleFilter = () => { @@ -53,6 +57,7 @@ export default function PurchaseReturnIndex({ purchaseReturns, filters }: Props) { search, status: status === 'all' ? undefined : status, + per_page: perPage, }, { preserveState: true, replace: true } ); @@ -61,9 +66,19 @@ export default function PurchaseReturnIndex({ purchaseReturns, filters }: Props) const handleReset = () => { setSearch(""); setStatus("all"); + setPerPage("15"); router.get(route('purchase-returns.index')); }; + const handlePerPageChange = (value: string) => { + setPerPage(value); + router.get( + route('purchase-returns.index'), + { ...filters, per_page: value, page: 1 }, + { preserveState: false, replace: true, preserveScroll: true } + ); + }; + const handleNavigateToCreate = () => { router.get(route('purchase-returns.create')); }; @@ -163,8 +178,29 @@ export default function PurchaseReturnIndex({ purchaseReturns, filters }: Props) /> {/* 分頁元件 */} -
- +
+
+
+ 每頁顯示 + + +
+ 共 {purchaseReturns.total} 筆資料 +
+
+ +
diff --git a/resources/js/Pages/Sales/Import/Index.tsx b/resources/js/Pages/Sales/Import/Index.tsx index 46a37bf..dc97a89 100644 --- a/resources/js/Pages/Sales/Import/Index.tsx +++ b/resources/js/Pages/Sales/Import/Index.tsx @@ -47,6 +47,7 @@ interface Props { batches: { data: ImportBatch[]; links: any[]; // Pagination links + total: number; }; filters?: { per_page?: string; @@ -250,22 +251,25 @@ export default function SalesImportIndex({ batches, filters = {} }: Props) {
{/* Pagination */} -
-
- 每頁顯示 - - +
+
+
+ 每頁顯示 + + +
+ 共 {batches.total} 筆資料
diff --git a/resources/js/Pages/ShippingOrder/Index.tsx b/resources/js/Pages/ShippingOrder/Index.tsx index a16c96a..457bedf 100644 --- a/resources/js/Pages/ShippingOrder/Index.tsx +++ b/resources/js/Pages/ShippingOrder/Index.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect } from "react"; -import { Plus, Package, Search, RotateCcw, ChevronDown, ChevronUp } from 'lucide-react'; +import { useState } from "react"; +import { Plus, Package, Search, RotateCcw } from 'lucide-react'; import { Button } from "@/Components/ui/button"; import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; import { Head, router, Link } from "@inertiajs/react"; @@ -30,7 +30,7 @@ interface Props { warehouses: { id: number; name: string }[]; } -export default function ShippingOrderIndex({ orders, filters, warehouses }: Props) { +export default function ShippingOrderIndex({ orders, filters }: Props) { const [search, setSearch] = useState(filters.search || ""); const [status, setStatus] = useState(filters.status || "all"); @@ -198,7 +198,10 @@ export default function ShippingOrderIndex({ orders, filters, warehouses }: Prop
-
+
+
+ 共 {(orders as any).total} 筆資料 +
diff --git a/resources/js/Pages/StoreRequisition/Index.tsx b/resources/js/Pages/StoreRequisition/Index.tsx index 34edada..b2900a7 100644 --- a/resources/js/Pages/StoreRequisition/Index.tsx +++ b/resources/js/Pages/StoreRequisition/Index.tsx @@ -367,7 +367,7 @@ export default function Index({
{/* 分頁 */} -
+
每頁顯示 @@ -385,7 +385,7 @@ export default function Index({ />
- 共 {requisitions.total} 筆紀錄 + 共 {requisitions.total} 筆資料
diff --git a/resources/js/Pages/UtilityFee/Index.tsx b/resources/js/Pages/UtilityFee/Index.tsx index 0ceef2c..5d72c11 100644 --- a/resources/js/Pages/UtilityFee/Index.tsx +++ b/resources/js/Pages/UtilityFee/Index.tsx @@ -464,22 +464,25 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }:
-
-
- 每頁顯示 - - +
+
+
+ 每頁顯示 + + +
+ 共 {fees.total} 筆資料
diff --git a/resources/js/Pages/Vendor/Index.tsx b/resources/js/Pages/Vendor/Index.tsx index 92604bd..5941a2e 100644 --- a/resources/js/Pages/Vendor/Index.tsx +++ b/resources/js/Pages/Vendor/Index.tsx @@ -34,6 +34,8 @@ interface PageProps { data: Vendor[]; links: any[]; meta: any; + total: number; + per_page: number; }; filters: { search?: string; @@ -47,7 +49,7 @@ export default function VendorManagement({ vendors, filters }: PageProps) { const [searchTerm, setSearchTerm] = useState(filters.search || ""); const [sortField, setSortField] = useState(filters.sort_field || null); const [sortDirection, setSortDirection] = useState<"asc" | "desc" | null>(filters.sort_direction as "asc" | "desc" || null); - const [perPage, setPerPage] = useState(filters.per_page || "10"); + const [perPage, setPerPage] = useState(filters.per_page || vendors.per_page?.toString() || "10"); const [isDialogOpen, setIsDialogOpen] = useState(false); const [editingVendor, setEditingVendor] = useState(null); @@ -56,7 +58,7 @@ export default function VendorManagement({ vendors, filters }: PageProps) { setSearchTerm(filters.search || ""); setSortField(filters.sort_field || null); setSortDirection(filters.sort_direction as "asc" | "desc" || null); - setPerPage(filters.per_page || "10"); + setPerPage(filters.per_page || vendors.per_page?.toString() || "10"); }, [filters]); // Debounced Search @@ -202,22 +204,25 @@ export default function VendorManagement({ vendors, filters }: PageProps) { /> {/* 分頁元件 - 統一樣式 */} -
-
- 每頁顯示 - - +
+
+
+ 每頁顯示 + + +
+ 共 {vendors.total} 筆資料
diff --git a/resources/js/Pages/Warehouse/Index.tsx b/resources/js/Pages/Warehouse/Index.tsx index 5e2c37d..148f1e6 100644 --- a/resources/js/Pages/Warehouse/Index.tsx +++ b/resources/js/Pages/Warehouse/Index.tsx @@ -300,7 +300,7 @@ export default function WarehouseIndex({ warehouses, totals, transitWarehouses, />
- 共 {warehouses.total} 筆紀錄 + 共 {warehouses.total} 筆資料