[PROMOTE] dev → demo:電子發票作廢權限與備註功能晉升驗收

1. [FEAT] 電子發票作廢權限控管、交易訂單與發票備註、作廢發票差異顯示(4766ec6)
   - 發票作廢限平台系統管理員(後端 403 守衛 + 前端按鈕隱藏)。
   - orders/invoices 新增 remark 欄位與更新端點,訂單詳情面板內嵌備註編輯。
   - 作廢確認視窗加備註輸入框,寫入 invoice.remark。
   - 訂單列表已作廢發票以刪除線加「已作廢」標記顯示。
   - 新增 5 組三語系 key 並對齊排序。

備註:本次含新增 migration(orders.remark、invoices.remark,皆 nullable,非破壞性),demo 部署將自動套用。
This commit is contained in:
sky121113 2026-06-18 10:53:53 +08:00
commit 80300dfc67
13 changed files with 237 additions and 13 deletions

View File

@ -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)) {

View File

@ -30,6 +30,7 @@ class Invoice extends Model
'retry_count',
'voided_at',
'void_reason',
'remark',
'random_number',
'love_code',
'rtn_code',

View File

@ -63,6 +63,7 @@ class Order extends Model
'status',
'metadata',
'delivery_status',
'remark',
];
protected $casts = [

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* 為交易訂單與電子發票各自新增「管理者備註」欄位。
* - orders.remark訂單內部備註出貨客訴等
* - invoices.remark發票內部備註作廢視窗亦寫入此欄不影響送綠界的 void_reason
* 兩欄皆 nullable對既有資料零影響。
*/
return new class extends Migration
{
public function up(): void
{
Schema::table('orders', function (Blueprint $table) {
$table->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');
});
}
};

View File

@ -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",

View File

@ -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": "支払待機中",

View File

@ -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": "等待付款",

View File

@ -96,7 +96,14 @@
{{-- 發票作廢 / 補開 確認框(自製 UI取代瀏覽器原生 confirm --}}
<x-confirm-modal alpineVar="showVoidModal" confirmAction="submitInvoiceAction()" :title="__('Void Invoice')"
:message="__('Void this invoice?')" :confirmText="__('Void Invoice')" :cancelText="__('Cancel')"
iconType="danger" confirmColor="rose" />
iconType="danger" confirmColor="rose">
<div>
<label class="block text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-2">{{ __('Remark') }}</label>
<textarea x-model="voidRemark" rows="2" maxlength="1000"
placeholder="{{ __('Enter remark (optional)') }}"
class="luxury-input w-full text-sm py-2.5 px-4 resize-none"></textarea>
</div>
</x-confirm-modal>
<x-confirm-modal alpineVar="showReissueModal" confirmAction="submitInvoiceAction()" :title="__('Re-issue')"
:message="__('Re-issue this invoice?')" :confirmText="__('Re-issue')" :cancelText="__('Cancel')"
iconType="info" confirmColor="sky" />
@ -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;

View File

@ -102,6 +102,56 @@
</div>
</section>
{{-- 管理者備註(訂單內部註記) --}}
<section class="space-y-4"
x-data="{
editing: false,
saving: false,
value: @js($order->remark ?? ''),
original: @js($order->remark ?? ''),
cancel() { this.value = this.original; this.editing = false; },
save() {
this.saving = true;
fetch(@js(route('admin.sales.orders.remark', $order)), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': @js(csrf_token()), 'X-Requested-With': 'XMLHttpRequest' },
body: JSON.stringify({ remark: this.value })
}).then(r => { if (!r.ok) throw 0; return r.json(); })
.then(d => { this.original = this.value; this.editing = false; window.showToast?.((d && d.message) || @js(__('Remark updated')), 'success'); })
.catch(() => window.showToast?.(@js(__('Operation failed')), 'error'))
.finally(() => this.saving = false);
}
}">
<div class="flex items-center justify-between gap-2 px-1">
<div class="flex items-center gap-2">
<div class="w-1.5 h-4 bg-cyan-500 rounded-full"></div>
<h3 class="text-sm font-black text-slate-800 dark:text-white uppercase tracking-widest">{{ __('Remark') }}</h3>
</div>
<button type="button" x-show="!editing" @click="editing = true"
class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-widest hover:underline">{{ __('Edit') }}</button>
</div>
<div class="p-6 bg-slate-50/50 dark:bg-slate-800/30 rounded-[2rem] border border-slate-100 dark:border-slate-800">
<template x-if="!editing">
<p class="text-sm font-bold whitespace-pre-wrap break-words"
:class="value ? 'text-slate-600 dark:text-slate-300' : 'text-slate-300 dark:text-slate-600'"
x-text="value || @js(__('No remark'))"></p>
</template>
<template x-if="editing">
<div class="space-y-3">
<textarea x-model="value" rows="3" maxlength="1000"
placeholder="{{ __('Enter remark (optional)') }}"
class="luxury-input w-full text-sm py-2.5 px-4 resize-none"></textarea>
<div class="flex items-center justify-end gap-2">
<button type="button" @click="cancel()" :disabled="saving"
class="px-5 py-2 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-black text-xs uppercase tracking-widest disabled:opacity-60">{{ __('Cancel') }}</button>
<button type="button" @click="save()" :disabled="saving"
class="px-5 py-2 rounded-xl bg-cyan-500 hover:bg-cyan-600 text-white font-black text-xs uppercase tracking-widest disabled:opacity-60">{{ __('Save') }}</button>
</div>
</div>
</template>
</div>
</section>
{{-- 員工卡資訊 --}}
@if($order->payment_type == 41 && $order->staffCardLog && $order->staffCardLog->staffCard)
<section class="space-y-4">
@ -322,7 +372,8 @@
@else
<span class="text-[11px] font-bold text-slate-400 dark:text-slate-500 px-2">{{ __('Donation invoices cannot be printed') }}</span>
@endif
{{-- 作廢:使用自製確認框 UI不用瀏覽器原生 confirm --}}
{{-- 作廢:僅平台系統管理員可見可用(後端亦有 403 守衛)。使用自製確認框 UI --}}
@if(auth()->user()->isSystemAdmin())
<form id="invoice-void-form-{{ $order->invoice->id }}" method="POST" action="{{ route('admin.sales.invoices.void', $order->invoice) }}" class="hidden">
@csrf
</form>
@ -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') }}
</button>
@endif
</div>
@endif
</div>
{{-- 發票備註(管理者內部註記;作廢視窗輸入的備註也會顯示於此) --}}
<div class="relative mt-6 pt-6 border-t border-indigo-100/70 dark:border-indigo-500/20"
x-data="{
editing: false,
saving: false,
value: @js($order->invoice->remark ?? ''),
original: @js($order->invoice->remark ?? ''),
cancel() { this.value = this.original; this.editing = false; },
save() {
this.saving = true;
fetch(@js(route('admin.sales.invoices.remark', $order->invoice)), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': @js(csrf_token()), 'X-Requested-With': 'XMLHttpRequest' },
body: JSON.stringify({ remark: this.value })
}).then(r => { if (!r.ok) throw 0; return r.json(); })
.then(d => { this.original = this.value; this.editing = false; window.showToast?.((d && d.message) || @js(__('Remark updated')), 'success'); })
.catch(() => window.showToast?.(@js(__('Operation failed')), 'error'))
.finally(() => this.saving = false);
}
}">
<div class="flex items-center justify-between gap-2 mb-2">
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Remark') }}</p>
<button type="button" x-show="!editing" @click="editing = true"
class="text-[10px] font-black text-indigo-500 dark:text-indigo-400 uppercase tracking-widest hover:underline">{{ __('Edit') }}</button>
</div>
<template x-if="!editing">
<p class="text-sm font-bold whitespace-pre-wrap break-words"
:class="value ? 'text-slate-600 dark:text-slate-300' : 'text-slate-300 dark:text-slate-600'"
x-text="value || @js(__('No remark'))"></p>
</template>
<template x-if="editing">
<div class="space-y-3">
<textarea x-model="value" rows="3" maxlength="1000"
placeholder="{{ __('Enter remark (optional)') }}"
class="luxury-input w-full text-sm py-2.5 px-4 resize-none"></textarea>
<div class="flex items-center justify-end gap-2">
<button type="button" @click="cancel()" :disabled="saving"
class="px-5 py-2 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-black text-xs uppercase tracking-widest disabled:opacity-60">{{ __('Cancel') }}</button>
<button type="button" @click="save()" :disabled="saving"
class="px-5 py-2 rounded-xl bg-indigo-500 hover:bg-indigo-600 text-white font-black text-xs uppercase tracking-widest disabled:opacity-60">{{ __('Save') }}</button>
</div>
</div>
</template>
</div>
</div>
</section>
@endif

