From 003ba9b1ad06523f399d707dc456c42e53b01c17 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 23 Jun 2026 01:55:14 +0000 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=E6=96=B0=E5=A2=9E=E9=A0=98=E8=97=A5?= =?UTF-8?q?=E5=96=AE=EF=BC=88Pharmacy=20Pickup=20/=20Rx=EF=BC=89=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E4=B8=A6=E5=BC=B7=E5=8C=96=E5=87=BA=E8=B2=A8=E9=98=B2?= =?UTF-8?q?=E5=91=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 領藥單後台:新增 PharmacyPickupController 與 PharmacyPickupService,提供領藥單列表/建立/作廢/列印 QR;routes/web.php 新增受 menu.sales.pharmacy-pickup 權限控管的路由群組,RoleSeeder 補該權限、sidebar 新增選單。 2. 訂單模型:Order 新增 order_type(sale/pharmacy_pickup)、pricing_slip_no、created_by 欄位與 AWAITING_PICKUP 狀態,新增 sales/pharmacyPickup scope 及 creator/pickupCode 關聯。 3. 機台設定:系統設定新增「領藥單」開關,僅在取物單(pickup_sheet)模式下可啟用、其他模式強制關閉;含後端防呆、toggle UI 與啟用功能標籤顯示。 4. 機台 API:getSettings FunctionSet 增加 PharmacyPickup 旗標;B660 取貨核銷擴充領藥單多貨道出貨清單回傳,庫存不足/無可用貨道時嚴格擋關且不消耗領藥碼。 5. 出貨流程:TransactionService finalizeTransaction 以 order_no(RX) 對應預建領藥單,走 finalizePharmacyDispense 獨立冪等出貨流程,並標記領藥碼已領與核銷日誌。 6. 防呆修正:recordDispense 庫存<=0 時不再扣減以避免負庫存;syncSlots 上報清單為空時不執行全量刪除,避免誤清貨道。 7. 資料庫:新增 2 個 migration(orders 增領藥單欄位、pickup_codes 之 slot_no 改 nullable);PickupCode 開放 status 可填、isValid 容許 null expires_at。 8. 多語系/文件:zh_TW/en/ja 三語系對齊補齊領藥單相關詞彙(27 個);config/api-docs.php 補 PharmacyPickup 旗標說明。 --- .../MachineSettingController.php | 9 + .../Admin/PharmacyPickupController.php | 140 ++++++++ .../Api/V1/App/MachineController.php | 92 +++++- app/Models/Transaction/Order.php | 33 ++ app/Models/Transaction/PickupCode.php | 5 +- app/Services/Machine/MachineService.php | 10 +- .../Transaction/PharmacyPickupService.php | 304 ++++++++++++++++++ .../Transaction/TransactionService.php | 117 ++++++- config/api-docs.php | 3 +- ...pharmacy_pickup_fields_to_orders_table.php | 46 +++ ...slot_no_nullable_on_pickup_codes_table.php | 27 ++ database/seeders/RoleSeeder.php | 1 + lang/en.json | 30 ++ lang/ja.json | 30 ++ lang/zh_TW.json | 30 ++ package-lock.json | 10 +- .../basic-settings/machines/index.blade.php | 22 ++ .../partials/tab-system-settings.blade.php | 6 + .../sales/pharmacy-pickup/index.blade.php | 186 +++++++++++ .../sales/pharmacy-pickup/print.blade.php | 79 +++++ .../layouts/partials/sidebar-menu.blade.php | 6 + routes/web.php | 8 +- 22 files changed, 1160 insertions(+), 34 deletions(-) create mode 100644 app/Http/Controllers/Admin/PharmacyPickupController.php create mode 100644 app/Services/Transaction/PharmacyPickupService.php create mode 100644 database/migrations/2026_06_18_130000_add_pharmacy_pickup_fields_to_orders_table.php create mode 100644 database/migrations/2026_06_18_130100_make_slot_no_nullable_on_pickup_codes_table.php create mode 100644 resources/views/admin/sales/pharmacy-pickup/index.blade.php create mode 100644 resources/views/admin/sales/pharmacy-pickup/print.blade.php diff --git a/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php b/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php index b007952..3d03d19 100644 --- a/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php +++ b/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php @@ -364,7 +364,11 @@ class MachineSettingController extends AdminController 'ambient_temp_monitoring_enabled', ]; + $allowedShoppingModes = ['basic', 'employee_card', 'pickup_sheet']; $shoppingMode = $settings['shopping_mode'] ?? 'basic'; + if (!in_array($shoppingMode, $allowedShoppingModes, true)) { + $shoppingMode = 'basic'; + } $data = []; foreach ($allowedFields as $field) { @@ -434,6 +438,11 @@ class MachineSettingController extends AdminController $jsonSettings[$field] = $data[$field]; } + // 領藥單模組:僅在「取物單(pickup_sheet)」模式下可啟用,其他模式一律關閉 + $jsonSettings['pharmacy_pickup_enabled'] = ($shoppingMode === 'pickup_sheet') + ? (bool) ($settings['pharmacy_pickup_enabled'] ?? false) + : false; + // 顯示語系 (languages):機台螢幕可切換的語系,最多 N 種,僅系統管理員可設定。 // 商品翻譯範圍由「公司機台語系聯集」反推,故此處異動需失效聯集快取並重建商品目錄。 // 非系統管理員即使送出 languages 亦一律忽略(不報錯,讓其仍可儲存其他設定)。 diff --git a/app/Http/Controllers/Admin/PharmacyPickupController.php b/app/Http/Controllers/Admin/PharmacyPickupController.php new file mode 100644 index 0000000..e38f60a --- /dev/null +++ b/app/Http/Controllers/Admin/PharmacyPickupController.php @@ -0,0 +1,140 @@ +where('settings->shopping_mode', 'pickup_sheet') + ->where('settings->pharmacy_pickup_enabled', true) + ->orderBy('name') + ->get(); + + $selectedMachine = null; + $products = new Collection(); + + if ($request->filled('machine_id')) { + $selectedMachine = $machines->firstWhere('id', (int) $request->input('machine_id')); + if ($selectedMachine) { + $products = $this->service->availableProducts($selectedMachine); + } + } + + $orders = Order::pharmacyPickup() + ->with(['machine:id,name,serial_no', 'items', 'creator:id,name', 'pickupCode']) + ->latest() + ->paginate($request->input('per_page', 10)) + ->withQueryString(); + + return view('admin.sales.pharmacy-pickup.index', [ + 'title' => __('menu.sales.pharmacy-pickup'), + 'machines' => $machines, + 'selectedMachine' => $selectedMachine, + 'products' => $products, + 'orders' => $orders, + ]); + } + + public function store(Request $request) + { + $request->validate([ + 'machine_id' => 'required|exists:machines,id', + 'pricing_slip_no' => 'nullable|string|max:64', + ]); + + // 由勾選的 products[] + qty[product_id] 組出 items + $productIds = (array) $request->input('products', []); + $qtyMap = (array) $request->input('qty', []); + $items = []; + foreach ($productIds as $pid) { + $q = (int) ($qtyMap[$pid] ?? 0); + if ($q > 0) { + $items[] = ['product_id' => (int) $pid, 'quantity' => $q]; + } + } + + try { + $order = $this->service->create([ + 'machine_id' => (int) $request->input('machine_id'), + 'pricing_slip_no' => $request->input('pricing_slip_no'), + 'items' => $items, + ], Auth::id()); + } catch (ValidationException $e) { + return back()->withErrors($e->errors())->withInput(); + } + + return redirect() + ->route('admin.sales.pharmacy-pickup', ['machine_id' => $request->input('machine_id')]) + ->with('success', __('Pharmacy pickup order created: :no', ['no' => $order->order_no])) + ->with('created_order_id', $order->id); + } + + /** + * 可列印的領藥單(QR 內嵌 SVG,內容為 8 碼領藥碼,機台掃描後送 B660 驗證)。 + */ + public function print(Order $order) + { + abort_unless($order->order_type === Order::TYPE_PHARMACY_PICKUP, 404); + + $order->load(['items', 'machine', 'pickupCode', 'creator:id,name']); + $code = $order->pickupCode?->code; + $qrSvg = $code + ? QrCode::format('svg')->size(220)->margin(1)->errorCorrection('M')->generate($code) + : null; + + return view('admin.sales.pharmacy-pickup.print', [ + 'order' => $order, + 'qrSvg' => $qrSvg, + ]); + } + + public function cancel(Order $order) + { + abort_unless($order->order_type === Order::TYPE_PHARMACY_PICKUP, 404); + + if (in_array($order->status, [Order::STATUS_COMPLETED, Order::STATUS_FAILED], true) + || $order->delivery_status === Order::DELIVERY_STATUS_SUCCESS) { + return back()->with('error', __('This pharmacy pickup order can no longer be cancelled.')); + } + + $order->update(['status' => Order::STATUS_FAILED]); + if ($order->pickupCode) { + $order->pickupCode->update(['status' => 'cancelled']); + } + + SystemOperationLog::create([ + 'company_id' => $order->company_id, + 'user_id' => Auth::id(), + 'module' => 'pharmacy_pickup', + 'action' => 'cancel', + 'target_id' => $order->id, + 'target_type' => Order::class, + ]); + + return back()->with('success', __('Pharmacy pickup order cancelled.')); + } +} diff --git a/app/Http/Controllers/Api/V1/App/MachineController.php b/app/Http/Controllers/Api/V1/App/MachineController.php index 02eaebd..e0b6ab3 100644 --- a/app/Http/Controllers/Api/V1/App/MachineController.php +++ b/app/Http/Controllers/Api/V1/App/MachineController.php @@ -501,6 +501,7 @@ class MachineController extends Controller 'WelcomeGift' => (bool) ($s['welcome_gift_enabled'] ?? false), // 來店禮 'MemberSystem' => (bool) ($s['member_system_enabled'] ?? false), // 會員系統 'AmbientTemp' => (bool) ($s['ambient_temp_monitoring_enabled'] ?? false), // 環境溫度監控 + 'PharmacyPickup' => (bool) ($s['pharmacy_pickup_enabled'] ?? false), // 領藥單(雲端建單,取物單模式下開關) ]; // 5-4 ShoppingMode:購物方式(頂層字串) @@ -690,17 +691,94 @@ class MachineController extends Controller ]); // 移除 StaffCardLog 重複紀錄,因為取貨碼本身有 status 和 order_id 可以追蹤 - // 憑證的「核銷」將由 MQTT finalizeTransaction 流程處理,不再需要 B660 PUT + // 一般取貨碼的「核銷」仍由 MQTT finalizeTransaction 流程處理(行為不變)。 + + $data = [ + 'slot_no' => $pickupCode->slot_no, // 保留:既有單貨道機台讀此欄(領藥單為 null) + 'pickup_code_id' => $pickupCode->id, + 'code_id' => $pickupCode->id, // 統一回傳 code_id + 'status' => $pickupCode->status, + ]; + + // === 領藥單(pharmacy_pickup)擴充:回傳多貨道清單 + 標記碼已領(僅領藥單,不影響既有單貨道流程) === + $order = $pickupCode->order; + $isPharmacy = $order && $order->order_type === \App\Models\Transaction\Order::TYPE_PHARMACY_PICKUP; + + if ($isPharmacy) { + // Gate:機台須仍為「取物單模式 + 領藥單開關開啟」才放行領藥碼 + $settings = $machine->settings ?? []; + $enabled = (($settings['shopping_mode'] ?? null) === 'pickup_sheet') + && (bool) ($settings['pharmacy_pickup_enabled'] ?? false); + + if (!$enabled) { + return response()->json(['success' => false, 'message' => 'Pharmacy pickup not enabled on this machine', 'code' => 403], 403); + } + + $pharmacyService = app(\App\Services\Transaction\PharmacyPickupService::class); + + // 嚴格擋關:任一商品的貨道可出量 < 領藥單需求量 → 整單擋下、不出貨、不消耗領藥碼,回明確提示 + $shortage = $pharmacyService->dispenseShortage($order); + if (!empty($shortage)) { + $detail = collect($shortage) + ->map(fn ($s) => "{$s['product_name']}(需 {$s['required']}、可出 {$s['available']})") + ->implode('、'); + + PickupCodeLog::create([ + 'company_id' => $machine->company_id, + 'machine_id' => $machine->id, + 'pickup_code_id' => $pickupCode->id, + 'order_id' => $order->id, + 'action' => 'verify_failed', + 'remark' => 'log.pickup.pharmacy_insufficient_stock', + 'raw_data' => ['order_no' => $order->order_no, 'shortage' => $shortage], + ]); + + return response()->json([ + 'success' => false, + 'code' => 409, + 'message' => '因貨道庫存不足,無法出貨:' . $detail, + ], 409); + } + + // 依目前貨道庫存解析多貨道出貨清單(一商品可跨多貨道湊量) + $items = $pharmacyService->resolveDispenseItems($order); + + // 解析不到可出貨貨道(機台無對應貨道/庫存不足)→ 不消耗領藥碼,回明確錯誤讓藥師處理 + if (empty($items)) { + PickupCodeLog::create([ + 'company_id' => $machine->company_id, + 'machine_id' => $machine->id, + 'pickup_code_id' => $pickupCode->id, + 'order_id' => $order->id, + 'action' => 'verify_failed', + 'remark' => 'log.pickup.pharmacy_no_slot', + 'raw_data' => ['order_no' => $order->order_no, 'reason' => 'no available slot/stock'], + ]); + return response()->json([ + 'success' => false, + 'code' => 409, + 'message' => '此領藥單在本機台無可出貨貨道(請確認貨道對應與庫存)', + ], 409); + } + + // 標記碼已領(usage_count++;達上限轉 used)——僅領藥單,避免動到既有單貨道核銷流程 + $pickupCode->usage_count = (int) $pickupCode->usage_count + 1; + $pickupCode->used_at = $pickupCode->used_at ?? now(); + if ($pickupCode->usage_count >= (int) $pickupCode->usage_limit) { + $pickupCode->status = 'used'; + } + $pickupCode->save(); + + $data['order_type'] = \App\Models\Transaction\Order::TYPE_PHARMACY_PICKUP; + $data['order_no'] = $order->order_no; + $data['items'] = $items; // [{slot_no, product_id, product_name, qty}, ...] + $data['status'] = $pickupCode->status; + } return response()->json([ 'success' => true, 'code' => 200, - 'data' => [ - 'slot_no' => $pickupCode->slot_no, - 'pickup_code_id' => $pickupCode->id, - 'code_id' => $pickupCode->id, // 統一回傳 code_id - 'status' => 'active' - ] + 'data' => $data, ]); } diff --git a/app/Models/Transaction/Order.php b/app/Models/Transaction/Order.php index d8840ce..87e29fb 100644 --- a/app/Models/Transaction/Order.php +++ b/app/Models/Transaction/Order.php @@ -14,6 +14,10 @@ class Order extends Model { use HasFactory, SoftDeletes, TenantScoped, HasDisplayFlowId; + // 訂單類型 (order_type) + public const TYPE_SALE = 'sale'; // 一般銷售 + public const TYPE_PHARMACY_PICKUP = 'pharmacy_pickup'; // 領藥單 + // 支付狀態 public const PAYMENT_STATUS_FAILED = 0; public const PAYMENT_STATUS_SUCCESS = 1; @@ -31,6 +35,8 @@ class Order extends Model public const STATUS_COMPLETED = 'completed'; public const STATUS_FAILED = 'failed'; public const STATUS_ABANDONED = 'abandoned'; + // 領藥單專用:已開立、等待病人到機台領取(非 pending,不被逾時清掃;非終態,可被出貨 finalize 更新)。 + public const STATUS_AWAITING_PICKUP = 'awaiting_pickup'; /** 終態清單:到了這些狀態就不再變更(冪等)。 */ public const TERMINAL_STATUSES = [ @@ -43,6 +49,9 @@ class Order extends Model 'company_id', 'flow_id', 'order_no', + 'order_type', + 'pricing_slip_no', + 'created_by', 'machine_id', 'member_id', 'payment_type', @@ -183,11 +192,29 @@ class Order extends Model . mb_substr($name, -1); } + /** 一般銷售訂單 */ + public function scopeSales($query) + { + return $query->where('order_type', self::TYPE_SALE); + } + + /** 領藥單 */ + public function scopePharmacyPickup($query) + { + return $query->where('order_type', self::TYPE_PHARMACY_PICKUP); + } + public function machine() { return $this->belongsTo(Machine::class); } + /** 建單者 (領藥單藥師) */ + public function creator() + { + return $this->belongsTo(\App\Models\System\User::class, 'created_by'); + } + public function member() { return $this->belongsTo(Member::class); @@ -208,6 +235,12 @@ class Order extends Model return $this->hasMany(DispenseRecord::class); } + /** 領藥單對應的領藥碼(一單一碼) */ + public function pickupCode() + { + return $this->hasOne(PickupCode::class); + } + public function staffCardLog() { return $this->hasOne(\App\Models\StaffCardLog::class); diff --git a/app/Models/Transaction/PickupCode.php b/app/Models/Transaction/PickupCode.php index f8b3bc4..a5e52f4 100644 --- a/app/Models/Transaction/PickupCode.php +++ b/app/Models/Transaction/PickupCode.php @@ -18,6 +18,7 @@ class PickupCode extends Model 'slot_no', 'code', 'slug', + 'status', 'expires_at', 'used_at', 'usage_limit', @@ -78,9 +79,9 @@ class PickupCode extends Model */ public function isValid(): bool { - return $this->status === 'active' + return $this->status === 'active' && ($this->usage_count < $this->usage_limit) - && ($this->expires_at->isFuture()); + && ($this->expires_at?->isFuture() ?? true); // null expires_at = 無到期限制 } /** diff --git a/app/Services/Machine/MachineService.php b/app/Services/Machine/MachineService.php index 7964815..9086329 100644 --- a/app/Services/Machine/MachineService.php +++ b/app/Services/Machine/MachineService.php @@ -392,10 +392,12 @@ class MachineService ->values() ->toArray(); - // 找出資料庫中屬於此機台但不在清單內的舊貨道 - $orphanedSlots = $machine->slots() - ->whereNotIn('slot_no', $reportedSlotNos) - ->get(); + // 防呆:上報清單為空時不執行全量刪除(避免機台/APP 誤報空清單而把所有貨道清光)。 + $orphanedSlots = empty($reportedSlotNos) + ? collect() + : $machine->slots() + ->whereNotIn('slot_no', $reportedSlotNos) + ->get(); foreach ($orphanedSlots as $orphan) { $oldStock = (int) $orphan->stock; diff --git a/app/Services/Transaction/PharmacyPickupService.php b/app/Services/Transaction/PharmacyPickupService.php new file mode 100644 index 0000000..8a5ce57 --- /dev/null +++ b/app/Services/Transaction/PharmacyPickupService.php @@ -0,0 +1,304 @@ +id) + ->where('order_type', Order::TYPE_PHARMACY_PICKUP) + ->where('status', Order::STATUS_AWAITING_PICKUP) + ->with('items') + ->get(); + + foreach ($orders as $order) { + foreach ($order->items as $item) { + $map[$item->product_id] = ($map[$item->product_id] ?? 0) + (int) $item->quantity; + } + } + + return $map; + } + + /** + * 取得某機台目前「有庫存」的可領藥品(依商品彙總,跨貨道加總庫存)。 + * available 已扣除「尚未領取的領藥單預留量」(生成領藥單即預扣)。 + */ + public function availableProducts(Machine $machine): Collection + { + $reserved = $this->reservedQtyMap($machine); + + return MachineSlot::where('machine_id', $machine->id) + ->where('is_active', true) + ->where('stock', '>', 0) + ->with('product') + ->get() + ->groupBy('product_id') + ->map(function ($slots) use ($reserved) { + $product = $slots->first()->product; + if (!$product) { + return null; + } + + $physical = (int) $slots->sum('stock'); + $reservedQty = (int) ($reserved[$product->id] ?? 0); + $available = max(0, $physical - $reservedQty); + + return (object) [ + 'product_id' => $product->id, + 'name' => $product->localized_name ?? $product->name, + 'spec' => $product->localized_spec ?? $product->spec, + 'barcode' => $product->barcode, + 'price' => (float) ($product->price ?? 0), + 'available' => $available, // 預扣後可用(= 實體 − 預留) + 'physical' => $physical, // 實體庫存 + 'reserved' => $reservedQty, // 已生成領藥單預留量 + 'slot_count' => $slots->count(), + 'slots' => $slots->pluck('slot_no')->implode(', '), + ]; + }) + ->filter() + ->sortBy('name') + ->values(); + } + + /** + * 建立一張領藥單:Order(pharmacy_pickup) + N 個 OrderItem + 1 個 PickupCode。 + * + * @param array{machine_id:int, pricing_slip_no?:string, items:array, expires_at?:\DateTimeInterface} $data + */ + public function create(array $data, int $userId): Order + { + $machine = Machine::findOrFail($data['machine_id']); + $items = collect($data['items'] ?? []) + ->map(fn ($i) => ['product_id' => (int) $i['product_id'], 'quantity' => (int) $i['quantity']]) + ->filter(fn ($i) => $i['quantity'] > 0) + ->values(); + + if ($items->isEmpty()) { + throw ValidationException::withMessages([ + 'items' => __('Please select at least one medicine with quantity.'), + ]); + } + + return DB::transaction(function () use ($machine, $items, $data, $userId) { + $orderItemsData = []; + $total = 0.0; + + // 既有「尚未領取領藥單」的預留量(本單尚未建立,不含本單)→ 可用 = 實體 − 預留,防超領。 + $reserved = $this->reservedQtyMap($machine); + + foreach ($items as $item) { + $product = Product::findOrFail($item['product_id']); + $qty = $item['quantity']; + + // 該機台此商品跨貨道可用庫存 = 實體庫存 − 已生成領藥單預留量 + $physical = (int) MachineSlot::where('machine_id', $machine->id) + ->where('product_id', $product->id) + ->where('is_active', true) + ->sum('stock'); + $available = $physical - (int) ($reserved[$product->id] ?? 0); + + if ($qty > $available) { + throw ValidationException::withMessages([ + 'items' => __('Insufficient stock for :name (requested :qty, available :available).', [ + 'name' => $product->localized_name ?? $product->name, + 'qty' => $qty, + 'available' => $available, + ]), + ]); + } + + $price = (float) ($product->price ?? 0); + $subtotal = $price * $qty; + $total += $subtotal; + + $orderItemsData[] = [ + 'product_id' => $product->id, + 'product_name' => $product->localized_name ?? $product->name, + 'barcode' => $product->barcode, + 'quantity' => $qty, + 'price' => $price, + 'subtotal' => $subtotal, + ]; + } + + $serial = $this->generateSerial(); + + $order = Order::create([ + 'company_id' => $machine->company_id, + 'flow_id' => $serial, + 'order_no' => $serial, + 'order_type' => Order::TYPE_PHARMACY_PICKUP, + 'pricing_slip_no' => $data['pricing_slip_no'] ?? null, + 'created_by' => $userId, + 'machine_id' => $machine->id, + 'payment_type' => self::PAYMENT_TYPE_PHARMACY, + 'total_amount' => $total, + 'discount_amount' => 0, + 'pay_amount' => 0, + 'change_amount' => 0, + 'points_used' => 0, + 'payment_status' => Order::PAYMENT_STATUS_SUCCESS, + 'status' => Order::STATUS_AWAITING_PICKUP, + 'delivery_status' => Order::DELIVERY_STATUS_FAILED, // 0:尚未出貨 + 'machine_time' => now(), + ]); + + foreach ($orderItemsData as $oi) { + $order->items()->create($oi); + } + + $pickupCode = PickupCode::create([ + 'company_id' => $machine->company_id, + 'machine_id' => $machine->id, + 'slot_no' => null, // 多貨道:由 B660 掃碼時解析 + 'code' => PickupCode::generateUniqueCode($machine->id), + 'status' => 'active', + 'expires_at' => $data['expires_at'] ?? now()->endOfDay(), // 預設當日有效 + 'usage_limit' => 1, + 'usage_count' => 0, + 'created_by' => $userId, + 'order_id' => $order->id, + ]); + + SystemOperationLog::create([ + 'company_id' => $machine->company_id, + 'user_id' => $userId, + 'module' => 'pharmacy_pickup', + 'action' => 'create', + 'target_id' => $order->id, + 'target_type' => Order::class, + 'new_values' => [ + 'order_no' => $order->order_no, + 'pricing_slip_no' => $order->pricing_slip_no, + 'pickup_code' => $pickupCode->code, + 'items' => $orderItemsData, + ], + ]); + + return $order->load(['items', 'pickupCode', 'machine']); + }); + } + + /** + * 檢查一張領藥單在「目前機台貨道庫存」下是否有商品庫存不足(無法整單出貨)。 + * 回傳不足清單:[['product_name'=>, 'required'=>, 'available'=>], ...];空陣列表示可整單出貨。 + * 用於 B660 掃碼出貨前的嚴格擋關:任何商品不足即整單擋下、不出貨、不消耗領藥碼。 + */ + public function dispenseShortage(Order $order): array + { + $short = []; + + foreach ($order->items as $orderItem) { + $available = (int) MachineSlot::where('machine_id', $order->machine_id) + ->where('product_id', $orderItem->product_id) + ->where('is_active', true) + ->where('stock', '>', 0) + ->sum('stock'); + + if ($available < (int) $orderItem->quantity) { + $short[] = [ + 'product_name' => $orderItem->product_name, + 'required' => (int) $orderItem->quantity, + 'available' => $available, + ]; + } + } + + return $short; + } + + /** + * 解析一張領藥單在「目前機台貨道庫存」下的出貨清單(一商品可跨多貨道湊量)。 + * 回傳:[['slot_no'=>, 'product_id'=>, 'product_name'=>, 'qty'=>], ...] + * 庫存不足時盡力分配(出可出的數量;部分/失敗由出貨回報 Phase 標記)。 + */ + public function resolveDispenseItems(Order $order): array + { + $result = []; + + foreach ($order->items as $orderItem) { + $remaining = (int) $orderItem->quantity; + + $slots = MachineSlot::where('machine_id', $order->machine_id) + ->where('product_id', $orderItem->product_id) + ->where('is_active', true) + ->where('stock', '>', 0) + ->orderByDesc('stock') + ->orderBy('slot_no') + ->get(); + + foreach ($slots as $slot) { + if ($remaining <= 0) { + break; + } + $take = min($remaining, (int) $slot->stock); + if ($take <= 0) { + continue; + } + $result[] = [ + 'slot_no' => $slot->slot_no, + 'product_id' => $orderItem->product_id, + 'product_name' => $orderItem->product_name, + 'qty' => $take, + ]; + $remaining -= $take; + } + // $remaining > 0 表示目前庫存不足以完全滿足此商品,僅回傳可出的部分。 + } + + return $result; + } + + /** + * 產生領藥單序號:RX + 西元年月日 + 當日 4 碼流水(與 flow_id 唯一性衝突時自動遞增)。 + */ + protected function generateSerial(): string + { + $datePart = now()->format('Ymd'); + $base = Order::withoutGlobalScopes() + ->where('order_type', Order::TYPE_PHARMACY_PICKUP) + ->whereDate('created_at', now()->toDateString()) + ->count(); + + $attempt = 0; + do { + $seq = $base + 1 + $attempt; + $serial = 'RX' . $datePart . str_pad((string) $seq, 4, '0', STR_PAD_LEFT); + $attempt++; + } while ( + Order::withoutGlobalScopes()->where('flow_id', $serial)->exists() + && $attempt < 100 + ); + + return $serial; + } +} diff --git a/app/Services/Transaction/TransactionService.php b/app/Services/Transaction/TransactionService.php index f96e78f..a83ac99 100644 --- a/app/Services/Transaction/TransactionService.php +++ b/app/Services/Transaction/TransactionService.php @@ -303,18 +303,29 @@ class TransactionService // 執行實體庫存扣除 (真實交易化) if ((int) ($data['dispense_status'] ?? 0) === 1) { if ($slot) { - $slot->decrement('stock'); + // 防呆:庫存已為 0(或更低)時不再扣減,避免出現負庫存(-1)。 + // 正常情況下 B660 已嚴格擋下庫存不足的領藥單;此處為下位機與後台庫存不同步時的最後防線。 + if ((int) $slot->stock > 0) { + $slot->decrement('stock'); - $type = \App\Models\Machine\MachineStockMovement::TYPE_SALE; + $type = \App\Models\Machine\MachineStockMovement::TYPE_SALE; - $this->machineService->recordStockMovement( - $slot, - -1, - $type, - $order ?? $record, - $order ? "Sale Order: #{$order->order_no}" : "Dispense Record: #{$record->id}", - ['order_id' => $order?->id, 'dispense_id' => $record->id] - ); + $this->machineService->recordStockMovement( + $slot, + -1, + $type, + $order ?? $record, + $order ? "Sale Order: #{$order->order_no}" : "Dispense Record: #{$record->id}", + ['order_id' => $order?->id, 'dispense_id' => $record->id] + ); + } else { + \Log::warning("Dispense decrement skipped: slot stock already <= 0", [ + 'machine_id' => $machine->id, + 'slot_no' => $slotNo, + 'current_stock' => (int) $slot->stock, + 'flow_id' => $data['flow_id'] ?? null, + ]); + } } else { // Log warning if slot matching fails to help diagnostic \Log::warning("Dispense record created but slot not found for decrement", [ @@ -367,6 +378,31 @@ class TransactionService return $existingOrder; } + // 領藥單(pharmacy_pickup):機台沿用中國醫(CMUH)出貨流程,finalize 的 flow_id 是「機台本地流水號」, + // 但 order.order_no = 後端預建的領藥單號(RX…)。故優先以 order_no 對應預建領藥單,避免被當成新銷售單建立。 + $pharmacyOrderNo = $data['order']['order_no'] ?? ($data['order_no'] ?? null); + if ($pharmacyOrderNo) { + $pharmacyOrder = Order::withoutGlobalScopes() + ->where('order_type', Order::TYPE_PHARMACY_PICKUP) + ->where('order_no', $pharmacyOrderNo) + ->first(); + if ($pharmacyOrder) { + if (in_array($pharmacyOrder->status, Order::TERMINAL_STATUSES, true)) { + Log::info("Pharmacy order already finalized (terminal)", [ + 'order_no' => $pharmacyOrderNo, + 'status' => $pharmacyOrder->status, + ]); + return $pharmacyOrder; // 冪等 + } + return $this->finalizePharmacyDispense($pharmacyOrder, $data); + } + } + + // 後備:若 flow_id 本身就對到領藥單(例如機台有正確帶 RX flow_id),也走領藥流程。 + if ($existingOrder && $existingOrder->order_type === Order::TYPE_PHARMACY_PICKUP) { + return $this->finalizePharmacyDispense($existingOrder, $data); + } + // 1. Process Order (B600) $orderData = $data['order']; $orderData['serial_no'] = $serialNo; @@ -533,6 +569,67 @@ class TransactionService }); } + /** + * 領藥單出貨回報(獨立於銷售流程)。 + * + * 機台逐道出貨後,以 transaction/finalize 帶 dispense[](每出貨一單位一筆,含 slot_no/dispense_status)回報。 + * 本方法:建立出貨紀錄(沿用 recordDispense:成功則扣 1 庫存)→ 彙整 delivery_status → + * 將訂單轉為終態 completed(冪等:重送會在 finalizeTransaction 開頭 early-return)→ 標記領藥碼已領 + 核銷日誌。 + */ + protected function finalizePharmacyDispense(Order $order, array $data): Order + { + return DB::transaction(function () use ($order, $data) { + $serialNo = $data['serial_no'] ?? $order->machine?->serial_no; + + if (!empty($data['dispense'])) { + $dispenseList = isset($data['dispense'][0]) ? $data['dispense'] : [$data['dispense']]; + foreach ($dispenseList as $dispenseItem) { + $dispenseItem['serial_no'] = $serialNo; + $dispenseItem['flow_id'] = $order->flow_id; + $dispenseItem['order_id'] = $order->id; + $this->recordDispense($dispenseItem); // 建 DispenseRecord;dispense_status=1 時扣 1 庫存 + } + $deliveryStatus = $this->resolveDeliveryStatusFromDispense($data['dispense']); + } else { + // 無出貨明細視為失敗 + $deliveryStatus = Order::DELIVERY_STATUS_FAILED; + } + + $order->update([ + 'delivery_status' => $deliveryStatus, + 'status' => Order::STATUS_COMPLETED, // 終態 → 後續重送冪等 + 'machine_time' => $data['machine_time'] ?? $order->machine_time, + ]); + + // 標記領藥碼已領(B660 通常已標 used;此處保險並留核銷日誌) + $pickupCode = $order->pickupCode; + if ($pickupCode) { + if ($pickupCode->status !== 'used') { + $pickupCode->update(['status' => 'used', 'used_at' => $pickupCode->used_at ?? now()]); + } + PickupCodeLog::create([ + 'company_id' => $order->company_id, + 'machine_id' => $order->machine_id, + 'pickup_code_id' => $pickupCode->id, + 'order_id' => $order->id, + 'action' => 'used', + 'remark' => "log.pickup.pharmacy_dispensed", + 'raw_data' => [ + 'order_no' => $order->order_no, + 'delivery_status' => $deliveryStatus, + ], + ]); + } + + Log::info('Pharmacy pickup dispense finalized', [ + 'flow_id' => $order->flow_id, + 'delivery_status' => $deliveryStatus, + ]); + + return $order; + }); + } + /** * Resolve order-level delivery status from item-level dispense results. */ diff --git a/config/api-docs.php b/config/api-docs.php index 4cafd83..5b5efbb 100644 --- a/config/api-docs.php +++ b/config/api-docs.php @@ -183,7 +183,7 @@ return [ ], 'data.FunctionSet' => [ 'type' => 'object', - 'description' => "非支付功能模組旗標(新定義,機台端待實作對應結構;布林):\n• PickupModule ← pickup_module_enabled(取貨模組)\n• PickupCode ← pickup_code_enabled(取貨碼)\n• PassCode ← pass_code_enabled(通行碼)\n• WelcomeGift ← welcome_gift_enabled(來店禮)\n• MemberSystem ← member_system_enabled(會員系統)\n• AmbientTemp ← ambient_temp_monitoring_enabled(環境溫度監控)", + 'description' => "非支付功能模組旗標(新定義,機台端待實作對應結構;布林):\n• PickupModule ← pickup_module_enabled(取貨模組)\n• PickupCode ← pickup_code_enabled(取貨碼)\n• PassCode ← pass_code_enabled(通行碼)\n• WelcomeGift ← welcome_gift_enabled(來店禮)\n• MemberSystem ← member_system_enabled(會員系統)\n• AmbientTemp ← ambient_temp_monitoring_enabled(環境溫度監控)\n• PharmacyPickup ← pharmacy_pickup_enabled(領藥單;雲端建單、掃 QR 出貨。僅在 ShoppingMode=pickup_sheet 取物單模式下有效)", ], 'data.ShoppingMode' => [ 'type' => 'string', @@ -258,6 +258,7 @@ return [ 'WelcomeGift' => false, 'MemberSystem' => false, 'AmbientTemp' => false, + 'PharmacyPickup' => false, ], 'ShoppingMode' => 'basic', 'LangSet' => [ diff --git a/database/migrations/2026_06_18_130000_add_pharmacy_pickup_fields_to_orders_table.php b/database/migrations/2026_06_18_130000_add_pharmacy_pickup_fields_to_orders_table.php new file mode 100644 index 0000000..4c8b80c --- /dev/null +++ b/database/migrations/2026_06_18_130000_add_pharmacy_pickup_fields_to_orders_table.php @@ -0,0 +1,46 @@ +string('order_type', 20)->default('sale')->index() + ->comment('訂單類型: sale=一般銷售, pharmacy_pickup=領藥單')->after('order_no'); + } + if (!Schema::hasColumn('orders', 'pricing_slip_no')) { + $table->string('pricing_slip_no', 64)->nullable() + ->comment('批價單號 (領藥單藥師填寫之依據)')->after('order_type'); + } + if (!Schema::hasColumn('orders', 'created_by')) { + $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete() + ->comment('建單者 (領藥單由後台藥師建立)')->after('pricing_slip_no'); + } + }); + } + + public function down(): void + { + Schema::table('orders', function (Blueprint $table) { + if (Schema::hasColumn('orders', 'created_by')) { + $table->dropForeign(['created_by']); + $table->dropColumn('created_by'); + } + if (Schema::hasColumn('orders', 'pricing_slip_no')) { + $table->dropColumn('pricing_slip_no'); + } + if (Schema::hasColumn('orders', 'order_type')) { + $table->dropColumn('order_type'); + } + }); + } +}; diff --git a/database/migrations/2026_06_18_130100_make_slot_no_nullable_on_pickup_codes_table.php b/database/migrations/2026_06_18_130100_make_slot_no_nullable_on_pickup_codes_table.php new file mode 100644 index 0000000..2fbc32e --- /dev/null +++ b/database/migrations/2026_06_18_130100_make_slot_no_nullable_on_pickup_codes_table.php @@ -0,0 +1,27 @@ +string('slot_no')->nullable()->change(); + }); + } + + public function down(): void + { + Schema::table('pickup_codes', function (Blueprint $table) { + $table->string('slot_no')->nullable(false)->change(); + }); + } +}; diff --git a/database/seeders/RoleSeeder.php b/database/seeders/RoleSeeder.php index 2a98060..ce88935 100644 --- a/database/seeders/RoleSeeder.php +++ b/database/seeders/RoleSeeder.php @@ -39,6 +39,7 @@ class RoleSeeder extends Seeder 'menu.sales.promotions', 'menu.sales.pass-codes', 'menu.sales.store-gifts', + 'menu.sales.pharmacy-pickup', 'menu.analysis', 'menu.analysis.change-stock', 'menu.analysis.machine-reports', diff --git a/lang/en.json b/lang/en.json index ef124a7..2a28c50 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1,4 +1,6 @@ { + "(deducted by issued pickup orders)": "(deducted by issued pickup orders)", + "-- Select Machine --": "-- Select Machine --", "10s": "10s", "15 Seconds": "15 Seconds", "1920x1080 (Max 10MB)": "1920x1080 (Max 10MB)", @@ -216,9 +218,11 @@ "Auto-load succeeded but purchase failed": "Auto-load succeeded but purchase failed", "Automatically calculate replenishment needs based on machine capacity": "Automatically calculate replenishment needs based on machine capacity", "Availability": "Availability", + "Available": "Available", "Available Machines": "Available Machines", "Avatar updated successfully.": "Avatar updated successfully.", "Avg Cycle": "Avg Cycle", + "Awaiting Pickup": "Awaiting Pickup", "Back to History": "Back to History", "Back to List": "Back to List", "Badge Settings": "Badge Settings", @@ -265,6 +269,7 @@ "Cancel Pass Code": "Cancel Pass Code", "Cancel Pickup Code": "Cancel Pickup Code", "Cancel Purchase": "Cancel Purchase", + "Cancel this pharmacy pickup order?": "Cancel this pharmacy pickup order?", "Cancelled": "Cancelled", "Cannot Delete Role": "Cannot Delete Role", "Cannot cancel this order": "Cannot cancel this order", @@ -424,10 +429,12 @@ "Courier/Dispatcher": "Courier/Dispatcher", "Courier/Replenisher": "Courier/Replenisher", "Create": "Create", + "Create & Generate QR": "Create & Generate QR", "Create Config": "Create Config", "Create Machine": "Create Machine", "Create New Role": "Create New Role", "Create Payment Config": "Create Payment Config", + "Create Pharmacy Pickup Order": "Create Pharmacy Pickup Order", "Create Product": "Create Product", "Create Replenishment Order": "Create Replenishment Order", "Create Role": "Create Role", @@ -440,6 +447,7 @@ "Create stock replenishment for specific machines": "Create stock replenishment for specific machines", "Create stock transfers between warehouses and machine returns": "Create stock transfers between warehouses and machine returns", "Create stock-in orders": "Create stock-in orders", + "Created": "Created", "Created At": "Created At", "Created By": "Created By", "Creation Time": "Creation Time", @@ -951,6 +959,7 @@ "Initial contract registration": "Initial contract registration", "Installation": "Installation", "Installation Location": "Installation Location", + "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 personnel selection": "Invalid personnel selection", @@ -971,6 +980,8 @@ "Invoice Number / Time": "Invoice Number / Time", "Invoice Status": "Invoice Status", "Invoice Store ID": "Invoice Store ID", + "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 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", "Item List": "Item List", @@ -1185,6 +1196,7 @@ "Max Capacity:": "Max Capacity:", "Max Stock": "Max Stock", "Maximum file size: 100MB": "Maximum file size: 100MB", + "Medicine": "Medicine", "Member": "Member", "Member & External": "Member & External", "Member + 1": "Member + 1", @@ -1317,6 +1329,7 @@ "No machines assigned": "No machines assigned", "No machines available": "No machines available", "No machines available in this company.": "No machines available in this company.", + "No machines enabled for pharmacy pickup. Enable it under Machine System Settings (Pickup Sheet mode → Pharmacy Pickup switch).": "No machines enabled for pharmacy pickup. Enable it under Machine System Settings (Pickup Sheet mode → Pharmacy Pickup switch).", "No machines found": "No machines found", "No machines in this status": "No machines in this status", "No machines match your criteria": "No machines match your criteria", @@ -1329,6 +1342,7 @@ "No operation logs found": "No operation logs found", "No pass codes found": "No pass codes found", "No permissions": "No permissions", + "No pharmacy pickup orders yet.": "No pharmacy pickup orders yet.", "No pickup codes found": "No pickup codes found", "No product data matching search criteria": "No product data matching search criteria", "No product record found": "No product record found", @@ -1535,10 +1549,15 @@ "Permissions": "Permissions", "Permissions updated successfully": "Permissions updated successfully", "Personnel assigned successfully": "Personnel assigned successfully", + "Pharmacy Pickup (Rx)": "Pharmacy Pickup (Rx)", + "Pharmacy Pickup Orders": "Pharmacy Pickup Orders", + "Pharmacy pickup order cancelled.": "Pharmacy pickup order cancelled.", + "Pharmacy pickup order created: :no": "Pharmacy pickup order created: :no", "Phone": "Phone", "Photo Slot": "Photo Slot", "Pi Pay": "Pi Pay", "Pi Wallet": "Pi Wallet", + "Picked Up": "Picked Up", "Picked up": "Picked up", "Picked up Time": "Picked up Time", "Pickup Code": "Pickup Code", @@ -1586,6 +1605,7 @@ "Please select a material": "Please select a material", "Please select a slot": "Please select a slot", "Please select an APK file to upload": "Please select an APK file to upload", + "Please select at least one medicine with quantity.": "Please select at least one medicine with quantity.", "Please select warehouse and machine": "Please select warehouse and machine", "Please use our standard template to ensure data compatibility.": "Please use our standard template to ensure data compatibility.", "PlusPay": "PlusPay", @@ -1606,6 +1626,7 @@ "Previous": "Previous", "Price / Member": "Price / Member", "Pricing Information": "Pricing Information", + "Pricing Slip No.": "Pricing Slip No.", "Primary Theme Color": "Primary Theme Color", "Print": "Print", "Print & PDF": "Print & PDF", @@ -1820,6 +1841,7 @@ "Scan QR Code": "Scan QR Code", "Scan Store ID": "Scan Store ID", "Scan Term ID": "Scan Term ID", + "Scan the QR code or enter the pickup code at the machine to collect your medicine.": "Scan the QR code or enter the pickup code at the machine to collect your medicine.", "Scan this code at the machine or share the link with the customer.": "Scan this code at the machine or share the link with the customer.", "Scan this code at the machine to authorize testing or maintenance.": "Scan this code at the machine to authorize testing or maintenance.", "Scan this code to quickly access the maintenance form for this device.": "Scan this code to quickly access the maintenance form for this device.", @@ -1973,6 +1995,7 @@ "Source Machine": "Source Machine", "Source Warehouse": "Source Warehouse", "Source of temperature limit settings": "Source of temperature limit settings", + "Spec": "Spec", "Special Permission": "Special Permission", "Specification": "Specification", "Specifications": "Specifications", @@ -2137,8 +2160,10 @@ "This account does not belong to this company.": "This account does not belong to this company.", "This invoice has no printable copy": "This invoice has no printable copy", "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 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.", "This slot has a pending command. Please wait.": "This slot has a pending command. Please wait.", "This slot has a pending update. Please wait for the previous command to complete.": "This slot has a pending update. Please wait for the previous command to complete.", @@ -2453,6 +2478,7 @@ "e.g. Taiwan Star": "e.g. Taiwan Star", "e.g. Test Code for Maintenance": "e.g. Test Code for Maintenance", "e.g. johndoe": "e.g. johndoe", + "e.g. the hospital pricing slip number": "e.g. the hospital pricing slip number", "e.g., 12345678": "e.g., 12345678", "e.g., Beverage": "e.g., Beverage", "e.g., Company Standard Pay": "e.g., Company Standard Pay", @@ -2528,6 +2554,7 @@ "menu.sales": "Sales Management", "menu.sales.orders": "Purchase Orders", "menu.sales.pass-codes": "Pass Codes", + "menu.sales.pharmacy-pickup": "Pharmacy Pickup", "menu.sales.pickup-codes": "Pickup Codes", "menu.sales.promotions": "Promotions", "menu.sales.records": "Sales Records", @@ -2558,12 +2585,14 @@ "name_dictionary_key": "name_dictionary_key", "of": "of", "of items": "items", + "optional": "optional", "orders": "orders", "pending": "Pending", "permissions": "Permission Settings", "permissions.accounts": "Account Management", "permissions.companies": "Customer Management", "permissions.roles": "Role Permissions", + "physical": "physical", "price": "price", "product": "product", "product_catalog_synced_to_all_machines": "product_catalog_synced_to_all_machines", @@ -2573,6 +2602,7 @@ "product_updated": "product_updated", "remote": "Remote Management", "reservation": "Reservation System", + "reserved": "reserved", "roles": "Roles", "s": "s", "sales": "Sales Management", diff --git a/lang/ja.json b/lang/ja.json index eb86d99..5222a22 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -1,4 +1,6 @@ { + "(deducted by issued pickup orders)": "(発行済み受取注文の予約分を差し引き済み)", + "-- Select Machine --": "-- 機器を選択 --", "10s": "10秒", "15 Seconds": "15秒", "1920x1080 (Max 10MB)": "1920x1080 (最大10MB)", @@ -216,9 +218,11 @@ "Auto-load succeeded but purchase failed": "オートチャージ成功、但し購買取引失敗", "Automatically calculate replenishment needs based on machine capacity": "機器容量に基づいて補充必要量を自動計算", "Availability": "可用性 (Availability)", + "Available": "利用可能在庫", "Available Machines": "割り当て可能な機器", "Avatar updated successfully.": "アバターが更新されました。", "Avg Cycle": "平均サイクル", + "Awaiting Pickup": "受取待ち", "Back to History": "履歴に戻る", "Back to List": "一覧に戻る", "Badge Settings": "バッジ設定", @@ -265,6 +269,7 @@ "Cancel Pass Code": "パスコードキャンセル", "Cancel Pickup Code": "受取コードキャンセル", "Cancel Purchase": "購入キャンセル", + "Cancel this pharmacy pickup order?": "この薬品受取注文を取り消しますか?", "Cancelled": "キャンセル済み", "Cannot Delete Role": "ロールを削除できません", "Cannot cancel this order": "この注文はキャンセルできません", @@ -424,10 +429,12 @@ "Courier/Dispatcher": "配送担当", "Courier/Replenisher": "配送・補充担当", "Create": "作成", + "Create & Generate QR": "作成してQRを生成", "Create Config": "設定作成", "Create Machine": "機器追加", "Create New Role": "新規ロール作成", "Create Payment Config": "決済設定作成", + "Create Pharmacy Pickup Order": "薬品受取注文を作成", "Create Product": "商品作成", "Create Replenishment Order": "補充伝票作成", "Create Role": "ロール作成", @@ -440,6 +447,7 @@ "Create stock replenishment for specific machines": "特定の機器の在庫補充伝票を作成", "Create stock transfers between warehouses and machine returns": "倉庫間の移動または機器からの返品を作成", "Create stock-in orders": "入庫伝票を作成", + "Created": "作成日時", "Created At": "作成日時", "Created By": "作成者", "Creation Time": "作成時間", @@ -951,6 +959,7 @@ "Initial contract registration": "初期契約登録", "Installation": "設置", "Installation Location": "設置場所", + "Insufficient stock for :name (requested :qty, available :available).": ":name の在庫が不足しています(要求 :qty、利用可能 :available)。", "Insufficient stock for transfer": "在庫不足のため移動できません", "Insufficient stored-value / e-wallet balance": "電子マネー/電子ウォレット残高不足", "Invalid personnel selection": "無効な担当者選択", @@ -971,6 +980,8 @@ "Invoice Number / Time": "領収書番号 / 時間", "Invoice Status": "発行ステータス", "Invoice Store ID": "請求書ストアID", + "Issue a pharmacy pickup order, print the QR ticket, patient scans at the machine to dispense.": "薬品受取注文を発行し、QRチケットを印刷します。患者が機器でスキャンして払い出します。", + "Issue pharmacy pickup orders from the backend; patient scans QR to dispense.": "バックエンドで薬品受取注文を発行し、患者がQRをスキャンして払い出します。", "Issued": "Issued", "Issued At": "発行日時", "Item List": "商品リスト", @@ -1185,6 +1196,7 @@ "Max Capacity:": "最大容量:", "Max Stock": "在庫上限", "Maximum file size: 100MB": "Maximum file size: 100MB", + "Medicine": "薬品", "Member": "会員価格", "Member & External": "会員および外部システム", "Member + 1": "会員 + 1", @@ -1317,6 +1329,7 @@ "No machines assigned": "機器が割り当てられていません", "No machines available": "利用可能な機器がありません", "No machines available in this company.": "この会社で利用可能な機器がありません。", + "No machines enabled for pharmacy pickup. Enable it under Machine System Settings (Pickup Sheet mode → Pharmacy Pickup switch).": "薬品受取が有効な機器がありません。「機器システム設定」(受取シートモード → 薬品受取スイッチ)で有効にしてください。", "No machines found": "機器が見つかりません", "No machines in this status": "このステータスのデバイスはありません", "No machines match your criteria": "No machines match your criteria", @@ -1329,6 +1342,7 @@ "No operation logs found": "No operation logs found", "No pass codes found": "パスコードが見つかりません", "No permissions": "権限なし", + "No pharmacy pickup orders yet.": "薬品受取注文はまだありません。", "No pickup codes found": "受取コードが見つかりません", "No product data matching search criteria": "検索条件に一致する商品データはありません", "No product record found": "商品記録が見つかりません", @@ -1535,10 +1549,15 @@ "Permissions": "権限", "Permissions updated successfully": "権限が正常に更新されました", "Personnel assigned successfully": "担当者が正常に割り当てられました", + "Pharmacy Pickup (Rx)": "薬品受取", + "Pharmacy Pickup Orders": "薬品受取注文一覧", + "Pharmacy pickup order cancelled.": "薬品受取注文を取り消しました。", + "Pharmacy pickup order created: :no": "薬品受取注文を作成しました::no", "Phone": "電話番号", "Photo Slot": "写真スロット", "Pi Pay": "Pi 拍錢包 (Pi Pay)", "Pi Wallet": "Pi Wallet", + "Picked Up": "受取済み", "Picked up": "受取済み", "Picked up Time": "受取時間", "Pickup Code": "受取コード", @@ -1586,6 +1605,7 @@ "Please select a material": "素材を選択してください", "Please select a slot": "スロットを選択してください", "Please select an APK file to upload": "Please select an APK file to upload", + "Please select at least one medicine with quantity.": "数量を入力した薬品を少なくとも1つ選択してください。", "Please select warehouse and machine": "倉庫と機器を選択してください", "Please use our standard template to ensure data compatibility.": "データの互換性を確保するために、標準テンプレートを使用してください。", "PlusPay": "PlusPay", @@ -1606,6 +1626,7 @@ "Previous": "前へ", "Price / Member": "価格 / 会員価格", "Pricing Information": "価格情報", + "Pricing Slip No.": "請求伝票番号", "Primary Theme Color": "プライマリーテーマカラー", "Print": "印刷", "Print & PDF": "印刷 & PDF", @@ -1820,6 +1841,7 @@ "Scan QR Code": "QRコードをスキャン", "Scan Store ID": "スキャンStoreID", "Scan Term ID": "スキャンTermID", + "Scan the QR code or enter the pickup code at the machine to collect your medicine.": "機器でQRコードをスキャンするか、受取コードを入力して薬品を受け取ってください。", "Scan this code at the machine or share the link with the customer.": "機器でこのコードをスキャンするか、リンクを顧客と共有してください。", "Scan this code at the machine to authorize testing or maintenance.": "機器でこのコードをスキャンして、テストまたはメンテナンスを承認します。", "Scan this code to quickly access the maintenance form for this device.": "このQRコードをスキャンすると、このデバイスのメンテナンスフォームに素早くアクセスできます。", @@ -1973,6 +1995,7 @@ "Source Machine": "移動元自販機", "Source Warehouse": "移動元倉庫", "Source of temperature limit settings": "温度制限設定のソース", + "Spec": "規格", "Special Permission": "特別権限", "Specification": "仕様", "Specifications": "仕様", @@ -2137,8 +2160,10 @@ "This account does not belong to this company.": "このアカウントは該当企業に所属していません。", "This invoice has no printable copy": "この請求書は印刷可能な紙面がありません", "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 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.": "このロールは別の会社に属しているため、割り当てることができません。", "This slot has a pending command. Please wait.": "このスロットには実行中のコマンドがあります。しばらくお待ちください。", "This slot has a pending update. Please wait for the previous command to complete.": "このスロットには保留中の更新があります。前のコマンドが完了するまでお待ちください。", @@ -2453,6 +2478,7 @@ "e.g. Taiwan Star": "例:台湾星", "e.g. Test Code for Maintenance": "例:メンテナンステスト用コード", "e.g. johndoe": "例:yamadataro", + "e.g. the hospital pricing slip number": "例:病院の請求伝票番号", "e.g., 12345678": "例:12345678", "e.g., Beverage": "例:飲料", "e.g., Company Standard Pay": "例:会社標準決済", @@ -2528,6 +2554,7 @@ "menu.sales": "販売管理", "menu.sales.orders": "購入注文", "menu.sales.pass-codes": "パスコード", + "menu.sales.pharmacy-pickup": "薬品受取", "menu.sales.pickup-codes": "受取コード", "menu.sales.promotions": "プロモーション期間", "menu.sales.records": "販売記録", @@ -2558,12 +2585,14 @@ "name_dictionary_key": "name_dictionary_key", "of": "/全", "of items": "件中", + "optional": "任意", "orders": "件", "pending": "機器受取待機中", "permissions": "権限設定", "permissions.accounts": "アカウント管理", "permissions.companies": "顧客管理", "permissions.roles": "ロール権限管理", + "physical": "実在庫", "price": "price", "product": "product", "product_catalog_synced_to_all_machines": "product_catalog_synced_to_all_machines", @@ -2573,6 +2602,7 @@ "product_updated": "product_updated", "remote": "遠隔管理", "reservation": "予約システム", + "reserved": "予約", "roles": "ロール権限", "s": "秒", "sales": "販売管理", diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 8f06c72..0d7ca8d 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -1,4 +1,6 @@ { + "(deducted by issued pickup orders)": "(已扣除已生成領藥單的預留量)", + "-- Select Machine --": "-- 選擇機台 --", "10s": "10秒", "15 Seconds": "15 秒", "1920x1080 (Max 10MB)": "1920x1080 (最大 10MB)", @@ -216,9 +218,11 @@ "Auto-load succeeded but purchase failed": "自動加值成功,但購貨交易失敗", "Automatically calculate replenishment needs based on machine capacity": "根據機台容量自動計算補貨需求", "Availability": "可用性 (Availability)", + "Available": "可用庫存", "Available Machines": "可供分配的機台", "Avatar updated successfully.": "頭像已成功更新。", "Avg Cycle": "平均週期", + "Awaiting Pickup": "待領取", "Back to History": "返回紀錄", "Back to List": "返回列表", "Badge Settings": "識別證", @@ -265,6 +269,7 @@ "Cancel Pass Code": "取消通行碼", "Cancel Pickup Code": "取消取貨碼", "Cancel Purchase": "取消購買", + "Cancel this pharmacy pickup order?": "確定要作廢這張領藥單嗎?", "Cancelled": "已取消", "Cannot Delete Role": "無法刪除該角色", "Cannot cancel this order": "無法取消此訂單", @@ -424,10 +429,12 @@ "Courier/Dispatcher": "物流經辦/派送員", "Courier/Replenisher": "物流經辦/補貨員", "Create": "新增", + "Create & Generate QR": "建立並產生 QR", "Create Config": "建立配置", "Create Machine": "新增機台", "Create New Role": "建立新角色", "Create Payment Config": "建立金流配置", + "Create Pharmacy Pickup Order": "建立領藥單", "Create Product": "建立商品", "Create Replenishment Order": "建立補貨單", "Create Role": "建立角色", @@ -440,6 +447,7 @@ "Create stock replenishment for specific machines": "為指定機台建立庫存補貨單", "Create stock transfers between warehouses and machine returns": "建立倉庫間調撥或機台退庫單", "Create stock-in orders": "建立入庫單", + "Created": "建立時間", "Created At": "建立時間", "Created By": "建立者", "Creation Time": "建立時間", @@ -951,6 +959,7 @@ "Initial contract registration": "初始合約註冊", "Installation": "裝機", "Installation Location": "安裝位置", + "Insufficient stock for :name (requested :qty, available :available).": ":name 庫存不足(需求 :qty,可用 :available)。", "Insufficient stock for transfer": "庫存不足,無法調撥", "Insufficient stored-value / e-wallet balance": "電票/電子錢包餘額不足", "Invalid personnel selection": "無效的人員選擇", @@ -971,6 +980,8 @@ "Invoice Number / Time": "發票號碼 / 時間", "Invoice Status": "發票開立狀態", "Invoice Store ID": "發票商店代號", + "Issue a pharmacy pickup order, print the QR ticket, patient scans at the machine to dispense.": "開立領藥單、列印 QR 領藥單,病人到機台掃碼即可出貨。", + "Issue pharmacy pickup orders from the backend; patient scans QR to dispense.": "由後台開立領藥單,病人到機台掃 QR 出貨。", "Issued": "已開立", "Issued At": "開立時間", "Item List": "商品清單", @@ -1185,6 +1196,7 @@ "Max Capacity:": "最大容量:", "Max Stock": "庫存上限", "Maximum file size: 100MB": "檔案大小上限:100MB", + "Medicine": "藥品", "Member": "會員價", "Member & External": "會員與外部系統", "Member + 1": "會員 + 1", @@ -1317,6 +1329,7 @@ "No machines assigned": "未分配機台", "No machines available": "目前沒有可供分配的機台", "No machines available in this company.": "此客戶目前沒有可供分配的機台。", + "No machines enabled for pharmacy pickup. Enable it under Machine System Settings (Pickup Sheet mode → Pharmacy Pickup switch).": "尚無啟用領藥單的機台。請至「機台系統設定」(取物單模式 → 領藥單開關)啟用。", "No machines found": "未找到機台", "No machines in this status": "此狀態下無設備", "No machines match your criteria": "沒有符合條件的機台", @@ -1329,6 +1342,7 @@ "No operation logs found": "找不到操作紀錄", "No pass codes found": "找不到通行碼", "No permissions": "無權限項目", + "No pharmacy pickup orders yet.": "目前尚無領藥單。", "No pickup codes found": "找不到取貨碼", "No product data matching search criteria": "沒有符合搜尋條件的商品資料", "No product record found": "無商品紀錄", @@ -1535,10 +1549,15 @@ "Permissions": "權限", "Permissions updated successfully": "授權更新成功", "Personnel assigned successfully": "人員指派成功", + "Pharmacy Pickup (Rx)": "領藥單", + "Pharmacy Pickup Orders": "領藥單列表", + "Pharmacy pickup order cancelled.": "領藥單已作廢。", + "Pharmacy pickup order created: :no": "領藥單已建立::no", "Phone": "手機號碼", "Photo Slot": "照片欄位", "Pi Pay": "Pi 拍錢包", "Pi Wallet": "PI 拍錢包", + "Picked Up": "已領取", "Picked up": "領取", "Picked up Time": "領取時間", "Pickup Code": "取貨碼", @@ -1586,6 +1605,7 @@ "Please select a material": "請選擇素材", "Please select a slot": "請選擇貨道", "Please select an APK file to upload": "請選擇要上傳的 APK 檔案", + "Please select at least one medicine with quantity.": "請至少勾選一項藥品並填寫數量。", "Please select warehouse and machine": "請選擇倉庫和機台", "Please use our standard template to ensure data compatibility.": "請使用標準範例檔以確保資料相容性。", "PlusPay": "全盈+Pay", @@ -1606,6 +1626,7 @@ "Previous": "上一頁", "Price / Member": "售價 / 會員價", "Pricing Information": "價格資訊", + "Pricing Slip No.": "批價單號", "Primary Theme Color": "主題色系設定", "Print": "列印", "Print & PDF": "列印與 PDF", @@ -1820,6 +1841,7 @@ "Scan QR Code": "掃描 QR Code", "Scan Store ID": "掃碼StoreID", "Scan Term ID": "掃碼TermID", + "Scan the QR code or enter the pickup code at the machine to collect your medicine.": "請至機台掃描 QR 碼,或輸入領藥碼領取藥品。", "Scan this code at the machine or share the link with the customer.": "請在機台掃描此碼,或將連結分享給客戶。", "Scan this code at the machine to authorize testing or maintenance.": "請在機台上掃描此 QR Code 以進行測試或維護授權。", "Scan this code to quickly access the maintenance form for this device.": "掃描此 QR Code 即可快速進入此設備的維修單填寫頁面。", @@ -1973,6 +1995,7 @@ "Source Machine": "來源機台", "Source Warehouse": "來源倉庫", "Source of temperature limit settings": "溫度限制設定來源", + "Spec": "規格", "Special Permission": "特殊權限", "Specification": "規格", "Specifications": "規格", @@ -2137,8 +2160,10 @@ "This account does not belong to this company.": "此帳號不屬於該公司。", "This invoice has no printable copy": "此發票無可列印紙本", "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 no ECPay invoice settings": "此機台未設定綠界電子發票", + "This pharmacy pickup order can no longer be cancelled.": "此領藥單已無法作廢。", "This role belongs to another company and cannot be assigned.": "此角色屬於其他公司,無法指派。", "This slot has a pending command. Please wait.": "此貨道已有指令正在執行,請稍後。", "This slot has a pending update. Please wait for the previous command to complete.": "此貨道已有更新指令在執行中,請等候前一個指令完成。", @@ -2453,6 +2478,7 @@ "e.g. Taiwan Star": "例如:台灣之星", "e.g. Test Code for Maintenance": "例如:維修測試代碼", "e.g. johndoe": "例如:xiaoming", + "e.g. the hospital pricing slip number": "例如:醫院批價單上的單號", "e.g., 12345678": "例:12345678", "e.g., Beverage": "例如:飲料", "e.g., Company Standard Pay": "例如:公司標準支付", @@ -2528,6 +2554,7 @@ "menu.sales": "銷售管理", "menu.sales.orders": "購買單", "menu.sales.pass-codes": "通行碼", + "menu.sales.pharmacy-pickup": "領藥單", "menu.sales.pickup-codes": "取貨碼", "menu.sales.promotions": "促銷時段", "menu.sales.records": "銷售紀錄", @@ -2558,12 +2585,14 @@ "name_dictionary_key": "多語系鍵值", "of": "/共", "of items": "筆項目", + "optional": "選填", "orders": "筆", "pending": "等待機台領取", "permissions": "權限設定", "permissions.accounts": "帳號管理", "permissions.companies": "客戶管理", "permissions.roles": "角色權限管理", + "physical": "實體", "price": "售價", "product": "商品", "product_catalog_synced_to_all_machines": "商品目錄已同步至所有機台", @@ -2573,6 +2602,7 @@ "product_updated": "商品資訊已更新", "remote": "遠端管理", "reservation": "預約系統", + "reserved": "預留", "roles": "角色權限", "s": "秒", "sales": "銷售管理", diff --git a/package-lock.json b/package-lock.json index 6870d10..33afe57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "star-cloud", + "name": "html", "lockfileVersion": 3, "requires": true, "packages": { @@ -875,7 +875,6 @@ "integrity": "sha512-/VNHWYhNu+BS7ktbYoVGrCmsXDh+chFMaONMwGNdIBcFHrWqk2jY8fNyr3DLdtQUIalvkPfM554ZSFa3dm3nxQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/Fuzzyma" @@ -901,7 +900,6 @@ "integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 14.18" }, @@ -1138,7 +1136,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -1804,7 +1801,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -2109,7 +2105,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -2532,7 +2527,6 @@ "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -2629,7 +2623,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2717,7 +2710,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/resources/views/admin/basic-settings/machines/index.blade.php b/resources/views/admin/basic-settings/machines/index.blade.php index d4e55a2..c29ca86 100644 --- a/resources/views/admin/basic-settings/machines/index.blade.php +++ b/resources/views/admin/basic-settings/machines/index.blade.php @@ -204,6 +204,9 @@ pickup_code_enabled: settings.pickup_code_enabled === true || settings.pickup_code_enabled === 1 || settings.pickup_code_enabled === '1', pass_code_enabled: settings.pass_code_enabled === true || settings.pass_code_enabled === 1 || settings.pass_code_enabled === '1', + // 領藥單(取物單模式下的開關) + pharmacy_pickup_enabled: settings.pharmacy_pickup_enabled === true || settings.pharmacy_pickup_enabled === 1 || settings.pharmacy_pickup_enabled === '1', + // 零售附加功能 shopping_cart_enabled: machine.shopping_cart_enabled === true || machine.shopping_cart_enabled === 1 || machine.shopping_cart_enabled === '1', welcome_gift_enabled: machine.welcome_gift_enabled === true || machine.welcome_gift_enabled === 1 || machine.welcome_gift_enabled === '1', @@ -619,6 +622,25 @@ + +
+
+

