diff --git a/app/Http/Controllers/Admin/SalesController.php b/app/Http/Controllers/Admin/SalesController.php index 9337f97..7a9c1a2 100644 --- a/app/Http/Controllers/Admin/SalesController.php +++ b/app/Http/Controllers/Admin/SalesController.php @@ -66,8 +66,8 @@ class SalesController extends Controller // 1. 建立基本查詢 (套用共用過濾器:機台、日期) $ordersQuery = Order::with([ 'machine:id,name,serial_no', - 'invoice:id,order_id,invoice_no', - 'items', + 'invoice:id,order_id,invoice_no,status', + 'items', 'staffCardLog:id,order_id,staff_card_id', 'staffCardLog.staffCard:id,employee_id,name,card_uid' ]); @@ -1261,6 +1261,11 @@ class SalesController extends Controller /** 作廢(Invalid),用於「已開票未出貨」等情況。 */ public function voidInvoice(Request $request, Invoice $invoice, \App\Services\Invoice\EcpayInvoiceService $ecpay) { + // 權限守衛:發票作廢僅限「平台系統管理員」,租戶帳號(含其管理員)一律不得作廢。 + // 真正的閘門在後端,前端僅隱藏按鈕無法防止偽造 POST。 + if (!Auth::user()->isSystemAdmin()) { + abort(403, __('Only system administrators can void invoices')); + } // 伺服器端狀態守衛:僅 issued 可作廢(防把已作廢/未開立的發票再作廢) if ($invoice->status !== Invoice::STATUS_ISSUED) { return back()->with('error', __('Only issued invoices can be voided')); @@ -1268,7 +1273,9 @@ class SalesController extends Controller if (empty($invoice->invoice_no)) { return back()->with('error', __('No invoice issued, cannot void')); } - $reason = $request->input('reason', '未出貨作廢'); + // 送綠界的作廢原因維持固定值(法定欄位、上限 20 字);管理者於視窗輸入的備註僅存內部 remark。 + $reason = '未出貨作廢'; + $remark = trim((string) $request->input('remark', '')); $date = $invoice->invoice_date ? Carbon::parse($invoice->invoice_date)->format('Y-m-d') : ''; @@ -1283,12 +1290,41 @@ class SalesController extends Controller 'status' => Invoice::STATUS_VOID, 'voided_at' => now(), 'void_reason' => $reason, + 'remark' => $remark !== '' ? $remark : $invoice->remark, ]); return back()->with('success', __('Void success')); } return back()->with('error', __('Void failed: ') . ($result['RtnMsg'] ?? '')); } + /** 更新訂單備註(管理者內部註記)。租戶隔離由 TenantScoped + route-model binding 自動處理。 */ + public function updateOrderRemark(Request $request, Order $order) + { + $data = $request->validate([ + 'remark' => ['nullable', 'string', 'max:1000'], + ]); + $order->update(['remark' => $data['remark'] ?? null]); + + if ($request->expectsJson()) { + return response()->json(['success' => true, 'message' => __('Remark updated')]); + } + return back()->with('success', __('Remark updated')); + } + + /** 更新發票備註(管理者內部註記,與送綠界的 void_reason 無關)。 */ + public function updateInvoiceRemark(Request $request, Invoice $invoice) + { + $data = $request->validate([ + 'remark' => ['nullable', 'string', 'max:1000'], + ]); + $invoice->update(['remark' => $data['remark'] ?? null]); + + if ($request->expectsJson()) { + return response()->json(['success' => true, 'message' => __('Remark updated')]); + } + return back()->with('success', __('Remark updated')); + } + private function safeDate($raw): ?string { if (empty($raw)) { diff --git a/app/Models/Transaction/Invoice.php b/app/Models/Transaction/Invoice.php index e027e9a..2fb7991 100644 --- a/app/Models/Transaction/Invoice.php +++ b/app/Models/Transaction/Invoice.php @@ -30,6 +30,7 @@ class Invoice extends Model 'retry_count', 'voided_at', 'void_reason', + 'remark', 'random_number', 'love_code', 'rtn_code', diff --git a/app/Models/Transaction/Order.php b/app/Models/Transaction/Order.php index 5b1de78..2e98628 100644 --- a/app/Models/Transaction/Order.php +++ b/app/Models/Transaction/Order.php @@ -63,6 +63,7 @@ class Order extends Model 'status', 'metadata', 'delivery_status', + 'remark', ]; protected $casts = [ diff --git a/database/migrations/2026_06_18_120000_add_remark_to_orders_and_invoices_tables.php b/database/migrations/2026_06_18_120000_add_remark_to_orders_and_invoices_tables.php new file mode 100644 index 0000000..a94c199 --- /dev/null +++ b/database/migrations/2026_06_18_120000_add_remark_to_orders_and_invoices_tables.php @@ -0,0 +1,36 @@ +text('remark')->nullable()->after('metadata'); + }); + + Schema::table('invoices', function (Blueprint $table) { + $table->text('remark')->nullable()->after('void_reason'); + }); + } + + public function down(): void + { + Schema::table('orders', function (Blueprint $table) { + $table->dropColumn('remark'); + }); + + Schema::table('invoices', function (Blueprint $table) { + $table->dropColumn('remark'); + }); + } +}; diff --git a/lang/en.json b/lang/en.json index c6b1680..c01a3da 100644 --- a/lang/en.json +++ b/lang/en.json @@ -700,6 +700,7 @@ "Enter machine name": "Enter machine name", "Enter machine name...": "Enter machine name...", "Enter model name": "Enter model name", + "Enter remark (optional)": "Enter remark (optional)", "Enter role name": "Enter role name", "Enter serial number": "Enter serial number", "Enter your password to confirm": "Enter your password to confirm", @@ -1321,6 +1322,7 @@ "No products found matching your criteria.": "No products found matching your criteria.", "No records found": "No records found", "No related detail found": "No related detail found", + "No remark": "No remark", "No replenishment orders found": "No replenishment orders found", "No results found": "No results found", "No roles available": "No roles available", @@ -1394,6 +1396,7 @@ "Only machines under the same company can be cloned for security.": "Only machines under the same company can be cloned for security.", "Only pending/failed invoices can be queried": "Only pending/failed invoices can be queried", "Only pending/failed invoices can be re-issued": "Only pending/failed invoices can be re-issued", + "Only system administrators can void invoices": "Only system administrators can void invoices", "Only system roles can be assigned to platform administrative accounts.": "Only system roles can be assigned to platform administrative accounts.", "Operation Logs": "Operation Logs", "Operation Note": "Operation Note", @@ -1699,6 +1702,7 @@ "Register a new firmware version for OTA deployment": "Register a new firmware version for OTA deployment", "Release Notes": "Release Notes", "Remark": "Remark", + "Remark updated": "Remark updated", "Remote Change": "Remote Change", "Remote Checkout": "Remote Checkout", "Remote Command Center": "Remote Command Center", @@ -2298,6 +2302,7 @@ "Void failed: ": "Void failed: ", "Void success": "Void success", "Void this invoice?": "Void this invoice?", + "Voided": "Voided", "W2W": "W2W", "Waiting": "Waiting", "Waiting for Payment": "Waiting for Payment", diff --git a/lang/ja.json b/lang/ja.json index c21bd69..e655d2a 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -700,6 +700,7 @@ "Enter machine name": "機器名を入力してください", "Enter machine name...": "機器名を入力...", "Enter model name": "モデル名を入力してください", + "Enter remark (optional)": "備考を入力(任意)", "Enter role name": "ロール名を入力してください", "Enter serial number": "シリアル番号を入力してください", "Enter your password to confirm": "確認のためパスワードを入力してください", @@ -1321,6 +1322,7 @@ "No products found matching your criteria.": "条件に一致する商品が見つかりません。", "No records found": "記録が見つかりません", "No related detail found": "関連する詳細が見つかりません", + "No remark": "備考なし", "No replenishment orders found": "補充伝票が見つかりません", "No results found": "結果が見つかりません", "No roles available": "利用可能なロールがありません。", @@ -1394,6 +1396,7 @@ "Only machines under the same company can be cloned for security.": "セキュリティのため、同じ会社の機器設定のみコピーできます。", "Only pending/failed invoices can be queried": "Only pending/failed invoices can be queried", "Only pending/failed invoices can be re-issued": "Only pending/failed invoices can be re-issued", + "Only system administrators can void invoices": "システム管理者のみが請求書を無効化できます", "Only system roles can be assigned to platform administrative accounts.": "プラットフォーム管理アカウントにはシステムロールのみを割り当てることができます。", "Operation Logs": "Operation Logs", "Operation Note": "操作メモ", @@ -1699,6 +1702,7 @@ "Register a new firmware version for OTA deployment": "Register a new firmware version for OTA deployment", "Release Notes": "Release Notes", "Remark": "備考", + "Remark updated": "備考を更新しました", "Remote Change": "遠隔お釣り", "Remote Checkout": "遠隔決済", "Remote Command Center": "遠隔コマンドセンター", @@ -2298,6 +2302,7 @@ "Void failed: ": "Void failed: ", "Void success": "Void success", "Void this invoice?": "Void this invoice?", + "Voided": "無効化済み", "W2W": "倉庫間", "Waiting": "待機中", "Waiting for Payment": "支払待機中", diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 413dc63..34c5719 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -700,6 +700,7 @@ "Enter machine name": "請輸入機台名稱", "Enter machine name...": "輸入機台名稱...", "Enter model name": "請輸入型號名稱", + "Enter remark (optional)": "輸入備註(選填)", "Enter role name": "請輸入角色名稱", "Enter serial number": "請輸入機台序號", "Enter your password to confirm": "請輸入您的密碼以確認", @@ -1321,6 +1322,7 @@ "No products found matching your criteria.": "找不到符合條件的商品。", "No records found": "未找到相關紀錄", "No related detail found": "無關聯詳細資料", + "No remark": "尚無備註", "No replenishment orders found": "查無補貨單紀錄", "No results found": "查無搜尋結果", "No roles available": "目前沒有角色資料。", @@ -1394,6 +1396,7 @@ "Only machines under the same company can be cloned for security.": "基於安全性,僅限複製同公司旗下的機台設定。", "Only pending/failed invoices can be queried": "僅待開立/失敗的發票可查詢", "Only pending/failed invoices can be re-issued": "僅待開立/失敗的發票可補開", + "Only system administrators can void invoices": "僅系統管理員可作廢發票", "Only system roles can be assigned to platform administrative accounts.": "僅系統角色可指派給平台管理帳號。", "Operation Logs": "操作紀錄", "Operation Note": "操作備註", @@ -1699,6 +1702,7 @@ "Register a new firmware version for OTA deployment": "註冊新韌體版本以進行 OTA 部署", "Release Notes": "更新說明", "Remark": "備註", + "Remark updated": "備註已更新", "Remote Change": "遠端找零", "Remote Checkout": "遠端結帳", "Remote Command Center": "遠端指令中心", @@ -2298,6 +2302,7 @@ "Void failed: ": "作廢失敗:", "Void success": "作廢成功", "Void this invoice?": "確定要作廢這張發票嗎?", + "Voided": "已作廢", "W2W": "倉對倉", "Waiting": "等待中", "Waiting for Payment": "等待付款", diff --git a/resources/views/admin/sales/index.blade.php b/resources/views/admin/sales/index.blade.php index 733c684..199c47b 100644 --- a/resources/views/admin/sales/index.blade.php +++ b/resources/views/admin/sales/index.blade.php @@ -96,7 +96,14 @@ {{-- 發票作廢 / 補開 確認框(自製 UI,取代瀏覽器原生 confirm) --}} + iconType="danger" confirmColor="rose"> +
+ + +
+
@@ -113,9 +120,11 @@ function salesCenter() { showVoidModal: false, showReissueModal: false, pendingInvoiceForm: '', + voidRemark: '', confirmVoidInvoice(formId) { this.pendingInvoiceForm = formId; + this.voidRemark = ''; this.showVoidModal = true; }, @@ -125,9 +134,20 @@ function salesCenter() { }, submitInvoiceAction() { - if (this.pendingInvoiceForm) { - const form = document.getElementById(this.pendingInvoiceForm); - if (form) form.submit(); + const form = this.pendingInvoiceForm ? document.getElementById(this.pendingInvoiceForm) : null; + if (form) { + // 作廢時附帶管理者備註(注入隱藏 input,後端寫入 invoice.remark) + if (this.showVoidModal) { + let input = form.querySelector('input[name="remark"]'); + if (!input) { + input = document.createElement('input'); + input.type = 'hidden'; + input.name = 'remark'; + form.appendChild(input); + } + input.value = this.voidRemark || ''; + } + form.submit(); } this.showVoidModal = false; this.showReissueModal = false; diff --git a/resources/views/admin/sales/partials/order-detail-panel.blade.php b/resources/views/admin/sales/partials/order-detail-panel.blade.php index 1eeec53..f9d8ca3 100644 --- a/resources/views/admin/sales/partials/order-detail-panel.blade.php +++ b/resources/views/admin/sales/partials/order-detail-panel.blade.php @@ -102,6 +102,56 @@ + {{-- 管理者備註(訂單內部註記) --}} +
+
+
+
+

{{ __('Remark') }}

+
+ +
+
+ + +
+
+ {{-- 員工卡資訊 --}} @if($order->payment_type == 41 && $order->staffCardLog && $order->staffCardLog->staffCard)
@@ -322,7 +372,8 @@ @else {{ __('Donation invoices cannot be printed') }} @endif - {{-- 作廢:使用自製確認框 UI(不用瀏覽器原生 confirm) --}} + {{-- 作廢:僅平台系統管理員可見可用(後端亦有 403 守衛)。使用自製確認框 UI --}} + @if(auth()->user()->isSystemAdmin()) @@ -330,9 +381,55 @@ class="flex-1 sm:flex-none px-6 py-3 rounded-xl bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-bold text-xs border border-slate-200 dark:border-slate-700 hover:bg-rose-500 hover:text-white hover:border-rose-500 transition-all duration-300 shadow-sm"> {{ __('Void Invoice') }} + @endif @endif + + {{-- 發票備註(管理者內部註記;作廢視窗輸入的備註也會顯示於此) --}} +
+
+

{{ __('Remark') }}

+ +
+ + +
@endif diff --git a/resources/views/admin/sales/partials/tab-invoices.blade.php b/resources/views/admin/sales/partials/tab-invoices.blade.php index f8bd0af..cbf391a 100644 --- a/resources/views/admin/sales/partials/tab-invoices.blade.php +++ b/resources/views/admin/sales/partials/tab-invoices.blade.php @@ -394,7 +394,7 @@ @endif - @if($st === 'issued') + @if($st === 'issued' && auth()->user()->isSystemAdmin())
@csrf diff --git a/resources/views/admin/sales/partials/tab-orders.blade.php b/resources/views/admin/sales/partials/tab-orders.blade.php index 8cd23e0..bb84bda 100644 --- a/resources/views/admin/sales/partials/tab-orders.blade.php +++ b/resources/views/admin/sales/partials/tab-orders.blade.php @@ -289,16 +289,20 @@ $targetUrl = route('admin.sales.index', ['tab' => 'invoices', 'search' => $invoiceSearchValue]); @endphp @if($order->invoice) + @php $invVoided = $order->invoice->status === 'void'; @endphp
- {{ + {{ $order->invoice->invoice_no }} + @if($invVoided) + {{ __('Voided') }} + @endif
@else --- @@ -428,9 +432,13 @@

{{ __('Invoice Number') }}

+ @php $invVoided = $order->invoice && $order->invoice->status === 'void'; @endphp

- {{ $order->invoice->invoice_no ?? '---' }} + class="text-sm font-bold cursor-pointer hover:text-cyan-500 transition-colors flex items-center gap-1.5 {{ $invVoided ? 'text-slate-400 dark:text-slate-600' : 'text-slate-700 dark:text-slate-300' }}"> + {{ $order->invoice->invoice_no ?? '---' }} + @if($invVoided) + {{ __('Voided') }} + @endif

diff --git a/resources/views/components/confirm-modal.blade.php b/resources/views/components/confirm-modal.blade.php index 68276f9..655d9ed 100644 --- a/resources/views/components/confirm-modal.blade.php +++ b/resources/views/components/confirm-modal.blade.php @@ -76,6 +76,12 @@
+ {{-- 選用插槽:作廢視窗用來放備註輸入框(其他用途留空) --}} + @if(trim($slot) !== '') +
+ {{ $slot }} +
+ @endif