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) --}}
{{ __('Remark') }}
+ +