{{ __('Pharmacy Pickup (Rx)') }}

+

{{ __('Issue pharmacy pickup orders from the backend; patient scans QR to dispense.') }}

+
+
+ +
+
+
diff --git a/resources/views/admin/basic-settings/machines/partials/tab-system-settings.blade.php b/resources/views/admin/basic-settings/machines/partials/tab-system-settings.blade.php index 0d11b43..20ffb18 100644 --- a/resources/views/admin/basic-settings/machines/partials/tab-system-settings.blade.php +++ b/resources/views/admin/basic-settings/machines/partials/tab-system-settings.blade.php @@ -99,6 +99,9 @@ $activeFeatures[] = __('Staff Card'); } elseif ($shoppingMode === 'pickup_sheet') { $activeFeatures[] = __('Pickup Sheet (Material No.)'); + if ($machine->settings['pharmacy_pickup_enabled'] ?? false) { + $activeFeatures[] = __('Pharmacy Pickup (Rx)'); + } } else { $activeFeatures[] = __('Basic Mode'); @@ -242,6 +245,9 @@ $activeFeatures[] = __('Staff Card'); } elseif ($shoppingMode === 'pickup_sheet') { $activeFeatures[] = __('Pickup Sheet (Material No.)'); + if ($machine->settings['pharmacy_pickup_enabled'] ?? false) { + $activeFeatures[] = __('Pharmacy Pickup (Rx)'); + } } else { $activeFeatures[] = __('Basic Mode'); diff --git a/resources/views/admin/sales/pharmacy-pickup/index.blade.php b/resources/views/admin/sales/pharmacy-pickup/index.blade.php new file mode 100644 index 0000000..8065d81 --- /dev/null +++ b/resources/views/admin/sales/pharmacy-pickup/index.blade.php @@ -0,0 +1,186 @@ +@extends('layouts.admin') + +@section('content') +@php + $statusLabels = [ + 'awaiting_pickup' => __('Awaiting Pickup'), + 'completed' => __('Picked Up'), + 'failed' => __('Voided'), + 'pending' => __('Pending'), + 'abandoned' => __('Abandoned'), + ]; + $statusColors = [ + 'awaiting_pickup' => 'bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400', + 'completed' => 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400', + 'failed' => 'bg-rose-100 text-rose-700 dark:bg-rose-500/10 dark:text-rose-400', + ]; +@endphp + +
+ +
+

{{ __('menu.sales.pharmacy-pickup') }}

+ {{ __('Issue a pharmacy pickup order, print the QR ticket, patient scans at the machine to dispense.') }} +
+ + @if(session('success')) +
{{ session('success') }}
+ @endif + @if(session('error')) +
{{ session('error') }}
+ @endif + @if($errors->any()) +
+
    + @foreach($errors->all() as $err)
  • {{ $err }}
  • @endforeach +
+
+ @endif + + {{-- 建立領藥單 --}} +
+

{{ __('Create Pharmacy Pickup Order') }}

+ + @if($machines->isEmpty()) +

{{ __('No machines enabled for pharmacy pickup. Enable it under Machine System Settings (Pickup Sheet mode → Pharmacy Pickup switch).') }}

+ @else + {{-- 選機台(變更即重新載入該機台可領藥品) --}} +
+
+ + +
+
+ + @if($selectedMachine) +
+ @csrf + + +
+ + +
+ + @if($products->isEmpty()) +

{{ __('This machine currently has no medicine in stock.') }}

+ @else +
+ + + + + + + + + + + + @foreach($products as $p) + + + + + + + + @endforeach + +
{{ __('Medicine') }}{{ __('Spec') }}{{ __('Available') }}
{{ __('(deducted by issued pickup orders)') }}
{{ __('Quantity') }}
+ + {{ $p->name }}{{ $p->spec ?: '-' }} + {{ $p->available }} + @if(($p->reserved ?? 0) > 0) + ({{ __('physical') }} {{ $p->physical }} − {{ __('reserved') }} {{ $p->reserved }}) + @endif + + +
+
+ + + @endif +
+ @endif + @endif +
+ + {{-- 領藥單列表 --}} +
+

{{ __('Pharmacy Pickup Orders') }}

+ +
+ + + + + + + + + + + + + + + @forelse($orders as $order) + + + + + + + + + + + @empty + + @endforelse + +
{{ __('Order No.') }}{{ __('Pricing Slip No.') }}{{ __('Machine') }}{{ __('Items') }}{{ __('Pickup Code') }}{{ __('Status') }}{{ __('Created') }}{{ __('Actions') }}
{{ $order->order_no }}{{ $order->pricing_slip_no ?: '-' }}{{ $order->machine?->name ?? '-' }} + @foreach($order->items as $it) +
{{ $it->product_name }} × {{ $it->quantity }}
+ @endforeach +
{{ $order->pickupCode?->code ?? '-' }} + {{ $statusLabels[$order->status] ?? $order->status }} + +
{{ $order->created_at?->format('Y-m-d H:i') }}
+
{{ $order->creator?->name }}
+
+
+ @if($order->status !== 'failed' && $order->pickupCode) + {{ __('Print') }} + @endif + @if($order->status === 'awaiting_pickup') +
+ @csrf + +
+ @endif + @if($order->status === 'failed' && !($order->pickupCode)) + - + @endif +
+
{{ __('No pharmacy pickup orders yet.') }}
+
+ +
{{ $orders->links() }}
+
+
+@endsection diff --git a/resources/views/admin/sales/pharmacy-pickup/print.blade.php b/resources/views/admin/sales/pharmacy-pickup/print.blade.php new file mode 100644 index 0000000..4aaaedb --- /dev/null +++ b/resources/views/admin/sales/pharmacy-pickup/print.blade.php @@ -0,0 +1,79 @@ + + + + + + {{ __('menu.sales.pharmacy-pickup') }} {{ $order->order_no }} + + + +
+ +
+ +
+
+

{{ __('menu.sales.pharmacy-pickup') }}

+
{{ $order->machine?->name }}@if($order->machine?->location) · {{ $order->machine->location }}@endif
+
+ +
{{ __('Order No.') }}{{ $order->order_no }}
+ @if($order->pricing_slip_no) +
{{ __('Pricing Slip No.') }}{{ $order->pricing_slip_no }}
+ @endif +
{{ __('Created') }}{{ $order->created_at?->format('Y-m-d H:i') }}
+ @if($order->pickupCode?->expires_at) +
{{ __('Valid Until') }}{{ $order->pickupCode->expires_at->format('Y-m-d H:i') }}
+ @endif + + + + + + + @foreach($order->items as $it) + + @endforeach + +
{{ __('Medicine') }}{{ __('Quantity') }}
{{ $it->product_name }}{{ $it->quantity }}
+ +
+
{{ __('Pickup Code') }}
+
{{ $order->pickupCode?->code ?? '--------' }}
+
+ + @if($qrSvg) +
{!! $qrSvg !!}
+ @endif + +
{{ __('Scan the QR code or enter the pickup code at the machine to collect your medicine.') }}
+
+ + diff --git a/resources/views/layouts/partials/sidebar-menu.blade.php b/resources/views/layouts/partials/sidebar-menu.blade.php index 434b3cf..8109f6c 100644 --- a/resources/views/layouts/partials/sidebar-menu.blade.php +++ b/resources/views/layouts/partials/sidebar-menu.blade.php @@ -216,6 +216,12 @@ {{ __('Store Gifts') }} @endcan + @can('menu.sales.pharmacy-pickup') +
  • + + {{ __('menu.sales.pharmacy-pickup') }} +
  • + @endcan
    diff --git a/routes/web.php b/routes/web.php index bfa8bd6..ea1a262 100644 --- a/routes/web.php +++ b/routes/web.php @@ -170,7 +170,13 @@ Route::middleware(['auth', 'auth.session', 'verified', 'tenant.access'])->prefix Route::post('/store-gifts', [App\Http\Controllers\Admin\SalesController::class, 'storeWelcomeGift'])->name('store-gifts.store'); Route::patch('/store-gifts/{welcomeGift}', [App\Http\Controllers\Admin\SalesController::class, 'updateWelcomeGift'])->name('store-gifts.update'); Route::delete('/store-gifts/{welcomeGift}', [App\Http\Controllers\Admin\SalesController::class, 'destroyWelcomeGift'])->name('store-gifts.destroy'); - + + // 領藥單(取物單模式 / 領藥模組)— 受權限 menu.sales.pharmacy-pickup 控管 + Route::get('/pharmacy-pickup', [App\Http\Controllers\Admin\PharmacyPickupController::class, 'index'])->name('pharmacy-pickup')->middleware('can:menu.sales.pharmacy-pickup'); + Route::post('/pharmacy-pickup', [App\Http\Controllers\Admin\PharmacyPickupController::class, 'store'])->name('pharmacy-pickup.store')->middleware('can:menu.sales.pharmacy-pickup'); + Route::get('/pharmacy-pickup/{order}/print', [App\Http\Controllers\Admin\PharmacyPickupController::class, 'print'])->name('pharmacy-pickup.print')->middleware('can:menu.sales.pharmacy-pickup'); + Route::post('/pharmacy-pickup/{order}/cancel', [App\Http\Controllers\Admin\PharmacyPickupController::class, 'cancel'])->name('pharmacy-pickup.cancel')->middleware('can:menu.sales.pharmacy-pickup'); + // 詳情路由必須放在最後,避免攔截其他具體路由 Route::get('/{order}', [App\Http\Controllers\Admin\SalesController::class, 'show'])->name('show'); });