diff --git a/app/Http/Controllers/Admin/SalesController.php b/app/Http/Controllers/Admin/SalesController.php index a85d1b2..934d398 100644 --- a/app/Http/Controllers/Admin/SalesController.php +++ b/app/Http/Controllers/Admin/SalesController.php @@ -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) { diff --git a/app/Models/Transaction/Order.php b/app/Models/Transaction/Order.php index 87e29fb..3a235e7 100644 --- a/app/Models/Transaction/Order.php +++ b/app/Models/Transaction/Order.php @@ -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); diff --git a/app/Services/Transaction/TransactionService.php b/app/Services/Transaction/TransactionService.php index 338e4af..d3aa656 100644 --- a/app/Services/Transaction/TransactionService.php +++ b/app/Services/Transaction/TransactionService.php @@ -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; + } + /** * 領藥單出貨回報(獨立於銷售流程)。 * diff --git a/config/cache.php b/config/cache.php index d4171e2..ccfe46f 100644 --- a/config/cache.php +++ b/config/cache.php @@ -15,7 +15,7 @@ return [ | */ - 'default' => env('CACHE_DRIVER', 'file'), + 'default' => env('CACHE_STORE', env('CACHE_DRIVER', 'database')), /* |-------------------------------------------------------------------------- diff --git a/lang/en.json b/lang/en.json index 52de439..0bc6287 100644 --- a/lang/en.json +++ b/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.", diff --git a/lang/ja.json b/lang/ja.json index 7203934..c6f3835 100644 --- a/lang/ja.json +++ b/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.": "このロールは別の会社に属しているため、割り当てることができません。", diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 7047dab..f898cd0 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.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": "選擇公司 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.": "此角色屬於其他公司,無法指派。", diff --git a/resources/views/admin/sales/index.blade.php b/resources/views/admin/sales/index.blade.php index bdcddc7..feb71a7 100644 --- a/resources/views/admin/sales/index.blade.php +++ b/resources/views/admin/sales/index.blade.php @@ -2,7 +2,8 @@ @section('content')
+ @ajax:navigate.window.prevent="fetchTabData(activeTab, $event.detail.url)" + @manual-order-created.window="switchTab('orders'); fetchTabData('orders')"> {{-- Page Header --}} @@ -107,6 +108,11 @@ + + {{-- 手動補單彈窗:僅系統管理員可見 / 使用(後端再次把關) --}} + @if(auth()->user()->isSystemAdmin()) + @include('admin.sales.partials.manual-order-modal') + @endif
@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 = `{{ __('Select a machine to choose products') }}`; + 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; + } + }, + }; +} @endsection diff --git a/resources/views/admin/sales/partials/manual-order-modal.blade.php b/resources/views/admin/sales/partials/manual-order-modal.blade.php new file mode 100644 index 0000000..766f84a --- /dev/null +++ b/resources/views/admin/sales/partials/manual-order-modal.blade.php @@ -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" => '', + "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" => '
', +]; +@endphp + +{{-- 手動補單彈窗(機台斷線/漏報時人工補登銷售紀錄)— 僅系統管理員 --}} +
+
+ {{-- Backdrop --}} +
+ + {{-- Dialog --}} +
+
+
+ + {{-- Header --}} +
+
+

+ + {{ __('Manual Order Entry') }} +

+

+ {{ __('For sales not reported due to disconnection. System administrators only.') }} +

+
+ +
+ + {{-- Body --}} +
+ + {{-- 機台 + 發生時間 --}} +
+
+ + + @foreach($machines as $m) + + @endforeach + +
+
+ + +
+
+ + {{-- 貨道/商品(搜尋式,依機台動態載入) --}} +
+ +
+
{{-- Rebuilt by Alpine --}}
+
+ +
+
+
+ + {{-- 付款方式 + 價格 --}} +
+
+ + + @foreach($paymentTypes as $ptVal => $ptLabel) + + @endforeach + +
+
+ +
+ +
+
+ $ + +
+
+ +
+
+
+ + {{-- 數量(Luxury Counter +/-) --}} +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + {{-- 小計 --}} +
+ {{ __('Total') }} + +
+ + {{-- 扣庫存 --}} + + + {{-- 開立電子發票(僅機台啟用電子發票時顯示) --}} +
+ + +
+
+ + + + + + + + +
+
+ + +
+
+
+ + {{-- 備註 --}} +
+ + +
+
+ + {{-- Footer --}} +
+ + +
+
+
+
+
+
diff --git a/resources/views/admin/sales/partials/tab-orders.blade.php b/resources/views/admin/sales/partials/tab-orders.blade.php index 3df521a..0e5a159 100644 --- a/resources/views/admin/sales/partials/tab-orders.blade.php +++ b/resources/views/admin/sales/partials/tab-orders.blade.php @@ -101,6 +101,18 @@
+ @if(auth()->user()->isSystemAdmin()) + {{-- 手動補單:機台斷線/漏報時人工補登銷售紀錄(僅系統管理員) --}} + + @endif
@@ -404,6 +421,9 @@

{{ $order->order_no }} + @if($order->is_manual) + {{ __('Manual') }} + @endif

diff --git a/routes/web.php b/routes/web.php index c15556f..5035c87 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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');