input('tab', 'orders'); $isAjax = $request->ajax(); // 取得篩選參數 $search = $request->input('search'); $machineId = $request->input('machine_id'); $paymentType = $request->input('payment_type'); $status = $request->input('status'); $startDate = $request->input('start_date'); $endDate = $request->input('end_date'); // 判斷是否為隱含篩選 (未選日期時預設抓6個月) $isImplicitFilter = false; if (!$startDate || !$endDate) { $isImplicitFilter = true; $startDate = now()->subMonths(6)->format('Y-m-d H:i'); $endDate = now()->format('Y-m-d H:i'); } $data = [ 'title' => '銷售&金流紀錄', 'description' => '銷售交易與金流明細查詢', 'tab' => $tab, 'filters' => [ 'search' => $search, 'machine_id' => $machineId, 'payment_type' => $paymentType, 'status' => $status, 'start_date' => $request->input('start_date'), // 保留原始輸入以顯示於 UI (如果是隱含的則 UI 顯示空) 'end_date' => $request->input('end_date'), ], 'isImplicitFilter' => $isImplicitFilter, 'machines' => Machine::select('id', 'name', 'serial_no')->get(), 'paymentTypes' => Order::getPaymentTypeLabels(), ]; // 1. 建立基本查詢 (套用共用過濾器:機台、日期) $ordersQuery = Order::with([ 'machine:id,name,serial_no', 'invoice:id,order_id,invoice_no', 'items', 'staffCardLog:id,order_id,staff_card_id', 'staffCardLog.staffCard:id,employee_id,name,card_uid' ]); $invoicesQuery = Invoice::with(['machine:id,name,serial_no', 'order:id,order_no,flow_id,payment_type']); $dispenseQuery = DispenseRecord::with(['order:id,order_no,flow_id', 'machine:id,name,serial_no', 'product:id,name']); // 共用過濾器:日期 $start = Carbon::parse($startDate); $end = Carbon::parse($endDate)->endOfMinute(); $ordersQuery->whereBetween('created_at', [$start, $end]); $invoicesQuery->whereBetween('invoice_date', [$start, $end]); $dispenseQuery->whereBetween('machine_time', [$start, $end]); // 共用過濾器:機台 if ($machineId) { $ordersQuery->where('machine_id', $machineId); $invoicesQuery->where('machine_id', $machineId); $dispenseQuery->where('machine_id', $machineId); } // 2. 應用獨立過濾器:搜尋 (僅對當前 Tab 應用) if ($search) { if ($tab === 'orders') { $ordersQuery->where(function($q) use ($search) { $q->where('order_no', 'like', "%{$search}%") ->orWhere('flow_id', 'like', "%{$search}%") ->orWhere('invoice_info', 'like', "%{$search}%") ->orWhere('member_barcode', 'like', "%{$search}%") ->orWhereHas('staffCardLog.staffCard', function($sq) use ($search) { $sq->where('name', 'like', "%{$search}%") ->orWhere('employee_id', 'like', "%{$search}%") ->orWhere('card_uid', 'like', "%{$search}%"); }); }); } elseif ($tab === 'invoices') { $invoicesQuery->where(function($q) use ($search) { $q->where('invoice_no', 'like', "%{$search}%") ->orWhere('flow_id', 'like', "%{$search}%"); }); } elseif ($tab === 'dispense') { $dispenseQuery->where(function($q) use ($search) { $q->where('slot_no', 'like', "%{$search}%") ->orWhereHas('product', function($pq) use ($search) { $pq->where('name', 'like', "%{$search}%"); }) ->orWhereHas('order', function($oq) use ($search) { $oq->where('order_no', 'like', "%{$search}%"); }); }); } } // 訂單專用過濾器 if ($tab === 'orders') { if ($paymentType) $ordersQuery->where('payment_type', $paymentType); 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(); $data['invoices'] = $invoicesQuery->latest()->paginate($perPage, ['*'], 'invoices_page')->withQueryString(); $data['dispenseLogs'] = $dispenseQuery->latest()->paginate($perPage, ['*'], 'dispense_page')->withQueryString(); if ($isAjax) { return response()->json([ 'success' => true, 'tab' => $tab, 'html' => view('admin.sales.partials.tab-' . $tab, $data)->render() ]); } return view('admin.sales.index', $data); } /** * 取得單筆交易詳情 (用於 Slide-over) */ public function show(Order $order) { $order->load([ 'machine', 'invoice', 'items', 'dispenseRecords.product', 'staffCardLog:id,order_id,staff_card_id', 'staffCardLog.staffCard:id,employee_id,name,card_uid' ]); return response()->json([ 'success' => true, 'html' => view('admin.sales.partials.order-detail-panel', [ 'order' => $order, 'paymentTypes' => Order::getPaymentTypeLabels() ])->render() ]); } // 取貨碼設定 public function pickupCodes(Request $request) { $tab = $request->input('tab', 'list'); $isAjax = $request->ajax(); $data = [ 'title' => '取貨碼設定', 'description' => '產生與管理商品取貨驗證碼', 'tab' => $tab, ]; // 1. 取貨碼列表 (list) if (!$isAjax || $tab === 'list') { $query = PickupCode::with(['machine.slots.product', 'creator', 'order'])->latest(); if ($request->search) { $query->where(function ($q) use ($request) { $q->where('code', 'like', "%{$request->search}%") ->orWhereHas('machine', function ($mq) use ($request) { $mq->where('name', 'like', "%{$request->search}%") ->orWhere('serial_no', 'like', "%{$request->search}%"); }); }); } if ($request->status && trim($request->status) !== '') { $query->where('status', trim($request->status)); } $per_page = $request->input('per_page', 10); $data['pickupCodes'] = $query->paginate($per_page, ['*'], 'list_page')->withQueryString(); // 供新增彈窗使用的機台清單 $data['machines'] = Machine::all(); } // 2. 操作紀錄 (logs) if (!$isAjax || $tab === 'logs') { $logQuery = PickupCodeLog::with(['user:id,name', 'machine:id,name,serial_no', 'pickupCode', 'order']); if ($request->filled('search_log')) { $search = $request->input('search_log'); $logQuery->where(function ($q) use ($search) { $q->where('remark', 'like', "%{$search}%") ->orWhereHas('pickupCode', function($pq) use ($search) { $pq->where('code', 'like', "%{$search}%"); }); }); } // 新增:類型篩選 if ($request->filled('action')) { $logQuery->where('action', $request->action); } // 新增:日期區間篩選 (隱含 6 個月限制) $startDate = $request->input('start_date'); $endDate = $request->input('end_date'); if (!$startDate && !$endDate) { $logQuery->where('created_at', '>=', now()->subMonths(6)); } else { try { if ($startDate) { $logQuery->where('created_at', '>=', \Carbon\Carbon::parse($startDate)->startOfMinute()); } if ($endDate) { $logQuery->where('created_at', '<=', \Carbon\Carbon::parse($endDate)->endOfMinute()); } } catch (\Exception $e) { } } $data['logs'] = $logQuery->latest() ->paginate($request->input('per_page', 10), ['*'], 'log_page') ->withQueryString(); // 定義可用動作 $data['actions'] = [ 'create' => __('create'), 'update' => __('update'), 'cancel' => __('cancel'), 'used' => __('used'), 'consume' => __('consume'), 'consume_failed' => __('consume_failed'), 'verify_success' => __('verify_success'), ]; } if ($isAjax) { return response()->json([ 'success' => true, 'tab' => $tab, 'html' => view('admin.sales.pickup-codes.partials.tab-' . $tab, $data)->render() ]); } return view('admin.sales.pickup-codes.index', $data); } /** * 產生取貨碼 */ public function storePickupCode(Request $request) { $validated = $request->validate([ 'machine_id' => 'required|exists:machines,id', 'slot_no' => 'required|string', 'usage_limit' => 'nullable|integer|min:1|max:20', 'expires_hours' => 'nullable|integer|min:1|max:720', 'expires_at' => 'nullable|date|after:now', 'custom_code' => 'nullable|string|min:4|max:12', ]); $machine = Machine::findOrFail($validated['machine_id']); // 處理過期時間:優先使用直接傳入的日期,否則使用時數計算 $expiresAt = $request->filled('expires_at') ? Carbon::parse($validated['expires_at']) : now()->addHours((int) ($request->expires_hours ?? 24)); $pickupCode = PickupCode::create([ 'machine_id' => $validated['machine_id'], 'slot_no' => $validated['slot_no'], 'code' => $request->custom_code ?? PickupCode::generateUniqueCode($validated['machine_id']), 'usage_limit' => $validated['usage_limit'] ?? 1, 'usage_count' => 0, 'expires_at' => $expiresAt, 'status' => 'active', 'company_id' => $machine->company_id, 'created_by' => Auth::id(), ]); SystemOperationLog::create([ 'company_id' => $machine->company_id, 'user_id' => Auth::id(), 'module' => 'pickup_code', 'action' => 'create', 'target_id' => $pickupCode->id, 'target_type' => PickupCode::class, 'new_values' => $pickupCode->toArray(), ]); return back()->with('success', __('Pickup code generated: :code', ['code' => $pickupCode->code])); } /** * 更新取貨碼 (僅限修改時間) */ public function updatePickupCode(Request $request, PickupCode $pickupCode) { $validated = $request->validate([ 'expires_at' => 'nullable|date|after:now', 'usage_limit' => 'nullable|integer|min:1|max:20', ]); $oldValues = $pickupCode->toArray(); $pickupCode->update(array_filter([ 'expires_at' => $validated['expires_at'] ?? null, 'usage_limit' => $validated['usage_limit'] ?? null, ])); SystemOperationLog::create([ 'company_id' => $pickupCode->company_id, 'user_id' => Auth::id(), 'module' => 'pickup_code', 'action' => 'update', 'target_id' => $pickupCode->id, 'target_type' => PickupCode::class, 'old_values' => $oldValues, 'new_values' => $pickupCode->toArray(), ]); return back()->with('success', __('Pickup code updated.')); } /** * 刪除/取消取貨碼 */ public function destroyPickupCode(PickupCode $pickupCode) { $oldValues = $pickupCode->toArray(); $pickupCode->update(['status' => 'cancelled']); SystemOperationLog::create([ 'company_id' => $pickupCode->company_id, 'user_id' => Auth::id(), 'module' => 'pickup_code', 'action' => 'cancel', 'target_id' => $pickupCode->id, 'target_type' => PickupCode::class, 'old_values' => $oldValues, 'new_values' => $pickupCode->toArray(), ]); return back()->with('success', __('Pickup code cancelled.')); } // 購買單 public function orders() { return view('admin.placeholder', [ 'title' => '購買單', 'description' => '購買訂單管理', ]); } // 促銷時段設定 public function promotions() { return view('admin.placeholder', [ 'title' => '促銷時段設定', 'description' => '促銷活動時間設定', ]); } // 通行碼設定 public function passCodes(Request $request) { $tab = $request->input('tab', 'list'); $isAjax = $request->ajax(); $data = [ 'title' => '通行碼設定', 'description' => '特殊通行權限碼管理 (測試/補貨用)', 'tab' => $tab, ]; // 1. 通行碼列表 (list) if (!$isAjax || $tab === 'list') { $query = PassCode::with(['machine', 'creator'])->latest(); if ($request->search) { $query->where(function ($q) use ($request) { $q->where('code', 'like', "%{$request->search}%") ->orWhere('name', 'like', "%{$request->search}%") ->orWhereHas('machine', function ($mq) use ($request) { $mq->where('name', 'like', "%{$request->search}%") ->orWhere('serial_no', 'like', "%{$request->search}%"); }); }); } if ($request->status && trim($request->status) !== '') { $status = trim($request->status); if ($status === 'active') { $query->where('status', 'active') ->where(function ($q) { $q->whereNull('expires_at') ->orWhere('expires_at', '>', now()); }); } elseif ($status === 'expired') { $query->where('status', 'active') ->whereNotNull('expires_at') ->where('expires_at', '<=', now()); } else { $query->where('status', $status); } } $data['passCodes'] = $query->paginate($request->input('per_page', 10), ['*'], 'list_page')->withQueryString(); $data['machines'] = Machine::all(); } // 2. 操作紀錄 (logs) if (!$isAjax || $tab === 'logs') { $logQuery = PassCodeLog::with(['user:id,name', 'machine:id,name,serial_no', 'passCode']); if ($request->filled('search_log')) { $search = $request->input('search_log'); $logQuery->where(function ($q) use ($search) { $q->where('remark', 'like', "%{$search}%") ->orWhereHas('passCode', function($pq) use ($search) { $pq->where('code', 'like', "%{$search}%") ->orWhere('name', 'like', "%{$search}%"); }); }); } // 新增:類型篩選 if ($request->filled('action')) { $logQuery->where('action', $request->action); } // 新增:日期區間篩選 (隱含 6 個月限制) $startDate = $request->input('start_date'); $endDate = $request->input('end_date'); if (!$startDate && !$endDate) { $logQuery->where('created_at', '>=', now()->subMonths(6)); } else { try { if ($startDate) { $logQuery->where('created_at', '>=', \Carbon\Carbon::parse($startDate)->startOfMinute()); } if ($endDate) { $logQuery->where('created_at', '<=', \Carbon\Carbon::parse($endDate)->endOfMinute()); } } catch (\Exception $e) { } } $data['logs'] = $logQuery->latest() ->paginate($request->input('per_page', 10), ['*'], 'log_page') ->withQueryString(); // 定義可用動作 $data['actions'] = [ 'create' => __('create'), 'update' => __('update'), 'cancel' => __('cancel'), 'used' => __('used'), 'consume' => __('consume'), 'verify_success' => __('verify_success'), ]; } if ($isAjax) { return response()->json([ 'success' => true, 'tab' => $tab, 'html' => view('admin.sales.pass-codes.partials.tab-' . $tab, $data)->render() ]); } return view('admin.sales.pass-codes.index', $data); } /** * 產生通行碼 */ public function storePassCode(Request $request) { $validated = $request->validate([ 'machine_id' => 'required|exists:machines,id', 'name' => 'required|string|max:50', 'expires_days' => 'nullable|integer|min:0', 'custom_code' => 'required|string|min:4|max:12', ]); $machine = Machine::findOrFail($validated['machine_id']); $expiresAt = $request->expires_days ? now()->addDays((int) $request->expires_days) : null; $passCode = PassCode::create([ 'machine_id' => $validated['machine_id'], 'name' => $validated['name'] ?? 'Manual Generate', 'code' => $validated['custom_code'] ?? PassCode::generateUniqueCode($validated['machine_id']), 'expires_at' => $expiresAt, 'status' => 'active', 'company_id' => $machine->company_id, 'created_by' => Auth::id(), ]); SystemOperationLog::create([ 'company_id' => $machine->company_id, 'user_id' => Auth::id(), 'module' => 'pass_code', 'action' => 'create', 'target_id' => $passCode->id, 'target_type' => PassCode::class, 'new_values' => $passCode->toArray(), ]); return back()->with('success', __('Pass code created: :code', ['code' => $passCode->code])); } /** * 更新通行碼 */ public function updatePassCode(Request $request, PassCode $passCode) { $validated = $request->validate([ 'name' => 'nullable|string|max:50', 'expires_at' => 'nullable|date', 'status' => 'nullable|in:active,disabled', ]); // 確保 expires_at 為空字串時轉為 null if (isset($validated['expires_at']) && empty($validated['expires_at'])) { $validated['expires_at'] = null; } $oldValues = $passCode->toArray(); $passCode->update(array_filter($validated, function ($value, $key) use ($request) { return $request->has($key); }, ARRAY_FILTER_USE_BOTH)); SystemOperationLog::create([ 'company_id' => $passCode->company_id, 'user_id' => Auth::id(), 'module' => 'pass_code', 'action' => 'update', 'target_id' => $passCode->id, 'target_type' => PassCode::class, 'old_values' => $oldValues, 'new_values' => $passCode->toArray(), ]); return back()->with('success', __('Pass code updated.')); } /** * 刪除通行碼 (改為停用) */ public function destroyPassCode(PassCode $passCode) { $oldValues = $passCode->toArray(); $passCode->update(['status' => 'disabled']); SystemOperationLog::create([ 'company_id' => $passCode->company_id, 'user_id' => Auth::id(), 'module' => 'pass_code', 'action' => 'cancel', 'target_id' => $passCode->id, 'target_type' => PassCode::class, 'old_values' => $oldValues, 'new_values' => $passCode->toArray(), ]); return back()->with('success', __('Pass code cancelled.')); } // 來店禮設定 public function storeGifts() { return view('admin.placeholder', [ 'title' => '來店禮設定', '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, ''); } 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, ''); } fwrite($file, ''); } else { fputcsv($file, $row); } } }); if ($isExcel) { fwrite($file, '
' . htmlspecialchars($header) . '
' . htmlspecialchars($val) . '
'); } 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, ''); } 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, ''); } fwrite($file, ''); } else { fputcsv($file, $row); } } }); if ($isExcel) { fwrite($file, '
' . htmlspecialchars($header) . '
' . htmlspecialchars($val) . '
'); } 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, ''); } 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->localized_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, ''); } fwrite($file, ''); } else { fputcsv($file, $row); } } }); if ($isExcel) { fwrite($file, '
' . htmlspecialchars($header) . '
' . htmlspecialchars($val) . '
'); } 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(); } }