[PROMOTE] dev -> demo: 銷售紀錄手動補單功能

1. 手動補單(機台斷線/漏報時人工補登銷售紀錄):複用扣庫存與後台開發票邏輯,訂單標記 manual、flow_id 以 MAN 前綴區隔。
2. 補單彈窗採站內元件樣式(搜尋式下拉 + +/- 計數器),扣庫存與開立電子發票皆為選配;開發票受機台電子發票閘門限制。
3. 權限限系統管理員(前端隱藏 + 後端把關),全程寫入操作審計日誌。
4. 訂單列表顯示「手動」標籤;三語系字串補齊對齊。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
sky121113 2026-06-26 16:19:47 +08:00
commit ef1bdfe1d5
10 changed files with 877 additions and 4 deletions

View File

@ -197,6 +197,170 @@ class SalesController extends Controller
]);
}
/**
* 手動補單:取得指定機台的貨道(含商品)與電子發票開關,供補單彈窗動態載入。
* 僅限系統管理員(整個補單功能限系統管理員)。
*/
public function manualMachineSlots(Request $request)
{
if (!Auth::user()->isSystemAdmin()) {
abort(403);
}
$machine = Machine::with(['slots.product:id,name,price'])
->findOrFail($request->input('machine_id'));
$slots = $machine->slots
->sortBy(fn ($s) => str_pad(ltrim($s->slot_no, '0') ?: '0', 6, '0', STR_PAD_LEFT))
->values()
->map(fn ($slot) => [
'slot_no' => $slot->slot_no,
'product_id' => $slot->product_id,
'product_name' => $slot->product?->localized_name ?? __('Unknown Product'),
'price' => $slot->product?->price !== null ? (float) $slot->product->price : 0,
'stock' => (int) $slot->stock,
]);
return response()->json([
'success' => true,
'tax_invoice_enabled' => (bool) $machine->tax_invoice_enabled,
'slots' => $slots,
]);
}
/**
* 手動補單:機台斷線/漏報時人工補登銷售紀錄。
*
* 僅限系統管理員。複用 TransactionService::createManualOrder與機台上報共用扣庫存開發票邏輯
* 扣庫存與開立電子發票皆為選配;開發票受機台「電子發票開關」閘門限制(後端再驗一次)。
*/
public function storeManualOrder(Request $request, \App\Services\Transaction\TransactionService $service)
{
// 權限閘門:補單僅限平台系統管理員(前端僅隱藏入口,真正把關在後端,防偽造 POST
if (!Auth::user()->isSystemAdmin()) {
abort(403, __('Only system administrators can create manual orders'));
}
$validated = $request->validate([
'machine_id' => ['required', 'exists:machines,id'],
'payment_type' => ['required', 'integer', 'in:' . implode(',', array_keys(Order::getPaymentTypeLabels()))],
'occurred_at' => ['nullable', 'date'],
'deduct_stock' => ['nullable', 'boolean'],
'remark' => ['nullable', 'string', 'max:1000'],
'items' => ['required', 'array', 'min:1'],
'items.*.product_id' => ['required', 'integer', 'exists:products,id'],
'items.*.slot_no' => ['nullable', 'string', 'max:20'],
'items.*.price' => ['required', 'numeric', 'min:0'],
'items.*.quantity' => ['required', 'integer', 'min:1', 'max:999'],
'issue_invoice' => ['nullable', 'boolean'],
'invoice_type' => ['nullable', 'required_if:issue_invoice,1', 'in:b2c,taxid,mobile,citizen,donation'],
'invoice_value' => ['nullable', 'string', 'max:64'],
]);
$machine = Machine::findOrFail($validated['machine_id']);
// 組裝發票輸入(僅在勾選開立、且機台開關開啟時)。後端閘門:機台關閉電子發票一律不開。
$invoice = null;
if (!empty($validated['issue_invoice'])) {
if (!$machine->tax_invoice_enabled) {
return $this->manualOrderResponse($request, false, __('This machine has e-invoice disabled; cannot issue'));
}
$invoice = $this->buildManualInvoiceInput($validated['invoice_type'] ?? 'b2c', trim((string) ($validated['invoice_value'] ?? '')));
if (isset($invoice['error'])) {
return $this->manualOrderResponse($request, false, $invoice['error']);
}
}
try {
$order = $service->createManualOrder([
'serial_no' => $machine->serial_no,
'created_by' => Auth::id(),
'payment_type' => (int) $validated['payment_type'],
'occurred_at' => $validated['occurred_at'] ?? null,
'deduct_stock' => !empty($validated['deduct_stock']),
'remark' => $validated['remark'] ?? null,
'items' => array_map(fn ($i) => [
'product_id' => (int) $i['product_id'],
'slot_no' => $i['slot_no'] ?? null,
'price' => (float) $i['price'],
'quantity' => (int) $i['quantity'],
], $validated['items']),
'invoice' => $invoice,
]);
} catch (\Throwable $e) {
\Log::error('Manual order creation failed: ' . $e->getMessage(), [
'machine_id' => $machine->id,
'user_id' => Auth::id(),
]);
return $this->manualOrderResponse($request, false, __('Failed to create manual order'));
}
// 審計日誌:留痕「誰/何時/對哪台機台補了什麼、是否開發票/扣庫存」。
SystemOperationLog::create([
'company_id' => $machine->company_id,
'user_id' => Auth::id(),
'module' => 'manual_order',
'action' => 'create',
'target_id' => $order->id,
'target_type' => Order::class,
'new_values' => [
'order_no' => $order->order_no,
'flow_id' => $order->flow_id,
'machine_id' => $machine->id,
'total_amount' => $order->total_amount,
'payment_type' => $order->payment_type,
'deduct_stock' => !empty($validated['deduct_stock']),
'issue_invoice' => $invoice !== null,
'occurred_at' => optional($order->machine_time)->toDateTimeString(),
],
]);
return $this->manualOrderResponse($request, true, __('Manual order created: :no', ['no' => $order->order_no]));
}
/**
* 將補單表單的發票類型/輸入值轉成 recordInvoice 可用的欄位。
* reissue() business_tax_id / carrier_id / love_code 自動判別綠界載具類型與 Print 旗標。
*/
private function buildManualInvoiceInput(string $type, string $value): array
{
switch ($type) {
case 'b2c': // 一般 B2C綠界會員載具Print=0不可列印
return [];
case 'taxid': // 統一編號Print=1可列印
if (!preg_match('/^\d{8}$/', $value)) {
return ['error' => __('Tax ID must be 8 digits')];
}
return ['business_tax_id' => $value];
case 'mobile': // 手機條碼載具8 碼,/ 開頭)
if (!preg_match('#^/[0-9A-Z.\-+]{7}$#', $value)) {
return ['error' => __('Invalid mobile barcode carrier')];
}
return ['carrier_id' => $value];
case 'citizen': // 自然人憑證載具16 碼2 英文 + 14 數字)
if (!preg_match('/^[A-Z]{2}\d{14}$/', $value)) {
return ['error' => __('Invalid citizen digital certificate carrier')];
}
return ['carrier_id' => $value];
case 'donation': // 捐贈(愛心碼 3~7 碼數字)
if (!preg_match('/^\d{3,7}$/', $value)) {
return ['error' => __('Donation love code must be 3 to 7 digits')];
}
return ['love_code' => $value];
default:
return ['error' => __('Invalid invoice type')];
}
}
/** 補單回應AJAX 回 JSON、一般表單回 back()。 */
private function manualOrderResponse(Request $request, bool $success, string $message)
{
if ($request->expectsJson()) {
return response()->json(['success' => $success, 'message' => $message], $success ? 200 : 422);
}
return back()->with($success ? 'success' : 'error', $message);
}
// 取貨碼設定
public function pickupCodes(Request $request)
{

View File

@ -17,6 +17,7 @@ class Order extends Model
// 訂單類型 (order_type)
public const TYPE_SALE = 'sale'; // 一般銷售
public const TYPE_PHARMACY_PICKUP = 'pharmacy_pickup'; // 領藥單
public const TYPE_MANUAL = 'manual'; // 手動補單(後台人工補登,機台斷線漏報時使用)
// 支付狀態
public const PAYMENT_STATUS_FAILED = 0;
@ -204,6 +205,12 @@ class Order extends Model
return $query->where('order_type', self::TYPE_PHARMACY_PICKUP);
}
/** 是否為後台手動補單 */
public function getIsManualAttribute(): bool
{
return $this->order_type === self::TYPE_MANUAL;
}
public function machine()
{
return $this->belongsTo(Machine::class);

View File

@ -585,6 +585,125 @@ class TransactionService
});
}
/**
* 後台手動補單(機台斷線/漏報時,由系統管理員人工補登銷售紀錄)。
*
* 與機台上報共用底層 recordDispense()(扣庫存)與 recordInvoice()+IssueInvoiceJob後台開發票
* 但訂單標記 order_type=manual、created_by=操作者flow_id 'MAN' 前綴與機台上報區隔,
* 一眼可辨識並天然避開 orders.flow_id 唯一鍵衝突(長度 ≤30、純英數亦相容綠界 RelateNumber
* 扣庫存與開發票皆為選配(由呼叫端決定);權限把關(限系統管理員)在 Controller。
*
* @param array $data {
* serial_no, created_by, payment_type, occurred_at?,
* items: [{product_id, slot_no?, product_name?, price, quantity}],
* deduct_stock?: bool, remark?,
* invoice?: null|{business_tax_id?, carrier_id?, love_code?} // 給定才開立
* }
*/
public function createManualOrder(array $data): Order
{
$machine = Machine::where('serial_no', $data['serial_no'])->firstOrFail();
$items = $data['items'] ?? [];
if (empty($items)) {
throw new \InvalidArgumentException('Manual order requires at least one item');
}
$totalAmount = collect($items)
->sum(fn ($i) => (float) $i['price'] * (int) $i['quantity']);
// 補單常為事後補登machine_time / created_at 採實際發生時間,使銷售報表落在正確日期。
$occurredAt = !empty($data['occurred_at'])
? \Carbon\Carbon::parse($data['occurred_at'])
: now();
$flowId = $this->generateManualFlowId();
return DB::transaction(function () use ($machine, $flowId, $items, $totalAmount, $occurredAt, $data) {
// 1. 建立訂單(終態 completed + 付款成功)
$order = Order::create([
'company_id' => $machine->company_id,
'flow_id' => $flowId,
'order_no' => $this->generateOrderNo(),
'order_type' => Order::TYPE_MANUAL,
'created_by' => $data['created_by'] ?? null,
'machine_id' => $machine->id,
'payment_type' => (int) ($data['payment_type'] ?? 0),
'total_amount' => $totalAmount,
'original_amount' => $totalAmount,
'discount_amount' => 0,
'pay_amount' => $totalAmount,
'payment_status' => Order::PAYMENT_STATUS_SUCCESS,
'payment_at' => $occurredAt,
'machine_time' => $occurredAt,
'status' => Order::STATUS_COMPLETED,
'delivery_status' => Order::DELIVERY_STATUS_SUCCESS,
'remark' => $data['remark'] ?? null,
'metadata' => ['source' => 'manual'],
]);
// created_at 對齊實際發生時間(補單事後補登,報表需落在真實日期而非補登當下)
$order->forceFill(['created_at' => $occurredAt])->save();
$this->createOrderItems($order, $items);
// 2. 扣庫存(選配):逐單位呼叫 recordDispense與機台上報一致成功才扣 1庫存為 0 時不為負)
if (!empty($data['deduct_stock'])) {
foreach ($items as $item) {
$qty = max(1, (int) ($item['quantity'] ?? 1));
for ($n = 0; $n < $qty; $n++) {
$this->recordDispense([
'serial_no' => $machine->serial_no,
'flow_id' => $flowId,
'order_id' => $order->id,
'slot_no' => $item['slot_no'] ?? null,
'product_id' => $item['product_id'] ?? null,
'amount' => $item['price'],
'dispense_status' => 1,
'machine_time' => $occurredAt,
]);
}
}
}
// 3. 開立電子發票(選配):建 pending 發票 → commit 後派 IssueInvoiceJob 去綠界開立。
// 閘門:唯有機台「電子發票開關」開啟才開立(防禦縱深,與 finalizeTransaction 一致;前端已隱藏選項)。
if (!empty($data['invoice']) && $machine->tax_invoice_enabled) {
$invoiceData = $data['invoice'];
$invoiceData['serial_no'] = $machine->serial_no;
$invoiceData['flow_id'] = $flowId;
$invoiceData['order_id'] = $order->id;
$invoiceData['amount'] = $totalAmount;
$invoiceData['status'] = Invoice::STATUS_PENDING;
$invoiceData['machine_time'] = $occurredAt;
$invoice = $this->recordInvoice($invoiceData);
if ($invoice->status === Invoice::STATUS_PENDING) {
DB::afterCommit(function () use ($invoice) {
\App\Jobs\Transaction\IssueInvoiceJob::dispatch($invoice->id);
});
}
} elseif (!empty($data['invoice'])) {
Log::warning('手動補單要求開立發票,但機台已關閉電子發票,略過開立', [
'machine_id' => $machine->id,
'flow_id' => $flowId,
'order_id' => $order->id,
]);
}
return $order;
});
}
/** 產生手動補單專用 flow_id'MAN' 前綴全域唯一、純英數、≤30 字)。 */
protected function generateManualFlowId(): string
{
do {
$flowId = 'MAN' . now()->format('YmdHis') . strtoupper(bin2hex(random_bytes(3)));
} while (Order::withoutGlobalScopes()->where('flow_id', $flowId)->exists());
return $flowId;
}
/**
* 領藥單出貨回報(獨立於銷售流程)。
*

View File

@ -225,6 +225,7 @@
"Avatar updated successfully.": "Avatar updated successfully.",
"Avg Cycle": "Avg Cycle",
"Awaiting Pickup": "Awaiting Pickup",
"B2C (member carrier)": "B2C (member carrier)",
"Back to History": "Back to History",
"Back to List": "Back to List",
"Badge Settings": "Badge Settings",
@ -328,6 +329,7 @@
"Choose File": "Choose File",
"Choose Logo": "Choose Logo",
"Choose Overall BG": "Choose Overall BG",
"Citizen certificate carrier": "Citizen certificate carrier",
"Clear": "Clear",
"Clear Abnormal Status": "Clear Abnormal Status",
"Clear Filter": "Clear Filter",
@ -365,6 +367,7 @@
"Company Level": "Company Level",
"Company Name": "Company Name",
"Company Settings": "Company Settings",
"Company Tax ID": "Company Tax ID",
"Complete movement history log": "Complete movement history log",
"Completed": "Completed",
"Completed At": "Completed At",
@ -436,6 +439,7 @@
"Create Config": "Create Config",
"Create Machine": "Create Machine",
"Create New Role": "Create New Role",
"Create Order": "Create Order",
"Create Payment Config": "Create Payment Config",
"Create Pharmacy Pickup Order": "Create Pharmacy Pickup Order",
"Create Product": "Create Product",
@ -520,6 +524,7 @@
"Date Range": "Date Range",
"Day Before": "Day Before",
"Days": "Days",
"Deduct stock": "Deduct stock",
"Default": "Default",
"Default BG": "Default BG",
"Default Card BG": "Default Card BG",
@ -531,6 +536,7 @@
"Default Temp Limits": "Default Temp Limits",
"Default Temp Lower Limit (°C)": "Default Temp Lower Limit (°C)",
"Default Temp Upper Limit (°C)": "Default Temp Upper Limit (°C)",
"Defaults to now if empty": "Defaults to now if empty",
"Define and manage security roles and permissions.": "Define and manage security roles and permissions.",
"Define custom temperature limits or inherit from model": "Define custom temperature limits or inherit from model",
"Define new third-party payment parameters": "Define new third-party payment parameters",
@ -612,7 +618,10 @@
"Displayed below Logo on the left (defaults to customer name).": "Displayed below Logo on the left (defaults to customer name).",
"Displayed below main title on the left.": "Displayed below main title on the left.",
"Displaying": "Displaying",
"Donation": "Donation",
"Donation invoices cannot be printed": "Donation invoices cannot be printed",
"Donation love code": "Donation love code",
"Donation love code must be 3 to 7 digits": "Donation love code must be 3 to 7 digits",
"Done": "Done",
"Door Closed": "Door Closed",
"Door Opened": "Door Opened",
@ -764,11 +773,13 @@
"Export to Excel": "Export to Excel",
"External URL": "External URL",
"Failed": "Failed",
"Failed to create manual order": "Failed to create manual order",
"Failed to fetch machine data.": "Failed to fetch machine data.",
"Failed to get invoice print URL": "Failed to get invoice print URL",
"Failed to load permissions": "Failed to load permissions",
"Failed to load preview": "Failed to load preview",
"Failed to load pricing": "Failed to load pricing",
"Failed to load slots": "Failed to load slots",
"Failed to load tab content": "Failed to load tab content",
"Failed to resolve issues.": "Failed to resolve issues.",
"Failed to save permissions.": "Failed to save permissions.",
@ -880,6 +891,7 @@
"Flow ID / Slot / Time": "Flow ID / Slot / Time",
"Flow ID / Status": "Flow ID / Status",
"Flow ID / Time": "Flow ID / Time",
"For sales not reported due to disconnection. System administrators only.": "For sales not reported due to disconnection. System administrators only.",
"Force End Session": "Force End Session",
"Force end current session": "Force end current session",
"Forced Update": "Forced Update",
@ -968,6 +980,9 @@
"Insufficient stock for :name (requested :qty, available :available).": "Insufficient stock for :name (requested :qty, available :available).",
"Insufficient stock for transfer": "Insufficient stock for transfer",
"Insufficient stored-value / e-wallet balance": "Insufficient stored-value / e-wallet balance",
"Invalid citizen digital certificate carrier": "Invalid citizen digital certificate carrier",
"Invalid invoice type": "Invalid invoice type",
"Invalid mobile barcode carrier": "Invalid mobile barcode carrier",
"Invalid personnel selection": "Invalid personnel selection",
"Invalid status transition": "Invalid status transition",
"Inventory": "Inventory",
@ -986,10 +1001,13 @@
"Invoice Number / Time": "Invoice Number / Time",
"Invoice Status": "Invoice Status",
"Invoice Store ID": "Invoice Store ID",
"Invoice Type": "Invoice Type",
"Issue a pharmacy pickup order, print the QR ticket, patient scans at the machine to dispense.": "Issue a pharmacy pickup order, print the QR ticket, patient scans at the machine to dispense.",
"Issue e-invoice": "Issue e-invoice",
"Issue pharmacy pickup orders from the backend; patient scans QR to dispense.": "Issue pharmacy pickup orders from the backend; patient scans QR to dispense.",
"Issued": "Issued",
"Issued At": "Issued At",
"Issued via ECPay in the background after submission.": "Issued via ECPay in the background after submission.",
"Item List": "Item List",
"Items": "Items",
"JKO Pay": "JKO Pay",
@ -1049,6 +1067,7 @@
"Loading Data": "Loading Data",
"Loading failed": "Loading failed",
"Loading machines...": "Loading machines...",
"Loading slots...": "Loading slots...",
"Location": "Location",
"Location found!": "Location found!",
"Location not found": "Location not found",
@ -1191,9 +1210,13 @@
"Manage your warehouses, including main and branch warehouses": "Manage your warehouses, including main and branch warehouses",
"Management of operational parameters": "Management of operational parameters",
"Management of operational parameters and models": "Management of operational parameters and models",
"Manual": "Manual",
"Manual Entry": "Manual Entry",
"Manual Order Entry": "Manual Order Entry",
"Manual Sync Ads": "Manual Sync Ads",
"Manual Sync Products": "Manual Sync Products",
"Manual Sync Settings": "Manual Sync Settings",
"Manual order created: :no": "Manual order created: :no",
"Manufacturer": "Manufacturer",
"Marketing and Loyalty": "Marketing and Loyalty",
"Material Code": "Material Code",
@ -1265,6 +1288,7 @@
"Missing RelateNumber, cannot query": "Missing RelateNumber, cannot query",
"Mobile Pay": "Mobile Pay",
"Mobile Payment": "Mobile Payment",
"Mobile barcode carrier": "Mobile barcode carrier",
"Model": "Model",
"Model Default": "Model Default",
"Model Name": "Model Name",
@ -1337,6 +1361,7 @@
"No history records": "No history records",
"No images uploaded": "No images uploaded",
"No invoice issued, cannot void": "No invoice issued, cannot void",
"No items added yet": "No items added yet",
"No location set": "No location set",
"No login history yet": "No login history yet",
"No logs found": "No logs found",
@ -1447,6 +1472,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 create manual orders": "Only system administrators can create manual orders",
"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",
@ -1644,6 +1670,7 @@
"Preview Mode - Interactive Vending Portal Mockup": "Preview Mode - Interactive Vending Portal Mockup",
"Preview Mode: Login functionality is disabled.": "Preview Mode: Login functionality is disabled.",
"Previous": "Previous",
"Price": "Price",
"Price / Member": "Price / Member",
"Pricing Information": "Pricing Information",
"Pricing Slip No.": "Pricing Slip No.",
@ -1754,6 +1781,7 @@
"Recent Commands": "Recent Commands",
"Recent Login": "Recent Login",
"Recently reported errors or warnings in logs": "Recently reported errors or warnings in logs",
"Recommended when the machine physically dispensed but did not report, to reconcile cloud stock.": "Recommended when the machine physically dispensed but did not report, to reconcile cloud stock.",
"Recommended: 320x320 (Auto-cropped)": "Recommended: 320x320 (Auto-cropped)",
"Records": "Records",
"Refresh Now": "Refresh Now",
@ -1944,6 +1972,8 @@
"Select Target Slot": "Select Target Slot",
"Select Update Time (Optional)": "Select Update Time (Optional)",
"Select Warehouse": "Select Warehouse",
"Select a machine": "Select a machine",
"Select a machine to choose products": "Select a machine to choose products",
"Select a machine to copy settings from...": "Select a machine to copy settings from...",
"Select a machine to deep dive": "Select a machine to deep dive",
"Select a material to play on this machine": "Select a material to play on this machine",
@ -1953,6 +1983,8 @@
"Select date to sync data": "Select date to sync data",
"Select flavor...": "Select flavor...",
"Select future time...": "Select update time...",
"Select payment type": "Select payment type",
"Select slot / product": "Select slot / product",
"Select up to :max languages the machine can switch between. The first one is the default.": "Select up to :max languages the machine can switch between. The first one is the default.",
"Select...": "Select...",
"Selected": "Selected",
@ -2098,6 +2130,7 @@
"Submachine Exception": "Submachine Exception",
"Submachine Exception (B013)": "Submachine Exception (B013)",
"Submit Record": "Submit Record",
"Submitting...": "Submitting...",
"Subtotal": "Subtotal",
"Success": "Success",
"Super Admin": "Super Admin",
@ -2151,6 +2184,7 @@
"Target hardware flavor for this APK": "Target hardware flavor for this APK",
"Tax ID": "Tax ID",
"Tax ID (Optional)": "Tax ID (Optional)",
"Tax ID must be 8 digits": "Tax ID must be 8 digits",
"Tax System": "Tax System",
"Taxation System": "Taxation System",
"Temp Alert Range": "Temp Alert Range",
@ -2189,6 +2223,7 @@
"This is a system administrator role. Its name is locked to ensure system stability.": "This is a system administrator role. Its name is locked to ensure system stability.",
"This machine currently has no medicine in stock.": "This machine currently has no medicine in stock.",
"This machine has a pending command. Please wait.": "This machine has a pending command. Please wait.",
"This machine has e-invoice disabled; cannot issue": "This machine has e-invoice disabled; cannot issue",
"This machine has no ECPay invoice settings": "This machine has no ECPay invoice settings",
"This pharmacy pickup order can no longer be cancelled.": "This pharmacy pickup order can no longer be cancelled.",
"This role belongs to another company and cannot be assigned.": "This role belongs to another company and cannot be assigned.",

View File

@ -225,6 +225,7 @@
"Avatar updated successfully.": "アバターが更新されました。",
"Avg Cycle": "平均サイクル",
"Awaiting Pickup": "受取待ち",
"B2C (member carrier)": "一般B2C会員キャリア",
"Back to History": "履歴に戻る",
"Back to List": "一覧に戻る",
"Badge Settings": "バッジ設定",
@ -328,6 +329,7 @@
"Choose File": "ファイルを選択",
"Choose Logo": "ロゴを選択",
"Choose Overall BG": "全体背景画像を選択",
"Citizen certificate carrier": "自然人証明書キャリア",
"Clear": "クリア",
"Clear Abnormal Status": "異常状態のクリア",
"Clear Filter": "フィルターをクリア",
@ -365,6 +367,7 @@
"Company Level": "会社レベル",
"Company Name": "会社名",
"Company Settings": "会社グローバル設定",
"Company Tax ID": "統一番号",
"Complete movement history log": "完全な在庫変動履歴",
"Completed": "完了済み",
"Completed At": "完了日時",
@ -436,6 +439,7 @@
"Create Config": "設定作成",
"Create Machine": "機器追加",
"Create New Role": "新規ロール作成",
"Create Order": "注文を作成",
"Create Payment Config": "決済設定作成",
"Create Pharmacy Pickup Order": "薬品受取注文を作成",
"Create Product": "商品作成",
@ -520,6 +524,7 @@
"Date Range": "日付範囲",
"Day Before": "一昨日",
"Days": "日",
"Deduct stock": "在庫を減算",
"Default": "デフォルト",
"Default BG": "デフォルト背景",
"Default Card BG": "デフォルトカード背景",
@ -531,6 +536,7 @@
"Default Temp Limits": "デフォルト温度制御範囲",
"Default Temp Lower Limit (°C)": "デフォルト温度下限 (°C)",
"Default Temp Upper Limit (°C)": "デフォルト温度上限 (°C)",
"Defaults to now if empty": "空欄の場合は現在時刻",
"Define and manage security roles and permissions.": "セキュリティロールと権限を定義・管理します。",
"Define custom temperature limits or inherit from model": "カスタム温度制限を設定するか、機器モデルから継承します",
"Define new third-party payment parameters": "新しい外部決済パラメータを定義",
@ -612,7 +618,10 @@
"Displayed below Logo on the left (defaults to customer name).": "左側のロゴの下に表示されるメインタイトル(未設定の場合はデフォルトで顧客名になります)。",
"Displayed below main title on the left.": "左側のメインタイトルの下に表示されるサブタイトル。",
"Displaying": "表示中",
"Donation": "寄付",
"Donation invoices cannot be printed": "寄付の領収書は印刷できません",
"Donation love code": "寄付の愛心コード",
"Donation love code must be 3 to 7 digits": "寄付の愛心コードは3〜7桁の数字である必要があります",
"Done": "完了",
"Door Closed": "扉が閉まりました",
"Door Opened": "扉が開きました",
@ -764,11 +773,13 @@
"Export to Excel": "Excelエクスポート",
"External URL": "External URL",
"Failed": "失敗",
"Failed to create manual order": "手動補登に失敗しました",
"Failed to fetch machine data.": "機器データの取得に失敗しました。",
"Failed to get invoice print URL": "領収書印刷URLの取得に失敗しました",
"Failed to load permissions": "権限の読み込みに失敗しました",
"Failed to load preview": "プレビューの読み込みに失敗しました",
"Failed to load pricing": "価格設定の読み込みに失敗しました",
"Failed to load slots": "スロットの読み込みに失敗しました",
"Failed to load tab content": "タブ内容の読み込みに失敗しました",
"Failed to resolve issues.": "異常の解消に失敗しました。",
"Failed to save permissions.": "権限の保存に失敗しました。",
@ -880,6 +891,7 @@
"Flow ID / Slot / Time": "フローID / スロット / 時間",
"Flow ID / Status": "フローID / ステータス",
"Flow ID / Time": "フローID / 時間",
"For sales not reported due to disconnection. System administrators only.": "切断などで未報告の売上の補登用。システム管理者のみ。",
"Force End Session": "セッションを強制終了",
"Force end current session": "現在のセッションを強制終了",
"Forced Update": "強制アップデート",
@ -968,6 +980,9 @@
"Insufficient stock for :name (requested :qty, available :available).": ":name の在庫が不足しています(要求 :qty、利用可能 :available。",
"Insufficient stock for transfer": "在庫不足のため移動できません",
"Insufficient stored-value / e-wallet balance": "電子マネー/電子ウォレット残高不足",
"Invalid citizen digital certificate carrier": "自然人証明書キャリアの形式が無効です",
"Invalid invoice type": "インボイス種別が無効です",
"Invalid mobile barcode carrier": "モバイルバーコードキャリアの形式が無効です",
"Invalid personnel selection": "無効な担当者選択",
"Invalid status transition": "無効なステータス遷移",
"Inventory": "在庫概要",
@ -986,10 +1001,13 @@
"Invoice Number / Time": "領収書番号 / 時間",
"Invoice Status": "発行ステータス",
"Invoice Store ID": "請求書ストアID",
"Invoice Type": "インボイス種別",
"Issue a pharmacy pickup order, print the QR ticket, patient scans at the machine to dispense.": "薬品受取注文を発行し、QRチケットを印刷します。患者が機器でスキャンして払い出します。",
"Issue e-invoice": "電子インボイスを発行",
"Issue pharmacy pickup orders from the backend; patient scans QR to dispense.": "バックエンドで薬品受取注文を発行し、患者がQRをスキャンして払い出します。",
"Issued": "Issued",
"Issued At": "発行日時",
"Issued via ECPay in the background after submission.": "送信後にバックグラウンドでECPayにて発行。",
"Item List": "商品リスト",
"Items": "件",
"JKO Pay": "街口支付 (JKO Pay)",
@ -1049,6 +1067,7 @@
"Loading Data": "データを読み込み中",
"Loading failed": "読み込み失敗",
"Loading machines...": "機器を読み込み中...",
"Loading slots...": "スロットを読み込み中...",
"Location": "場所",
"Location found!": "座標を取得しました!",
"Location not found": "場所が見つかりません。ランドマーク名(例:花蓮県庁)を使用してみてください",
@ -1191,9 +1210,13 @@
"Manage your warehouses, including main and branch warehouses": "本庫と分庫を含む倉庫を管理",
"Management of operational parameters": "運用パラメータの管理",
"Management of operational parameters and models": "運用パラメータとモデルの管理",
"Manual": "手動",
"Manual Entry": "手動入力",
"Manual Order Entry": "手動受注入力",
"Manual Sync Ads": "手動広告同期",
"Manual Sync Products": "手動商品同期",
"Manual Sync Settings": "手動設定同期",
"Manual order created: :no": "手動補登を作成しました::no",
"Manufacturer": "製造メーカー",
"Marketing and Loyalty": "Marketing and Loyalty",
"Material Code": "資材コード",
@ -1265,6 +1288,7 @@
"Missing RelateNumber, cannot query": "Missing RelateNumber, cannot query",
"Mobile Pay": "モバイル決済",
"Mobile Payment": "モバイル決済",
"Mobile barcode carrier": "モバイルバーコードキャリア",
"Model": "機器モデル",
"Model Default": "モデルデフォルト",
"Model Name": "モデル名",
@ -1337,6 +1361,7 @@
"No history records": "履歴記録なし",
"No images uploaded": "画像がアップロードされていません",
"No invoice issued, cannot void": "No invoice issued, cannot void",
"No items added yet": "商品が未追加です",
"No location set": "場所が設定されていません",
"No login history yet": "ログイン履歴はまだありません",
"No logs found": "ログが見つかりません",
@ -1447,6 +1472,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 create manual orders": "手動補登はシステム管理者のみ可能です",
"Only system administrators can void invoices": "システム管理者のみが請求書を無効化できます",
"Only system roles can be assigned to platform administrative accounts.": "プラットフォーム管理アカウントにはシステムロールのみを割り当てることができます。",
"Operation Logs": "Operation Logs",
@ -1644,6 +1670,7 @@
"Preview Mode - Interactive Vending Portal Mockup": "プレビューモード - カスタムブランドログインページのモックアップ",
"Preview Mode: Login functionality is disabled.": "プレビューモード:ログイン機能は無効です。",
"Previous": "前へ",
"Price": "価格",
"Price / Member": "価格 / 会員価格",
"Pricing Information": "価格情報",
"Pricing Slip No.": "請求伝票番号",
@ -1754,6 +1781,7 @@
"Recent Commands": "最近のコマンド",
"Recent Login": "最近のログイン",
"Recently reported errors or warnings in logs": "最近のログにエラーまたは警告が報告されています",
"Recommended when the machine physically dispensed but did not report, to reconcile cloud stock.": "機台が実際に出庫済みだが未報告の場合、クラウド在庫を整合するため推奨。",
"Recommended: 320x320 (Auto-cropped)": "推奨サイズ: 320x320 (自動トリミング)",
"Records": "記録",
"Refresh Now": "今すぐ更新",
@ -1944,6 +1972,8 @@
"Select Target Slot": "対象のスロットを選択",
"Select Update Time (Optional)": "更新時間を選択してください(任意)",
"Select Warehouse": "倉庫を選択",
"Select a machine": "機台を選択",
"Select a machine to choose products": "商品を選ぶには先に機台を選択",
"Select a machine to copy settings from...": "設定のコピー元機器を選択...",
"Select a machine to deep dive": "詳細分析する機器を選択",
"Select a material to play on this machine": "この機器で再生する素材を選択",
@ -1953,6 +1983,8 @@
"Select date to sync data": "データを同期する日付を選択",
"Select flavor...": "Select flavor...",
"Select future time...": "配信日時を選択してください...",
"Select payment type": "支払方法を選択",
"Select slot / product": "スロット / 商品を選択",
"Select up to :max languages the machine can switch between. The first one is the default.": "機台が切り替え可能な言語を選択してください(最大 :max 種)。最初の言語がデフォルトになります。",
"Select...": "選択してください...",
"Selected": "選択中",
@ -2098,6 +2130,7 @@
"Submachine Exception": "下位機ハードウェア異常",
"Submachine Exception (B013)": "下位機/商品ラック異常 (B013)",
"Submit Record": "記録を提出",
"Submitting...": "送信中...",
"Subtotal": "小計",
"Success": "成功",
"Super Admin": "特権管理者",
@ -2151,6 +2184,7 @@
"Target hardware flavor for this APK": "Target hardware flavor for this APK",
"Tax ID": "統一企業番号",
"Tax ID (Optional)": "統一企業番号 (任意)",
"Tax ID must be 8 digits": "統一番号は8桁の数字である必要があります",
"Tax System": "税務システム",
"Taxation System": "税務システム",
"Temp Alert Range": "アラート温度範囲",
@ -2189,6 +2223,7 @@
"This is a system administrator role. Its name is locked to ensure system stability.": "これはシステム管理者ロールです。システムの安定性を確保するため、名前はロックされています。",
"This machine currently has no medicine in stock.": "この機器には現在、払い出し可能な薬品の在庫がありません。",
"This machine has a pending command. Please wait.": "この機器には実行中のコマンドがあります。しばらくお待ちください。",
"This machine has e-invoice disabled; cannot issue": "この機台は電子インボイスが無効のため発行できません",
"This machine has no ECPay invoice settings": "This machine has no ECPay invoice settings",
"This pharmacy pickup order can no longer be cancelled.": "この薬品受取注文はこれ以上取り消せません。",
"This role belongs to another company and cannot be assigned.": "このロールは別の会社に属しているため、割り当てることができません。",

View File

@ -225,6 +225,7 @@
"Avatar updated successfully.": "頭像已成功更新。",
"Avg Cycle": "平均週期",
"Awaiting Pickup": "待領取",
"B2C (member carrier)": "一般 B2C會員載具",
"Back to History": "返回紀錄",
"Back to List": "返回列表",
"Badge Settings": "識別證",
@ -328,6 +329,7 @@
"Choose File": "選擇檔案",
"Choose Logo": "選擇公司 Logo",
"Choose Overall BG": "選擇大背景圖",
"Citizen certificate carrier": "自然人憑證載具",
"Clear": "清除",
"Clear Abnormal Status": "清除異常狀態",
"Clear Filter": "清除篩選",
@ -365,6 +367,7 @@
"Company Level": "公司層級",
"Company Name": "公司名稱",
"Company Settings": "公司全域設定",
"Company Tax ID": "統一編號",
"Complete movement history log": "完整的庫存異動歷程",
"Completed": "已完成",
"Completed At": "完成時間",
@ -436,6 +439,7 @@
"Create Config": "建立配置",
"Create Machine": "新增機台",
"Create New Role": "建立新角色",
"Create Order": "建立訂單",
"Create Payment Config": "建立金流配置",
"Create Pharmacy Pickup Order": "建立領藥單",
"Create Product": "建立商品",
@ -520,6 +524,7 @@
"Date Range": "日期區間",
"Day Before": "前日",
"Days": "天",
"Deduct stock": "扣除庫存",
"Default": "預設",
"Default BG": "預設大背景",
"Default Card BG": "預設卡片背景",
@ -531,6 +536,7 @@
"Default Temp Limits": "預設溫控範圍",
"Default Temp Lower Limit (°C)": "預設溫度下限 (°C)",
"Default Temp Upper Limit (°C)": "預設溫度上限 (°C)",
"Defaults to now if empty": "留空則為現在時間",
"Define and manage security roles and permissions.": "定義並管理系統安全角色與權限。",
"Define custom temperature limits or inherit from model": "設定自訂溫度限制,或繼承自機台型號",
"Define new third-party payment parameters": "定義新的第三方支付參數",
@ -612,7 +618,10 @@
"Displayed below Logo on the left (defaults to customer name).": "顯示於左側 Logo 下方的主標題(未設定則預設帶入客戶名稱)。",
"Displayed below main title on the left.": "顯示於左側主標題下方的副標題/英文名稱。",
"Displaying": "目前顯示",
"Donation": "捐贈",
"Donation invoices cannot be printed": "捐贈發票無法列印",
"Donation love code": "捐贈愛心碼",
"Donation love code must be 3 to 7 digits": "捐贈愛心碼須為 3 至 7 碼數字",
"Done": "已完成",
"Door Closed": "機門已關閉",
"Door Opened": "機門已開啟",
@ -764,11 +773,13 @@
"Export to Excel": "匯出成 Excel",
"External URL": "外連 URL",
"Failed": "失敗",
"Failed to create manual order": "手動補單失敗",
"Failed to fetch machine data.": "無法取得機台資料。",
"Failed to get invoice print URL": "取得發票列印網址失敗",
"Failed to load permissions": "載入權限失敗",
"Failed to load preview": "載入預覽失敗",
"Failed to load pricing": "載入定價失敗",
"Failed to load slots": "載入貨道失敗",
"Failed to load tab content": "載入分頁內容失敗",
"Failed to resolve issues.": "排除異常失敗。",
"Failed to save permissions.": "無法儲存權限設定。",
@ -880,6 +891,7 @@
"Flow ID / Slot / Time": "流水號 / 貨道 / 時間",
"Flow ID / Status": "流水號 / 狀態",
"Flow ID / Time": "流水號 / 時間",
"For sales not reported due to disconnection. System administrators only.": "用於因斷線未上報的銷售。僅限系統管理員。",
"Force End Session": "強制結束當前會話",
"Force end current session": "強制結束目前的連線",
"Forced Update": "強制更新",
@ -968,6 +980,9 @@
"Insufficient stock for :name (requested :qty, available :available).": ":name 庫存不足(需求 :qty可用 :available。",
"Insufficient stock for transfer": "庫存不足,無法調撥",
"Insufficient stored-value / e-wallet balance": "電票/電子錢包餘額不足",
"Invalid citizen digital certificate carrier": "自然人憑證載具格式錯誤",
"Invalid invoice type": "發票類型無效",
"Invalid mobile barcode carrier": "手機條碼載具格式錯誤",
"Invalid personnel selection": "無效的人員選擇",
"Invalid status transition": "無效的狀態轉換",
"Inventory": "庫存概覽",
@ -986,10 +1001,13 @@
"Invoice Number / Time": "發票號碼 / 時間",
"Invoice Status": "發票開立狀態",
"Invoice Store ID": "發票商店代號",
"Invoice Type": "發票類型",
"Issue a pharmacy pickup order, print the QR ticket, patient scans at the machine to dispense.": "開立領藥單、列印 QR 領藥單,病人到機台掃碼即可出貨。",
"Issue e-invoice": "開立電子發票",
"Issue pharmacy pickup orders from the backend; patient scans QR to dispense.": "由後台開立領藥單,病人到機台掃 QR 出貨。",
"Issued": "已開立",
"Issued At": "開立時間",
"Issued via ECPay in the background after submission.": "送出後於背景透過綠界開立。",
"Item List": "商品清單",
"Items": "筆",
"JKO Pay": "街口支付",
@ -1049,6 +1067,7 @@
"Loading Data": "載入資料中",
"Loading failed": "載入失敗",
"Loading machines...": "正在載入機台...",
"Loading slots...": "載入貨道中...",
"Location": "位置",
"Location found!": "已成功獲取座標!",
"Location not found": "找不到位置,您可以嘗試使用地標名稱(例如:花蓮縣政府)",
@ -1191,9 +1210,13 @@
"Manage your warehouses, including main and branch warehouses": "管理您的倉庫,包含總倉與分倉設定",
"Management of operational parameters": "機台運作參數管理",
"Management of operational parameters and models": "管理運作參數與型號",
"Manual": "手動",
"Manual Entry": "手動補單",
"Manual Order Entry": "手動補單",
"Manual Sync Ads": "手動同步機台廣告",
"Manual Sync Products": "手動同步商品",
"Manual Sync Settings": "手動同步系統設定",
"Manual order created: :no": "已建立手動補單::no",
"Manufacturer": "製造商",
"Marketing and Loyalty": "行銷與點數",
"Material Code": "物料代碼",
@ -1265,6 +1288,7 @@
"Missing RelateNumber, cannot query": "缺少 RelateNumber無法查詢",
"Mobile Pay": "手機支付",
"Mobile Payment": "手機支付",
"Mobile barcode carrier": "手機條碼載具",
"Model": "機台型號",
"Model Default": "型號預設",
"Model Name": "型號名稱",
@ -1337,6 +1361,7 @@
"No history records": "尚無歷史紀錄",
"No images uploaded": "尚未上傳照片",
"No invoice issued, cannot void": "尚未開立發票,無法作廢",
"No items added yet": "尚未新增商品",
"No location set": "尚未設定位置",
"No login history yet": "尚無登入紀錄",
"No logs found": "暫無相關日誌",
@ -1447,6 +1472,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 create manual orders": "僅系統管理員可手動補單",
"Only system administrators can void invoices": "僅系統管理員可作廢發票",
"Only system roles can be assigned to platform administrative accounts.": "僅系統角色可指派給平台管理帳號。",
"Operation Logs": "操作紀錄",
@ -1644,6 +1670,7 @@
"Preview Mode - Interactive Vending Portal Mockup": "預覽模式 - 客製化品牌登入頁面模擬",
"Preview Mode: Login functionality is disabled.": "預覽模式:不提供帳號登入功能。",
"Previous": "上一頁",
"Price": "價格",
"Price / Member": "售價 / 會員價",
"Pricing Information": "價格資訊",
"Pricing Slip No.": "批價單號",
@ -1754,6 +1781,7 @@
"Recent Commands": "最近指令",
"Recent Login": "最近登入",
"Recently reported errors or warnings in logs": "近期日誌中有錯誤或警告回報",
"Recommended when the machine physically dispensed but did not report, to reconcile cloud stock.": "當機台實際已出貨但未上報時建議勾選,以校正雲端庫存。",
"Recommended: 320x320 (Auto-cropped)": "建議尺寸320x320 (系統將自動裁切)",
"Records": "筆",
"Refresh Now": "立即更新",
@ -1944,6 +1972,8 @@
"Select Target Slot": "選擇目標貨道",
"Select Update Time (Optional)": "選擇更新時間(選填)",
"Select Warehouse": "選擇倉庫",
"Select a machine": "選擇機台",
"Select a machine to choose products": "請先選擇機台以挑選商品",
"Select a machine to copy settings from...": "選擇要複製設定的來源機台...",
"Select a machine to deep dive": "請選擇機台以開始深度分析",
"Select a material to play on this machine": "選擇要在此機台播放的素材",
@ -1953,6 +1983,8 @@
"Select date to sync data": "選擇日期以同步數據",
"Select flavor...": "選擇風味...",
"Select future time...": "選擇更新時間",
"Select payment type": "選擇付款方式",
"Select slot / product": "選擇貨道 / 商品",
"Select up to :max languages the machine can switch between. The first one is the default.": "選擇機台可切換的語系(最多 :max 種),第一個為預設語系。",
"Select...": "請選擇...",
"Selected": "已選擇",
@ -2098,6 +2130,7 @@
"Submachine Exception": "下位機硬體異常",
"Submachine Exception (B013)": "下位機/貨道異常 (B013)",
"Submit Record": "提交紀錄",
"Submitting...": "送出中...",
"Subtotal": "小計",
"Success": "成功",
"Super Admin": "超級管理員",
@ -2151,6 +2184,7 @@
"Target hardware flavor for this APK": "此 APK 的目標硬體風味",
"Tax ID": "統一編號",
"Tax ID (Optional)": "統一編號 (選填)",
"Tax ID must be 8 digits": "統一編號須為 8 碼數字",
"Tax System": "稅務系統",
"Taxation System": "稅務系統",
"Temp Alert Range": "告警溫度範圍",
@ -2189,6 +2223,7 @@
"This is a system administrator role. Its name is locked to ensure system stability.": "這是系統管理員角色,名稱已鎖定以確保系統穩定性。",
"This machine currently has no medicine in stock.": "此機台目前無可領藥品庫存。",
"This machine has a pending command. Please wait.": "此機台已有指令正在執行,請稍後。",
"This machine has e-invoice disabled; cannot issue": "此機台未啟用電子發票,無法開立",
"This machine has no ECPay invoice settings": "此機台未設定綠界電子發票",
"This pharmacy pickup order can no longer be cancelled.": "此領藥單已無法作廢。",
"This role belongs to another company and cannot be assigned.": "此角色屬於其他公司,無法指派。",

View File

@ -2,7 +2,8 @@
@section('content')
<div class="space-y-2 pb-20" x-data="salesCenter()"
@ajax:navigate.window.prevent="fetchTabData(activeTab, $event.detail.url)">
@ajax:navigate.window.prevent="fetchTabData(activeTab, $event.detail.url)"
@manual-order-created.window="switchTab('orders'); fetchTabData('orders')">
{{-- Page Header --}}
<x-page-header :title="$title" :subtitle="$description" />
@ -107,6 +108,11 @@
<x-confirm-modal alpineVar="showReissueModal" confirmAction="submitInvoiceAction()" :title="__('Re-issue')"
:message="__('Re-issue this invoice?')" :confirmText="__('Re-issue')" :cancelText="__('Cancel')"
iconType="info" confirmColor="sky" />
{{-- 手動補單彈窗:僅系統管理員可見 / 使用(後端再次把關) --}}
@if(auth()->user()->isSystemAdmin())
@include('admin.sales.partials.manual-order-modal')
@endif
</div>
@endsection
@ -374,5 +380,230 @@ function salesCenter() {
}
}
}
function manualOrderForm(slotSelectConfig) {
return {
slotSelectConfig: slotSelectConfig,
show: false,
machineId: '',
taxInvoiceEnabled: false,
slots: [],
loadingSlots: false,
selectedSlot: '',
quantity: 1,
price: 0,
paymentType: '',
occurredAt: '',
deductStock: true,
issueInvoice: false,
invoiceType: 'b2c',
invoiceValue: '',
remark: '',
submitting: false,
_fp: null,
open() {
this.resetForm();
this.show = true;
this.$nextTick(() => {
this.initPicker();
// 重置/初始化 Preline 搜尋下拉的顯示值
this.syncSelect('manual-machine', ' ');
this.syncSelect('manual-payment', ' ');
this.syncSelect('manual-invoice-type', 'b2c');
if (window.HSStaticMethods?.autoInit) window.HSStaticMethods.autoInit();
this.updateSlotSelect();
});
},
resetForm() {
this.machineId = '';
this.taxInvoiceEnabled = false;
this.slots = [];
this.selectedSlot = '';
this.quantity = 1;
this.price = 0;
this.paymentType = '';
this.occurredAt = '';
this.deductStock = true;
this.issueInvoice = false;
this.invoiceType = 'b2c';
this.invoiceValue = '';
this.remark = '';
},
initPicker() {
if (this._fp) { this._fp.destroy(); this._fp = null; }
if (window.flatpickr && this.$refs.occurredAt) {
this._fp = flatpickr(this.$refs.occurredAt, {
dateFormat: 'Y-m-d H:i', enableTime: true, time_24hr: true, disableMobile: true,
locale: '{{ app()->getLocale() == 'zh_TW' ? 'zh_tw' : 'en' }}',
});
}
},
async onMachineChange() {
this.slots = [];
this.selectedSlot = '';
this.price = 0;
this.taxInvoiceEnabled = false;
this.issueInvoice = false;
this.updateSlotSelect();
if (!this.machineId) return;
this.loadingSlots = true;
try {
const res = await fetch(`{{ route('admin.sales.manual.machine-slots') }}?machine_id=${this.machineId}`, {
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
});
const data = await res.json();
if (data.success) {
this.slots = data.slots || [];
this.taxInvoiceEnabled = !!data.tax_invoice_enabled;
}
} catch (e) {
console.error('load slots failed', e);
window.showToast?.('{{ __('Failed to load slots') }}', 'error');
} finally {
this.loadingSlots = false;
this.updateSlotSelect();
}
},
// 依 slots 重建貨道/商品搜尋下拉(沿用全站 Preline searchable-select 樣式)
updateSlotSelect() {
this.$nextTick(() => {
const wrapper = document.getElementById('manual-slot-wrapper');
if (!wrapper) return;
wrapper.querySelectorAll('select').forEach(s => {
try { window.HSSelect?.getInstance(s)?.destroy(); } catch (e) {}
});
wrapper.innerHTML = '';
if (!this.machineId) {
const dummy = document.createElement('div');
dummy.className = 'luxury-input opacity-50 cursor-not-allowed flex items-center justify-between';
dummy.innerHTML = `<span class="text-slate-400">{{ __('Select a machine to choose products') }}</span><svg class="size-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 9-7 7-7-7"/></svg>`;
wrapper.appendChild(dummy);
return;
}
const selectEl = document.createElement('select');
selectEl.id = 'manual-slot-' + Date.now();
selectEl.className = 'hidden';
const cleanConfig = JSON.parse(JSON.stringify(this.slotSelectConfig), (key, value) => {
return typeof value === 'string' ? value.replace(/\r?\n|\r/g, ' ').trim() : value;
});
selectEl.setAttribute('data-hs-select', JSON.stringify(cleanConfig));
const ph = document.createElement('option');
ph.value = '';
ph.textContent = "{{ __('Select slot / product') }}";
ph.setAttribute('data-title', "{{ __('Select slot / product') }}");
selectEl.appendChild(ph);
this.slots.forEach(slot => {
const opt = document.createElement('option');
opt.value = slot.slot_no;
const text = `${slot.slot_no} · ${slot.product_name} ({{ __('Stock') }}: ${slot.stock})`;
opt.textContent = text;
opt.setAttribute('data-title', text);
if (String(this.selectedSlot) === String(slot.slot_no)) opt.selected = true;
selectEl.appendChild(opt);
});
wrapper.appendChild(selectEl);
selectEl.addEventListener('change', e => this.onSlotPicked(e.target.value));
if (window.HSStaticMethods?.autoInit) window.HSStaticMethods.autoInit(['select']);
});
},
onSlotPicked(slotNo) {
this.selectedSlot = slotNo;
const slot = this.slots.find(s => String(s.slot_no) === String(slotNo));
this.price = slot ? Number(slot.price) || 0 : 0;
},
// 同步 Preline 下拉顯示值(沿用取貨碼模式)
syncSelect(id, value) {
const el = document.getElementById(id);
if (!el) return;
const v = (value !== undefined && value !== null && value.toString().trim() !== '') ? value.toString() : ' ';
el.value = v;
try { window.HSSelect?.getInstance(el)?.setValue(v); } catch (e) {}
},
get total() {
return Math.round((Number(this.price) || 0) * (Number(this.quantity) || 0));
},
invoiceValueLabel() {
return {
taxid: '{{ __('Company Tax ID') }}',
mobile: '{{ __('Mobile barcode carrier') }}',
citizen: '{{ __('Citizen certificate carrier') }}',
donation: '{{ __('Donation love code') }}',
}[this.invoiceType] || '';
},
validate() {
if (!this.machineId) { window.showToast?.('{{ __('Select a machine') }}', 'error'); return false; }
if (!this.selectedSlot) { window.showToast?.('{{ __('Select slot / product') }}', 'error'); return false; }
if (!this.paymentType) { window.showToast?.('{{ __('Select payment type') }}', 'error'); return false; }
if (!(Number(this.quantity) > 0)) { return false; }
return true;
},
async submit() {
if (this.submitting || !this.validate()) return;
const slot = this.slots.find(s => String(s.slot_no) === String(this.selectedSlot));
if (!slot) { window.showToast?.('{{ __('Select slot / product') }}', 'error'); return; }
this.submitting = true;
try {
const payload = {
machine_id: this.machineId,
payment_type: this.paymentType,
occurred_at: this.occurredAt || null,
deduct_stock: this.deductStock,
issue_invoice: this.taxInvoiceEnabled && this.issueInvoice,
invoice_type: this.invoiceType,
invoice_value: this.invoiceValue,
remark: this.remark,
items: [{
product_id: slot.product_id,
slot_no: slot.slot_no,
price: this.price,
quantity: this.quantity,
}],
};
const res = await fetch('{{ route('admin.sales.manual.store') }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
},
body: JSON.stringify(payload),
});
const data = await res.json();
if (res.ok && data.success) {
window.showToast?.(data.message || 'OK', 'success');
this.show = false;
window.dispatchEvent(new CustomEvent('manual-order-created'));
} else {
window.showToast?.(data.message || '{{ __('Failed to create manual order') }}', 'error');
}
} catch (e) {
console.error('submit manual order failed', e);
window.showToast?.('{{ __('Failed to create manual order') }}', 'error');
} finally {
this.submitting = false;
}
},
};
}
</script>
@endsection

View File

@ -0,0 +1,223 @@
@php
// 貨道/商品搜尋下拉的 Preline 設定(沿用全站 searchable-select 樣式)
$manualSlotSelectConfig = [
"placeholder" => __('Select slot / product'),
"hasSearch" => true,
"searchPlaceholder" => __('Search...'),
"isHidePlaceholder" => false,
"searchClasses" => "block w-[calc(100%-16px)] mx-2 py-2 px-3 text-sm border-slate-200 dark:border-white/10 rounded-lg focus:border-cyan-500 focus:ring-cyan-500 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200 placeholder:text-slate-400 dark:placeholder:text-slate-500",
"searchWrapperClasses" => "sticky top-0 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md p-2 z-10",
"toggleClasses" => "hs-select-toggle luxury-select-toggle",
"toggleTemplate" => '<button type="button" aria-expanded="false"><span class="me-2" data-icon></span><span class="text-slate-800 dark:text-slate-200" data-title></span><div class="ms-auto"><svg class="size-4 text-slate-400 transition-transform duration-300" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg></div></button>',
"dropdownClasses" => "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10 rounded-xl shadow-[0_20px_50px_rgba(0,0,0,0.3)] mt-2 z-[200] animate-luxury-in",
"optionClasses" => "hs-select-option py-2.5 px-3 mb-0.5 text-sm text-slate-800 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-cyan-500/10 dark:hover:text-cyan-400 rounded-lg flex items-center justify-between transition-all duration-300",
"optionTemplate" => '<div class="flex items-center justify-between w-full"><span data-title></span><span class="hs-select-active-indicator hidden text-cyan-500"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg></span></div>',
];
@endphp
{{-- 手動補單彈窗(機台斷線/漏報時人工補登銷售紀錄)— 僅系統管理員 --}}
<div x-data="manualOrderForm(@js($manualSlotSelectConfig))" x-cloak
@open-manual-order.window="open()"
@keydown.window.escape="show = false">
<div class="fixed inset-0 z-[110] overflow-y-auto" x-show="show">
{{-- Backdrop --}}
<div class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity"
x-show="show"
x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
@click="show = false"></div>
{{-- Dialog --}}
<div class="flex min-h-full items-center justify-center p-4">
<div class="relative w-full max-w-2xl transform transition-all"
x-show="show"
x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-6 scale-95" x-transition:enter-end="opacity-100 translate-y-0 scale-100"
x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0 scale-95">
<div class="luxury-card rounded-3xl bg-white dark:bg-slate-900 shadow-2xl border border-slate-100 dark:border-slate-800 overflow-hidden">
{{-- Header --}}
<div class="flex items-start justify-between gap-4 px-6 sm:px-8 pt-6 pb-4 border-b border-slate-100 dark:border-slate-800">
<div>
<h3 class="text-lg font-black text-slate-800 dark:text-slate-100 tracking-tight flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-cyan-500"></span>
{{ __('Manual Order Entry') }}
</h3>
<p class="text-[11px] font-bold text-slate-400 dark:text-slate-500 mt-1 tracking-wide">
{{ __('For sales not reported due to disconnection. System administrators only.') }}
</p>
</div>
<button type="button" @click="show = false"
class="p-2 rounded-xl text-slate-400 hover:text-slate-600 hover:bg-slate-100 dark:hover:bg-slate-800 transition-all">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
{{-- Body --}}
<div class="px-6 sm:px-8 py-6 space-y-5 max-h-[68vh] overflow-y-auto">
{{-- 機台 + 發生時間 --}}
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<label class="block text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest pl-1">{{ __('Machine') }} <span class="text-rose-500">*</span></label>
<x-searchable-select name="manual_machine_id" id="manual-machine"
:placeholder="__('Select a machine')"
x-model="machineId" @change="machineId = $event.target.value; onMachineChange()">
@foreach($machines as $m)
<option value="{{ $m->id }}" data-title="{{ $m->name }} ({{ $m->serial_no }})">{{ $m->name }} ({{ $m->serial_no }})</option>
@endforeach
</x-searchable-select>
</div>
<div class="space-y-2">
<label class="block text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest pl-1">{{ __('Transaction Time') }}</label>
<input type="text" x-ref="occurredAt" x-model="occurredAt"
placeholder="{{ __('Defaults to now if empty') }}"
class="luxury-input w-full text-sm py-2.5 px-4 cursor-pointer">
</div>
</div>
{{-- 貨道/商品(搜尋式,依機台動態載入) --}}
<div class="space-y-2">
<label class="block text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest pl-1">{{ __('Select slot / product') }} <span class="text-rose-500">*</span></label>
<div class="relative">
<div id="manual-slot-wrapper" class="relative">{{-- Rebuilt by Alpine --}}</div>
<div x-show="loadingSlots" class="absolute right-12 top-1/2 -translate-y-1/2 z-10">
<x-luxury-spinner show="loadingSlots" />
</div>
</div>
</div>
{{-- 付款方式 + 價格 --}}
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<label class="block text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest pl-1">{{ __('Payment Type') }} <span class="text-rose-500">*</span></label>
<x-searchable-select name="manual_payment_type" id="manual-payment"
:placeholder="__('Select payment type')"
x-model="paymentType" @change="paymentType = $event.target.value">
@foreach($paymentTypes as $ptVal => $ptLabel)
<option value="{{ $ptVal }}" data-title="{{ $ptLabel }}">{{ $ptLabel }}</option>
@endforeach
</x-searchable-select>
</div>
<div class="space-y-2">
<label class="block text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest pl-1">{{ __('Price') }} <span class="text-rose-500">*</span></label>
<div class="flex items-center h-12 rounded-2xl border border-slate-200/50 dark:border-slate-700/50 bg-slate-50/50 dark:bg-slate-900/50 overflow-hidden w-full group focus-within:ring-2 focus-within:ring-cyan-500/20 transition-all">
<button type="button" @click="price = Math.max(0, (Number(price)||0) - 1)"
class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M20 12H4"/></svg>
</button>
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700/50"></div>
<div class="flex-1 min-w-0 flex items-center justify-center">
<span class="text-slate-400 font-black mr-0.5">$</span>
<input type="text" inputmode="decimal" x-model.number="price"
class="w-full bg-transparent border-none text-center text-lg font-black text-slate-800 dark:text-white focus:ring-0 p-0">
</div>
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700/50"></div>
<button type="button" @click="price = (Number(price)||0) + 1"
class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 5v14M5 12h14"/></svg>
</button>
</div>
</div>
</div>
{{-- 數量Luxury Counter +/- --}}
<div class="space-y-2">
<label class="block text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest pl-1">{{ __('Qty') }}</label>
<div class="flex flex-col sm:flex-row items-center gap-6">
<div class="flex items-center h-12 rounded-2xl border border-slate-200/50 dark:border-slate-700/50 bg-slate-50/50 dark:bg-slate-900/50 overflow-hidden flex-1 min-w-[200px] w-full group focus-within:ring-2 focus-within:ring-cyan-500/20 transition-all">
<button type="button" @click="quantity > 1 ? quantity-- : 1"
class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M20 12H4"/></svg>
</button>
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700/50"></div>
<div class="flex-1 min-w-[60px]">
<input type="text" x-model.number="quantity" readonly
class="w-full bg-transparent border-none text-center text-lg font-black text-slate-800 dark:text-white focus:ring-0 p-0">
</div>
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700/50"></div>
<button type="button" @click="quantity < 999 ? quantity++ : 999"
class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 5v14M5 12h14"/></svg>
</button>
</div>
<div class="flex items-center gap-1.5">
<template x-for="val in [1, 2, 3, 5]" :key="val">
<button type="button" @click="quantity = val"
class="w-10 h-10 rounded-xl border border-slate-200 dark:border-slate-700 flex items-center justify-center text-[11px] font-black transition-all shrink-0"
:class="quantity == val ? 'bg-cyan-500 text-white border-cyan-500 shadow-lg shadow-cyan-500/20' : 'bg-white dark:bg-slate-800 text-slate-500 hover:border-cyan-500/50'">
<span x-text="val"></span>
</button>
</template>
</div>
</div>
</div>
{{-- 小計 --}}
<div class="flex items-center justify-end gap-2 pt-1">
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Total') }}</span>
<span class="text-lg font-black text-slate-800 dark:text-white" x-text="`$${total}`"></span>
</div>
{{-- 扣庫存 --}}
<label class="flex items-start gap-3 p-3 rounded-2xl bg-slate-50 dark:bg-slate-800/40 border border-slate-100 dark:border-slate-800 cursor-pointer">
<input type="checkbox" x-model="deductStock" class="mt-0.5 rounded border-slate-300 text-cyan-500 focus:ring-cyan-500">
<span>
<span class="block text-sm font-bold text-slate-700 dark:text-slate-200">{{ __('Deduct stock') }}</span>
<span class="block text-[11px] text-slate-400 mt-0.5">{{ __('Recommended when the machine physically dispensed but did not report, to reconcile cloud stock.') }}</span>
</span>
</label>
{{-- 開立電子發票(僅機台啟用電子發票時顯示) --}}
<div x-show="taxInvoiceEnabled" x-cloak>
<label class="flex items-start gap-3 p-3 rounded-2xl bg-slate-50 dark:bg-slate-800/40 border border-slate-100 dark:border-slate-800 cursor-pointer">
<input type="checkbox" x-model="issueInvoice" class="mt-0.5 rounded border-slate-300 text-cyan-500 focus:ring-cyan-500">
<span class="flex-1">
<span class="block text-sm font-bold text-slate-700 dark:text-slate-200">{{ __('Issue e-invoice') }}</span>
<span class="block text-[11px] text-slate-400 mt-0.5">{{ __('Issued via ECPay in the background after submission.') }}</span>
</span>
</label>
<div x-show="issueInvoice" x-cloak class="grid grid-cols-1 sm:grid-cols-2 gap-3 mt-3 pl-1">
<div class="space-y-2">
<label class="block text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest pl-1">{{ __('Invoice Type') }}</label>
<x-searchable-select name="manual_invoice_type" id="manual-invoice-type"
:has-search="false" x-model="invoiceType" @change="invoiceType = $event.target.value">
<option value="b2c" data-title="{{ __('B2C (member carrier)') }}">{{ __('B2C (member carrier)') }}</option>
<option value="taxid" data-title="{{ __('Company Tax ID') }}">{{ __('Company Tax ID') }}</option>
<option value="mobile" data-title="{{ __('Mobile barcode carrier') }}">{{ __('Mobile barcode carrier') }}</option>
<option value="citizen" data-title="{{ __('Citizen certificate carrier') }}">{{ __('Citizen certificate carrier') }}</option>
<option value="donation" data-title="{{ __('Donation') }}">{{ __('Donation') }}</option>
</x-searchable-select>
</div>
<div class="space-y-2" x-show="invoiceType !== 'b2c'">
<label class="block text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest pl-1" x-text="invoiceValueLabel()"></label>
<input type="text" x-model="invoiceValue" class="luxury-input w-full text-sm py-2.5 px-4">
</div>
</div>
</div>
{{-- 備註 --}}
<div class="space-y-2">
<label class="block text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest pl-1">{{ __('Remark') }}</label>
<textarea x-model="remark" rows="2" maxlength="1000" placeholder="{{ __('Enter remark (optional)') }}"
class="luxury-input w-full text-sm py-2.5 px-4 resize-none"></textarea>
</div>
</div>
{{-- Footer --}}
<div class="flex items-center justify-end gap-3 px-6 sm:px-8 py-4 border-t border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/40">
<button type="button" @click="show = false"
class="px-5 py-2.5 rounded-xl text-sm font-bold text-slate-500 hover:text-slate-700 hover:bg-slate-100 dark:hover:bg-slate-800 transition-all">
{{ __('Cancel') }}
</button>
<button type="button" @click="submit()" :disabled="submitting"
class="px-6 py-2.5 rounded-xl text-sm font-black text-white bg-cyan-500 hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 transition-all active:scale-95 disabled:opacity-40 disabled:cursor-not-allowed disabled:active:scale-100 flex items-center gap-2">
<svg x-show="submitting" class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
<span x-text="submitting ? '{{ __('Submitting...') }}' : '{{ __('Create Order') }}'"></span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -101,6 +101,18 @@
</div>
<div class="flex items-center gap-2 ml-auto lg:ml-0 shrink-0">
@if(auth()->user()->isSystemAdmin())
{{-- 手動補單:機台斷線/漏報時人工補登銷售紀錄(僅系統管理員) --}}
<button type="button"
onclick="window.dispatchEvent(new CustomEvent('open-manual-order'))"
class="px-3.5 py-2.5 rounded-xl bg-slate-800 dark:bg-slate-700 text-white hover:bg-slate-900 dark:hover:bg-slate-600 shadow-lg transition-all active:scale-95 flex items-center gap-2 text-xs font-bold whitespace-nowrap"
title="{{ __('Manual Order Entry') }}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
</svg>
<span class="hidden sm:inline">{{ __('Manual Entry') }}</span>
</button>
@endif
<button type="submit"
class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 transition-all active:scale-95"
title="{{ __('Search') }}">
@ -223,9 +235,14 @@
class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors cursor-pointer">
{{ $order->order_no }}
</span>
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">
<div class="flex items-center gap-2 mt-1">
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">
{{ $order->created_at->format('Y-m-d H:i:s') }}
</span>
@if($order->is_manual)
<span class="px-1.5 py-0.5 rounded bg-amber-500/10 border border-amber-500/20 text-[9px] font-black text-amber-600 dark:text-amber-400 uppercase tracking-widest">{{ __('Manual') }}</span>
@endif
</div>
</div>
</td>
<td class="px-6 py-6">
@ -404,6 +421,9 @@
<h3 @click="openDetail({{ $order->id }})"
class="text-base font-extrabold text-slate-800 dark:text-slate-100 truncate hover:text-cyan-600 dark:hover:text-cyan-400 transition-colors tracking-tight cursor-pointer">
{{ $order->order_no }}
@if($order->is_manual)
<span class="px-1.5 py-0.5 rounded bg-amber-500/10 border border-amber-500/20 text-[9px] font-black text-amber-600 dark:text-amber-400 uppercase tracking-widest align-middle">{{ __('Manual') }}</span>
@endif
</h3>
<p
class="text-xs font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest truncate">

View File

@ -146,6 +146,10 @@ Route::middleware(['auth', 'auth.session', 'verified', 'tenant.access'])->prefix
Route::prefix('sales')->name('sales.')->group(function () {
Route::get('/', [App\Http\Controllers\Admin\SalesController::class, 'index'])->name('index');
// 手動補單(機台斷線/漏報時人工補登銷售紀錄)— 限系統管理員Controller 內把關)
Route::get('/manual/machine-slots', [App\Http\Controllers\Admin\SalesController::class, 'manualMachineSlots'])->name('manual.machine-slots');
Route::post('/manual', [App\Http\Controllers\Admin\SalesController::class, 'storeManualOrder'])->name('manual.store');
// 電子發票對帳/補開/作廢/列印
Route::post('/invoices/{invoice}/print', [App\Http\Controllers\Admin\SalesController::class, 'printInvoice'])->name('invoices.print');
Route::post('/invoices/{invoice}/reconcile', [App\Http\Controllers\Admin\SalesController::class, 'reconcileInvoice'])->name('invoices.reconcile');