875 lines
34 KiB
PHP
875 lines
34 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use Illuminate\Http\Request;
|
|
use App\Models\Transaction\Order;
|
|
use App\Models\Transaction\Invoice;
|
|
use App\Models\Transaction\DispenseRecord;
|
|
use App\Models\Transaction\PaymentType;
|
|
use App\Models\Transaction\OrderItem;
|
|
use App\Models\Transaction\PickupCode;
|
|
use App\Models\Transaction\PickupCodeLog;
|
|
use App\Models\Transaction\PassCode;
|
|
use App\Models\Transaction\PassCodeLog;
|
|
use App\Models\Machine\Machine;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
use App\Models\System\SystemOperationLog;
|
|
|
|
class SalesController extends Controller
|
|
{
|
|
// 銷售中心 (銷售&金流紀錄)
|
|
public function index(Request $request)
|
|
{
|
|
$tab = $request->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, '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40">');
|
|
fwrite($file, '<head><meta charset="utf-8"/><style>table { border-collapse: collapse; } th, td { border: 1px solid #cbd5e1; padding: 8px 12px; text-align: left; } th { background-color: #f1f5f9; font-weight: bold; }</style></head><body>');
|
|
fwrite($file, '<table><thead><tr>');
|
|
foreach ($headers as $header) {
|
|
fwrite($file, '<th>' . htmlspecialchars($header) . '</th>');
|
|
}
|
|
fwrite($file, '</tr></thead><tbody>');
|
|
} 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, '<tr>');
|
|
foreach ($row as $val) {
|
|
fwrite($file, '<td>' . htmlspecialchars($val) . '</td>');
|
|
}
|
|
fwrite($file, '</tr>');
|
|
} else {
|
|
fputcsv($file, $row);
|
|
}
|
|
}
|
|
});
|
|
|
|
if ($isExcel) {
|
|
fwrite($file, '</tbody></table></body></html>');
|
|
}
|
|
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, '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40">');
|
|
fwrite($file, '<head><meta charset="utf-8"/><style>table { border-collapse: collapse; } th, td { border: 1px solid #cbd5e1; padding: 8px 12px; text-align: left; } th { background-color: #f1f5f9; font-weight: bold; }</style></head><body>');
|
|
fwrite($file, '<table><thead><tr>');
|
|
foreach ($headers as $header) {
|
|
fwrite($file, '<th>' . htmlspecialchars($header) . '</th>');
|
|
}
|
|
fwrite($file, '</tr></thead><tbody>');
|
|
} 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, '<tr>');
|
|
foreach ($row as $val) {
|
|
fwrite($file, '<td>' . htmlspecialchars($val) . '</td>');
|
|
}
|
|
fwrite($file, '</tr>');
|
|
} else {
|
|
fputcsv($file, $row);
|
|
}
|
|
}
|
|
});
|
|
|
|
if ($isExcel) {
|
|
fwrite($file, '</tbody></table></body></html>');
|
|
}
|
|
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, '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40">');
|
|
fwrite($file, '<head><meta charset="utf-8"/><style>table { border-collapse: collapse; } th, td { border: 1px solid #cbd5e1; padding: 8px 12px; text-align: left; } th { background-color: #f1f5f9; font-weight: bold; }</style></head><body>');
|
|
fwrite($file, '<table><thead><tr>');
|
|
foreach ($headers as $header) {
|
|
fwrite($file, '<th>' . htmlspecialchars($header) . '</th>');
|
|
}
|
|
fwrite($file, '</tr></thead><tbody>');
|
|
} 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, '<tr>');
|
|
foreach ($row as $val) {
|
|
fwrite($file, '<td>' . htmlspecialchars($val) . '</td>');
|
|
}
|
|
fwrite($file, '</tr>');
|
|
} else {
|
|
fputcsv($file, $row);
|
|
}
|
|
}
|
|
});
|
|
|
|
if ($isExcel) {
|
|
fwrite($file, '</tbody></table></body></html>');
|
|
}
|
|
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();
|
|
}
|
|
}
|