[FEAT] 銷售紀錄手動補單功能(機台斷線/漏報時人工補登)
1. 新增 TransactionService::createManualOrder():複用既有 recordDispense()(扣庫存)與 recordInvoice()+IssueInvoiceJob(後台開發票),訂單標記 order_type=manual、created_by 操作者,flow_id 以 MAN 前綴與機台上報區隔並天然避開唯一鍵衝突。 2. Order 新增 TYPE_MANUAL 常數與 is_manual 存取器;created_at/machine_time 對齊實際發生時間,使銷售報表落在正確日期。 3. SalesController 新增 storeManualOrder()(建單)與 manualMachineSlots()(機台貨道 AJAX);發票類型支援統編/手機條碼/自然人憑證/捐贈/一般 B2C,並寫入 SystemOperationLog 審計留痕。 4. 權限:整個補單功能限系統管理員(按鈕、彈窗、AJAX、store 四處把關);開立電子發票受機台 tax_invoice_enabled 閘門限制,前端隱藏選項 + 後端再驗一次。 5. 新增路由 admin.sales.manual.store 與 admin.sales.manual.machine-slots。 6. 前端:銷售訂單頁籤新增「手動補單」按鈕(僅系統管理員),slide 彈窗全面採用站內元件樣式——下拉用搜尋式 x-searchable-select(機台/貨道/付款/發票類型)、價格與數量改用「新增取貨碼」同款 +/- 計數器、去除原生 number spinner。 7. 訂單列表(桌面與行動卡片)為手動補單顯示「手動」標籤。 8. 三語系(zh_TW/en/ja)補齊 35 組字串並對齊排序、斜線不逸出。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
55cf6b8331
commit
8a92e1c1ff
@ -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)
|
||||
{
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 領藥單出貨回報(獨立於銷售流程)。
|
||||
*
|
||||
|
||||
35
lang/en.json
35
lang/en.json
@ -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.",
|
||||
|
||||
35
lang/ja.json
35
lang/ja.json
@ -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.": "このロールは別の会社に属しているため、割り当てることができません。",
|
||||
|
||||
@ -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.": "此角色屬於其他公司,無法指派。",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
@ -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">
|
||||
{{ $order->created_at->format('Y-m-d H:i:s') }}
|
||||
</span>
|
||||
<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">
|
||||
|
||||
@ -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');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user