diff --git a/app/Http/Controllers/Admin/ProductController.php b/app/Http/Controllers/Admin/ProductController.php index 296354b..945ac3a 100644 --- a/app/Http/Controllers/Admin/ProductController.php +++ b/app/Http/Controllers/Admin/ProductController.php @@ -584,6 +584,136 @@ class ProductController extends Controller } } + /** + * 匯出商品資料(CSV / Excel),套用與 index 相同的篩選(搜尋/分類/公司)。 + * 參考 SalesController::handleExport 的串流下載模式;欄位順序對齊匯入範本,方便編輯後回匯入。 + */ + public function export(Request $request) + { + $user = auth()->user(); + $type = $request->input('export', 'csv'); + $isExcel = ($type === 'excel'); + $ext = $isExcel ? 'xls' : 'csv'; + $contentType = $isExcel ? 'application/vnd.ms-excel; charset=utf-8' : 'text/csv; charset=utf-8'; + + // 與 index 相同的商品查詢與篩選(租戶隔離沿用 Product 全域 scope) + $query = Product::with(['category.translations', 'translations', 'company']); + + if ($request->filled('search')) { + $search = $request->search; + $query->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('barcode', 'like', "%{$search}%") + ->orWhere('spec', 'like', "%{$search}%"); + }); + } + + if ($request->filled('category_id')) { + $query->where('category_id', $request->category_id); + } + + if ($user->isSystemAdmin() && $request->filled('product_company_id')) { + $query->where('company_id', $request->product_company_id); + } + + // 欄位順序對齊匯入範本(ProductImportService::downloadTemplate) + $headers = [ + '分類', + '商品名稱(zh_TW)', + '商品名稱(en)', + '商品名稱(ja)', + '條碼', + '規格', + '生產公司', + '售價', + '會員價', + '成本', + '履帶貨道上限', + '彈簧貨道上限', + '物料代碼', + '全額點數', + '半額點數', + '半額折抵金額', + '是否啟用', + '圖片檔名', + ]; + + $filename = '商品資料_' . now()->format('YmdHis') . '.' . $ext; + + $callback = function () use ($query, $headers, $isExcel) { + $file = fopen('php://output', 'w'); + if ($isExcel) { + fwrite($file, ''); + fwrite($file, ''); + fwrite($file, ''); + foreach ($headers as $header) { + fwrite($file, ''); + } + fwrite($file, ''); + } else { + fwrite($file, "\xEF\xBB\xBF"); // UTF-8 BOM,Excel 開啟不亂碼 + fputcsv($file, $headers); + } + + $locale = app()->getLocale(); + + $query->orderBy('id')->chunk(500, function ($products) use ($file, $isExcel, $locale) { + foreach ($products as $p) { + $meta = is_array($p->metadata) ? $p->metadata : []; + $nameEn = optional($p->translations->firstWhere('locale', 'en'))->value; + $nameJa = optional($p->translations->firstWhere('locale', 'ja'))->value; + + $categoryName = ''; + if ($p->category) { + $catTrans = $p->category->translations->firstWhere('locale', $locale) + ?? $p->category->translations->firstWhere('locale', 'zh_TW'); + $categoryName = $catTrans->value ?? ($p->category->name ?? ''); + } + + $imageFilename = $p->image_url ? basename($p->image_url) : ''; + + $row = [ + $categoryName, + $p->name, + $nameEn, + $nameJa, + $p->barcode, + $p->spec, + $p->manufacturer, + $p->price, + $p->member_price, + $p->cost, + $p->track_limit, + $p->spring_limit, + $meta['material_code'] ?? '', + $meta['points_full'] ?? 0, + $meta['points_half'] ?? 0, + $meta['points_half_amount'] ?? 0, + $p->is_active ? 1 : 0, + $imageFilename, + ]; + + if ($isExcel) { + fwrite($file, ''); + foreach ($row as $cell) { + fwrite($file, ''); + } + fwrite($file, ''); + } else { + fputcsv($file, $row); + } + } + }); + + if ($isExcel) { + fwrite($file, '
' . htmlspecialchars($header) . '
' . htmlspecialchars((string) $cell) . '
'); + } + fclose($file); + }; + + return response()->streamDownload($callback, $filename, ['Content-Type' => $contentType]); + } + public function downloadTemplate(ProductImportService $importService) { return $importService->downloadTemplate(); diff --git a/lang/en.json b/lang/en.json index 2d49c90..9c4bff1 100644 --- a/lang/en.json +++ b/lang/en.json @@ -777,6 +777,7 @@ "Expiry Time": "Expiry Time", "Expiry date tracking and warnings": "Expiry date tracking and warnings", "Export Report": "Export Report", + "Export": "Export", "Export to CSV": "Export to CSV", "Export to Excel": "Export to Excel", "External URL": "External URL", @@ -965,6 +966,8 @@ "Image Downloaded": "Image Downloaded", "Immediate": "Immediate", "Import Excel": "Import Excel", + "Export CSV": "Export CSV", + "Export Excel": "Export Excel", "Import Products": "Import Products", "Import Staff Cards": "Import Staff Cards", "Import failed": "Import failed", diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 806adcb..5512d47 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -778,6 +778,7 @@ "Expiry Time": "到期時間", "Expiry date tracking and warnings": "有效期限追蹤與預警", "Export Report": "匯出報表", + "Export": "匯出", "Export to CSV": "匯出成 CSV", "Export to Excel": "匯出成 Excel", "External URL": "外連 URL", @@ -966,6 +967,8 @@ "Image Downloaded": "圖片已下載", "Immediate": "立即", "Import Excel": "Excel 匯入", + "Export CSV": "匯出 CSV", + "Export Excel": "匯出 Excel", "Import Products": "匯入商品", "Import Staff Cards": "匯入員工識別卡", "Import failed": "匯入失敗", diff --git a/resources/views/admin/products/index.blade.php b/resources/views/admin/products/index.blade.php index 6473b0e..7aa8f93 100644 --- a/resources/views/admin/products/index.blade.php +++ b/resources/views/admin/products/index.blade.php @@ -36,13 +36,6 @@ hover:bg-slate-100 dark:hover:bg-white/5 rounded-lg flex items-center justify-be {{ __('Add Product') }}
- + +
+ + +
diff --git a/routes/web.php b/routes/web.php index 28e9aa7..ed4e10d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -213,6 +213,7 @@ Route::middleware(['auth', 'auth.session', 'verified', 'tenant.access'])->prefix Route::prefix('data-config')->name('data-config.')->group(function () { Route::middleware('can:menu.data-config.products')->group(function () { Route::get('/products/template', [App\Http\Controllers\Admin\ProductController::class, 'downloadTemplate'])->name('products.template'); + Route::get('/products/export', [App\Http\Controllers\Admin\ProductController::class, 'export'])->name('products.export'); Route::post('/products/import', [App\Http\Controllers\Admin\ProductController::class, 'import'])->name('products.import'); Route::resource('products', App\Http\Controllers\Admin\ProductController::class)->except(['show']); Route::patch('/products/{id}/toggle-status', [App\Http\Controllers\Admin\ProductController::class, 'toggleStatus'])->name('products.status.toggle');