[FEAT] 電子發票作廢權限控管、交易訂單與發票備註、作廢發票差異顯示
1. 發票作廢改為僅限平台系統管理員:voidInvoice() 加 isSystemAdmin() 403 守衛,訂單詳情面板與 tab-invoices 手機卡片作廢按鈕同步加 isSystemAdmin 顯示條件(列印鈕維持所有人可用)。 2. 新增 orders、invoices 各自獨立的 remark 欄位(migration + Model fillable),供管理者編輯內部備註。 3. 新增 updateOrderRemark / updateInvoiceRemark 端點與 PATCH 路由,AJAX 回傳 JSON、租戶隔離沿用 TenantScoped。 4. 訂單詳情面板加入訂單與發票備註的 Alpine 內嵌編輯(fetch + toast,不重整頁面),所有可進銷售中心者皆可編輯。 5. confirm-modal 加可選 slot;作廢確認視窗加備註輸入框,submitInvoiceAction() 送出前注入隱藏 remark,寫入 invoice.remark(不影響送綠界的 void_reason)。 6. 訂單列表(桌面與手機)已作廢發票改以灰色刪除線加「已作廢」標記顯示,index eager load 補載 invoice.status。 7. 新增 5 組三語系 key(Voided、Enter remark (optional)、Remark updated、No remark、Only system administrators can void invoices),zh_TW/en/ja 對齊排序至 2572 鍵。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bfaf0e7317
commit
4766ec6939
@ -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)) {
|
||||
|
||||
@ -30,6 +30,7 @@ class Invoice extends Model
|
||||
'retry_count',
|
||||
'voided_at',
|
||||
'void_reason',
|
||||
'remark',
|
||||
'random_number',
|
||||
'love_code',
|
||||
'rtn_code',
|
||||
|
||||
@ -63,6 +63,7 @@ class Order extends Model
|
||||
'status',
|
||||
'metadata',
|
||||
'delivery_status',
|
||||
'remark',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
||||
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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",
|
||||
|
||||
@ -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": "支払待機中",
|
||||
|
||||
@ -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": "等待付款",
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user