View File

@ -394,7 +394,7 @@
</form>
</div>
@endif
@if($st === 'issued')
@if($st === 'issued' && auth()->user()->isSystemAdmin())
<form method="POST" action="{{ route('admin.sales.invoices.void', $invoice) }}"
onsubmit="return confirm('{{ __('Void this invoice?') }}')">
@csrf

View File

@ -289,16 +289,20 @@
$targetUrl = route('admin.sales.index', ['tab' => 'invoices', 'search' => $invoiceSearchValue]);
@endphp
@if($order->invoice)
@php $invVoided = $order->invoice->status === 'void'; @endphp
<div @click.stop="switchTab('invoices', '{{ $targetUrl }}')"
class="flex items-center gap-1.5 text-sm font-extrabold text-slate-600 dark:text-slate-400 cursor-pointer hover:text-cyan-500 transition-colors group/invoice"
class="flex items-center gap-1.5 text-sm font-extrabold cursor-pointer hover:text-cyan-500 transition-colors group/invoice {{ $invVoided ? 'text-slate-400 dark:text-slate-600' : 'text-slate-600 dark:text-slate-400' }}"
title="{{ __('Go to E-Invoice') }}">
<svg class="w-4 h-4 text-slate-300 dark:text-slate-600 group-hover/invoice:text-cyan-500"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<span class="group-hover/invoice:underline decoration-2 underline-offset-4">{{
<span class="group-hover/invoice:underline decoration-2 underline-offset-4 {{ $invVoided ? 'line-through' : '' }}">{{
$order->invoice->invoice_no }}</span>
@if($invVoided)
<span class="px-1.5 py-0.5 rounded bg-slate-500/10 border border-slate-500/20 text-[9px] font-black text-slate-500 uppercase tracking-widest no-underline">{{ __('Voided') }}</span>
@endif
</div>
@else
<span class="text-xs font-bold text-slate-300 dark:text-slate-700 tracking-widest">---</span>
@ -428,9 +432,13 @@
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{
__('Invoice Number') }}</p>
@php $invVoided = $order->invoice && $order->invoice->status === 'void'; @endphp
<p @click.stop="switchTab('invoices', '{{ $targetUrl }}')"
class="text-sm font-bold text-slate-700 dark:text-slate-300 cursor-pointer hover:text-cyan-500 transition-colors">
{{ $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' }}">
<span class="{{ $invVoided ? 'line-through' : '' }}">{{ $order->invoice->invoice_no ?? '---' }}</span>
@if($invVoided)
<span class="px-1.5 py-0.5 rounded bg-slate-500/10 border border-slate-500/20 text-[9px] font-black text-slate-500 uppercase tracking-widest">{{ __('Voided') }}</span>
@endif
</p>
</div>
<div>

View File

@ -76,6 +76,12 @@
</div>
</div>
</div>
{{-- 選用插槽:作廢視窗用來放備註輸入框(其他用途留空) --}}
@if(trim($slot) !== '')
<div class="mt-6">
{{ $slot }}
</div>
@endif
<div class="mt-8 sm:mt-10 sm:flex sm:flex-row-reverse gap-3">
<button type="button" @click="{{ $confirmAction }}"
@if($isSubmittingVar) :disabled="{{ $isSubmittingVar }}" @endif

View File

@ -148,6 +148,10 @@ Route::middleware(['auth', 'auth.session', 'verified', 'tenant.access'])->prefix
Route::post('/invoices/{invoice}/reissue', [App\Http\Controllers\Admin\SalesController::class, 'reissueInvoice'])->name('invoices.reissue');
Route::post('/invoices/{invoice}/void', [App\Http\Controllers\Admin\SalesController::class, 'voidInvoice'])->name('invoices.void');
// 管理者備註(訂單/發票各自獨立)
Route::patch('/orders/{order}/remark', [App\Http\Controllers\Admin\SalesController::class, 'updateOrderRemark'])->name('orders.remark');
Route::patch('/invoices/{invoice}/remark', [App\Http\Controllers\Admin\SalesController::class, 'updateInvoiceRemark'])->name('invoices.remark');
// 取貨碼
Route::get('/pickup-codes', [App\Http\Controllers\Admin\SalesController::class, 'pickupCodes'])->name('pickup-codes');
Route::post('/pickup-codes', [App\Http\Controllers\Admin\SalesController::class, 'storePickupCode'])->name('pickup-codes.store');