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 旗標說明。
141 lines
4.9 KiB
PHP
141 lines
4.9 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers\Admin;
|
||
|
||
use App\Models\Machine\Machine;
|
||
use App\Models\System\SystemOperationLog;
|
||
use App\Models\Transaction\Order;
|
||
use App\Services\Transaction\PharmacyPickupService;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Support\Collection;
|
||
use Illuminate\Support\Facades\Auth;
|
||
use Illuminate\Validation\ValidationException;
|
||
use SimpleSoftwareIO\QrCode\Facades\QrCode;
|
||
|
||
/**
|
||
* 領藥單管理(銷售管理 → 領藥單)。
|
||
*
|
||
* 授權雙閘:
|
||
* 1. 選單/頁面:權限 menu.sales.pharmacy-pickup(角色/帳號權限可勾選)。
|
||
* 2. 機台行為:機台系統設定 shopping_mode=pickup_sheet 且 pharmacy_pickup_enabled=true。
|
||
*/
|
||
class PharmacyPickupController extends AdminController
|
||
{
|
||
public function __construct(private PharmacyPickupService $service)
|
||
{
|
||
}
|
||
|
||
public function index(Request $request)
|
||
{
|
||
// 僅列出「取物單模式 + 領藥單開關開啟」的機台(受 TenantScoped 自動租戶隔離)
|
||
$machines = Machine::query()
|
||
->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.'));
|
||
}
|
||
}
|