diff --git a/app/Http/Controllers/Admin/SalesController.php b/app/Http/Controllers/Admin/SalesController.php
index 142d376..6e13561 100644
--- a/app/Http/Controllers/Admin/SalesController.php
+++ b/app/Http/Controllers/Admin/SalesController.php
@@ -125,6 +125,12 @@ class SalesController extends Controller
if ($status) $ordersQuery->where('status', $status);
}
+ // 匯出功能攔截
+ if ($request->filled('export')) {
+ $exportType = $request->input('export');
+ return $this->handleExport($tab, $exportType, $ordersQuery, $invoicesQuery, $dispenseQuery);
+ }
+
// 3. 執行分頁 (使用獨立的 pageName)
$perPage = $request->input('per_page', 10);
$data['orders'] = $ordersQuery->latest()->paginate($perPage, ['*'], 'orders_page')->withQueryString();
@@ -597,4 +603,272 @@ class SalesController extends Controller
'description' => '來店優惠活動設定',
]);
}
+
+ /**
+ * 處理銷售中心資料匯出 (CSV 串流下載)
+ */
+ private function handleExport($tab, $type, $ordersQuery, $invoicesQuery, $dispenseQuery)
+ {
+ $isExcel = ($type === 'excel');
+ $ext = $isExcel ? 'xls' : 'csv';
+ $contentType = $isExcel ? 'application/vnd.ms-excel; charset=utf-8' : 'text/csv; charset=utf-8';
+
+ $filename = '';
+ $headers = [];
+ $callback = null;
+
+ if ($tab === 'orders') {
+ $filename = '交易紀錄_' . now()->format('YmdHis') . '.' . $ext;
+ $headers = [
+ __('Order Number'),
+ __('Transaction Time'),
+ __('Machine Name'),
+ __('Machine Serial No'),
+ __('Product Items'),
+ __('Quantity'),
+ __('Original Amount'),
+ __('Discount Amount'),
+ __('Payment Amount'),
+ __('Payment Type'),
+ __('Status'),
+ __('Invoice Number'),
+ __('Staff Name'),
+ __('Staff ID'),
+ __('Card UID'),
+ ];
+
+ // 預先載入關聯以防 N+1
+ $ordersQuery->with([
+ 'machine:id,name,serial_no',
+ 'invoice:id,order_id,invoice_no',
+ 'items:id,order_id,product_name,quantity',
+ 'staffCardLog:id,order_id,staff_card_id',
+ 'staffCardLog.staffCard:id,employee_id,name,card_uid'
+ ]);
+
+ $callback = function () use ($ordersQuery, $headers, $isExcel) {
+ $file = fopen('php://output', 'w');
+ if ($isExcel) {
+ fwrite($file, '');
+ fwrite($file, '
');
+ fwrite($file, '');
+ foreach ($headers as $header) {
+ fwrite($file, '| ' . htmlspecialchars($header) . ' | ');
+ }
+ fwrite($file, '
');
+ } else {
+ fwrite($file, "\xEF\xBB\xBF"); // 注入 UTF-8 BOM
+ fputcsv($file, $headers);
+ }
+
+ // 分批 (Chunk) 查詢,每次 500 筆,確保常數記憶體用量
+ $ordersQuery->latest()->chunk(500, function ($orders) use ($file, $isExcel) {
+ foreach ($orders as $order) {
+ $productSummary = $order->items->map(function ($item) {
+ return $item->product_name . ' x' . number_format($item->quantity, 0);
+ })->implode(', ');
+
+ $totalQty = $order->items->sum('quantity');
+
+ $row = [
+ $order->order_no,
+ $order->created_at->format('Y-m-d H:i:s'),
+ $order->machine->name ?? 'Unknown',
+ $order->machine->serial_no ?? '---',
+ $productSummary ?: '---',
+ $totalQty,
+ number_format($order->total_amount, 2, '.', ''),
+ number_format($order->discount_amount, 2, '.', ''),
+ number_format($order->pay_amount, 2, '.', ''),
+ $order->payment_type_label,
+ __($order->status),
+ $order->invoice->invoice_no ?? '---',
+ $order->staffCardLog->staffCard->name ?? '---',
+ $order->staffCardLog->staffCard->employee_id ?? '---',
+ $order->staffCardLog->staffCard->card_uid ?? '---',
+ ];
+
+ if ($isExcel) {
+ fwrite($file, '');
+ foreach ($row as $val) {
+ fwrite($file, '| ' . htmlspecialchars($val) . ' | ');
+ }
+ fwrite($file, '
');
+ } else {
+ fputcsv($file, $row);
+ }
+ }
+ });
+
+ if ($isExcel) {
+ fwrite($file, '
');
+ }
+ fclose($file);
+ };
+ } elseif ($tab === 'invoices') {
+ $filename = '電子發票紀錄_' . now()->format('YmdHis') . '.' . $ext;
+ $headers = [
+ __('Invoice Number'),
+ __('Invoice Date'),
+ __('Flow ID'),
+ __('Machine Name'),
+ __('Machine Serial No'),
+ __('Associated Order'),
+ __('Amount'),
+ __('Status'),
+ ];
+
+ // 預先載入關聯
+ $invoicesQuery->with([
+ 'machine:id,name,serial_no',
+ 'order:id,order_no'
+ ]);
+
+ $callback = function () use ($invoicesQuery, $headers, $isExcel) {
+ $file = fopen('php://output', 'w');
+ if ($isExcel) {
+ fwrite($file, '');
+ fwrite($file, '');
+ fwrite($file, '');
+ foreach ($headers as $header) {
+ fwrite($file, '| ' . htmlspecialchars($header) . ' | ');
+ }
+ fwrite($file, '
');
+ } else {
+ fwrite($file, "\xEF\xBB\xBF");
+ fputcsv($file, $headers);
+ }
+
+ $invoicesQuery->latest()->chunk(500, function ($invoices) use ($file, $isExcel) {
+ foreach ($invoices as $inv) {
+ // 狀態判斷邏輯需與 tab-invoices.blade.php 完全一致
+ $status = 'valid';
+ if (empty($inv->invoice_no)) $status = 'failed';
+ if ($inv->rtn_code === 'void') $status = 'void';
+
+ $statusMap = [
+ 'valid' => __('Valid'),
+ 'void' => __('Void'),
+ 'failed' => __('Failed'),
+ 'refunded' => __('Refunded'),
+ ];
+ $statusLabel = $statusMap[$status] ?? $status;
+
+ $row = [
+ $inv->invoice_no ?: '---',
+ $inv->invoice_date ?: '---',
+ $inv->flow_id ?: '---',
+ $inv->machine->name ?? ($inv->order->machine->name ?? 'Unknown'),
+ $inv->machine->serial_no ?? ($inv->order->machine->serial_no ?? '---'),
+ $inv->order->order_no ?? '---',
+ number_format($inv->amount, 2, '.', ''),
+ $statusLabel,
+ ];
+
+ if ($isExcel) {
+ fwrite($file, '');
+ foreach ($row as $val) {
+ fwrite($file, '| ' . htmlspecialchars($val) . ' | ');
+ }
+ fwrite($file, '
');
+ } else {
+ fputcsv($file, $row);
+ }
+ }
+ });
+
+ if ($isExcel) {
+ fwrite($file, '
');
+ }
+ fclose($file);
+ };
+ } elseif ($tab === 'dispense') {
+ $filename = '遠端出貨紀錄_' . now()->format('YmdHis') . '.' . $ext;
+ $headers = [
+ __('Machine'),
+ __('Machine Serial No'),
+ __('Product Name'),
+ __('Slot No'),
+ __('Amount'),
+ __('Dispense Status'),
+ __('Dispense Time'),
+ __('Associated Order'),
+ ];
+
+ // 預先載入關聯
+ $dispenseQuery->with([
+ 'machine:id,name,serial_no',
+ 'product:id,name',
+ 'order:id,order_no'
+ ]);
+
+ $callback = function () use ($dispenseQuery, $headers, $isExcel) {
+ $file = fopen('php://output', 'w');
+ if ($isExcel) {
+ fwrite($file, '');
+ fwrite($file, '');
+ fwrite($file, '');
+ foreach ($headers as $header) {
+ fwrite($file, '| ' . htmlspecialchars($header) . ' | ');
+ }
+ fwrite($file, '
');
+ } else {
+ fwrite($file, "\xEF\xBB\xBF");
+ fputcsv($file, $headers);
+ }
+
+ $dispenseQuery->latest()->chunk(500, function ($logs) use ($file, $isExcel) {
+ foreach ($logs as $log) {
+ // 出貨狀態對照,需與 tab-dispense.blade.php 完全一致
+ $statusMap = [
+ 'success' => __('Dispense Success'),
+ 'failed' => __('Dispense Failed'),
+ 'pending' => __('Dispense Pending'),
+ '1' => __('Dispense Success'),
+ '0' => __('Dispense Failed'),
+ ];
+ $statusKey = (string)($log->dispense_status ?? 'pending');
+ $statusLabel = $statusMap[$statusKey] ?? $statusKey;
+
+ $row = [
+ $log->machine->name ?? 'Unknown',
+ $log->machine->serial_no ?? '---',
+ $log->product->name ?? 'Unknown Product',
+ $log->slot_no,
+ number_format($log->amount, 2, '.', ''),
+ $statusLabel,
+ $log->machine_time ? $log->machine_time->format('Y-m-d H:i:s') : '---',
+ $log->order->order_no ?? '---',
+ ];
+
+ if ($isExcel) {
+ fwrite($file, '');
+ foreach ($row as $val) {
+ fwrite($file, '| ' . htmlspecialchars($val) . ' | ');
+ }
+ fwrite($file, '
');
+ } else {
+ fputcsv($file, $row);
+ }
+ }
+ });
+
+ if ($isExcel) {
+ fwrite($file, '
');
+ }
+ fclose($file);
+ };
+ }
+
+ if ($callback) {
+ return response()->streamDownload($callback, $filename, [
+ 'Content-Type' => $contentType,
+ 'Cache-Control' => 'no-cache, must-revalidate',
+ 'Pragma' => 'no-cache',
+ 'Expires' => '0',
+ ]);
+ }
+
+ return redirect()->back();
+ }
}
diff --git a/lang/zh_TW.json b/lang/zh_TW.json
index 37bd298..33a8035 100644
--- a/lang/zh_TW.json
+++ b/lang/zh_TW.json
@@ -2085,5 +2085,9 @@
"Total Net Profit": "總淨利潤",
"Gross Margin": "毛利率",
"No product data matching search criteria": "沒有符合搜尋條件的商品資料",
- "Analyze Solely": "單獨分析"
+ "Analyze Solely": "單獨分析",
+ "Order Number": "訂單編號",
+ "Product Items": "商品品項",
+ "Original Amount": "應付金額",
+ "Discount Amount": "折扣金額"
}
\ No newline at end of file
diff --git a/resources/views/admin/data-config/staff-cards/index.blade.php b/resources/views/admin/data-config/staff-cards/index.blade.php
index 77ac1ee..9f858c0 100644
--- a/resources/views/admin/data-config/staff-cards/index.blade.php
+++ b/resources/views/admin/data-config/staff-cards/index.blade.php
@@ -140,7 +140,8 @@
{{ __('Please use our standard template to ensure data compatibility.') }}
- { const bar = document.getElementById('top-loading-bar'); if(bar) bar.classList.remove('loading'); }, 1000);"
class="inline-flex items-center gap-2 text-sm font-bold text-cyan-600 hover:text-cyan-700 transition-colors group">
{{ __('Download Template') }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/views/admin/sales/partials/tab-invoices.blade.php b/resources/views/admin/sales/partials/tab-invoices.blade.php
index a208b03..153ae88 100644
--- a/resources/views/admin/sales/partials/tab-invoices.blade.php
+++ b/resources/views/admin/sales/partials/tab-invoices.blade.php
@@ -101,6 +101,37 @@
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/views/admin/sales/partials/tab-orders.blade.php b/resources/views/admin/sales/partials/tab-orders.blade.php
index 15e72ca..28ec407 100644
--- a/resources/views/admin/sales/partials/tab-orders.blade.php
+++ b/resources/views/admin/sales/partials/tab-orders.blade.php
@@ -101,6 +101,37 @@
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
+
+
+
+
+
+
+
+
+
+
+
+