star-cloud/app/Http/Controllers/Admin/SalesController.php
sky121113 023f639fb4 [STYLE] 銷售中心 UI 優化與出貨紀錄邏輯校正
1. 優化「出貨紀錄」介面,重新排列欄位優先級並移除冗餘的機台流水號。
2. 統一出貨紀錄中的商品與貨道樣式,與取貨碼模組視覺語言同步。
3. 修正出貨紀錄搜尋邏輯,支援商品名稱與貨道編號模糊搜尋。
4. 修正「訂單詳情」側滑面板中的金額顯示(修正 price/subtotal 映射)。
5. 補強訂單詳情的出貨狀態 (dispense_status) 標籤顯示邏輯。
6. 將出貨紀錄中的「數量」表頭修正為「金額」,確保與 MQTT 協議回傳資料語意一致。
7. 更新多語系檔,新增搜尋欄位 placeholder 與相關 UI 標籤翻譯。
8. 新增發票資料表 machine_time 欄位遷移檔,以完整記錄機台開立時間。
2026-05-04 17:20:43 +08:00

562 lines
20 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\PassCode;
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');
$data = [
'title' => '銷售&金流紀錄',
'description' => '銷售交易與金流明細查詢',
'tab' => $tab,
'filters' => [
'search' => $search,
'machine_id' => $machineId,
'payment_type' => $paymentType,
'status' => $status,
'start_date' => $startDate,
'end_date' => $endDate,
],
'machines' => Machine::select('id', 'name', 'serial_no')->get(),
'paymentTypes' => [
1 => '信用卡',
2 => '電子票證',
3 => '掃碼支付',
4 => '紙鈔機',
9 => '零錢',
30 => 'LINE Pay',
31 => '街口支付',
32 => '悠遊付',
33 => 'Pi 拍錢包',
34 => '全盈+PAY',
60 => '點數/優惠券',
],
];
// 1. 建立基本查詢 (套用共用過濾器:機台、日期)
$ordersQuery = Order::with(['machine:id,name,serial_no', 'invoice:id,order_id,invoice_no', 'items']);
$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']);
// 共用過濾器:日期
if ($startDate && $endDate) {
$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}%");
});
} 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);
}
// 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']);
return response()->json([
'success' => true,
'html' => view('admin.sales.partials.order-detail-panel', [
'order' => $order,
'paymentTypes' => [
1 => '信用卡', 2 => '電子票證', 3 => '掃碼支付', 4 => '紙鈔機', 9 => '零錢',
30 => 'LINE Pay', 31 => '街口支付', 32 => '悠遊付', 33 => 'Pi 拍錢包', 34 => '全盈+PAY',
60 => '點數/優惠券',
]
])->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'])->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 = SystemOperationLog::with('user:id,name')
->where('module', 'pickup_code');
if ($request->filled('search_log')) {
$search = $request->input('search_log');
$logQuery->where(function ($q) use ($search) {
$q->where('target_id', 'like', "%{$search}%")
->orWhere('new_values', 'like', "%{$search}%")
->orWhere('old_values', 'like', "%{$search}%");
});
}
// 新增:類型篩選
if ($request->filled('action')) {
$logQuery->where('action', $request->action);
}
// 新增:日期區間篩選
if ($request->filled('start_date') && $request->filled('end_date')) {
try {
$start = \Carbon\Carbon::parse($request->start_date)->startOfMinute();
$end = \Carbon\Carbon::parse($request->end_date)->endOfMinute();
$logQuery->whereBetween('created_at', [$start, $end]);
} 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',
'expires_hours' => 'nullable|integer|min:1|max:720', // 最長一個月
'custom_code' => 'nullable|string|min:4|max:12',
]);
$machine = Machine::findOrFail($validated['machine_id']);
$expiresAt = 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']),
'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' => 'required|date|after:now',
]);
$oldValues = $pickupCode->toArray();
$pickupCode->update([
'expires_at' => $validated['expires_at'],
]);
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 = SystemOperationLog::with('user:id,name')
->where('module', 'pass_code');
if ($request->filled('search_log')) {
$search = $request->input('search_log');
$logQuery->where(function ($q) use ($search) {
$q->where('target_id', 'like', "%{$search}%")
->orWhere('new_values', 'like', "%{$search}%")
->orWhere('old_values', 'like', "%{$search}%");
});
}
// 新增:類型篩選
if ($request->filled('action')) {
$logQuery->where('action', $request->action);
}
// 新增:日期區間篩選
if ($request->filled('start_date') && $request->filled('end_date')) {
try {
$start = \Carbon\Carbon::parse($request->start_date)->startOfMinute();
$end = \Carbon\Carbon::parse($request->end_date)->endOfMinute();
$logQuery->whereBetween('created_at', [$start, $end]);
} 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' => '來店優惠活動設定',
]);
}
}