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 旗標說明。
187 lines
13 KiB
PHP
187 lines
13 KiB
PHP
@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
|
||
|
||
<div class="space-y-6 pb-20">
|
||
|
||
<div class="flex items-center gap-3">
|
||
<h1 class="text-2xl font-black text-slate-800 dark:text-white tracking-tight">{{ __('menu.sales.pharmacy-pickup') }}</h1>
|
||
<span class="text-sm text-slate-400">{{ __('Issue a pharmacy pickup order, print the QR ticket, patient scans at the machine to dispense.') }}</span>
|
||
</div>
|
||
|
||
@if(session('success'))
|
||
<div class="px-4 py-3 rounded-xl bg-emerald-50 text-emerald-700 border border-emerald-200 dark:bg-emerald-500/10 dark:text-emerald-400 dark:border-emerald-500/20 text-sm font-bold">{{ session('success') }}</div>
|
||
@endif
|
||
@if(session('error'))
|
||
<div class="px-4 py-3 rounded-xl bg-rose-50 text-rose-700 border border-rose-200 dark:bg-rose-500/10 dark:text-rose-400 dark:border-rose-500/20 text-sm font-bold">{{ session('error') }}</div>
|
||
@endif
|
||
@if($errors->any())
|
||
<div class="px-4 py-3 rounded-xl bg-rose-50 text-rose-700 border border-rose-200 dark:bg-rose-500/10 dark:text-rose-400 dark:border-rose-500/20 text-sm">
|
||
<ul class="list-disc ps-5 space-y-1">
|
||
@foreach($errors->all() as $err)<li>{{ $err }}</li>@endforeach
|
||
</ul>
|
||
</div>
|
||
@endif
|
||
|
||
{{-- 建立領藥單 --}}
|
||
<div class="bg-white dark:bg-slate-800/60 rounded-2xl border border-slate-200 dark:border-slate-700/50 p-6 space-y-5">
|
||
<h2 class="text-lg font-black text-slate-700 dark:text-slate-200">{{ __('Create Pharmacy Pickup Order') }}</h2>
|
||
|
||
@if($machines->isEmpty())
|
||
<p class="text-sm text-amber-600 dark:text-amber-400">{{ __('No machines enabled for pharmacy pickup. Enable it under Machine System Settings (Pickup Sheet mode → Pharmacy Pickup switch).') }}</p>
|
||
@else
|
||
{{-- 選機台(變更即重新載入該機台可領藥品) --}}
|
||
<form method="GET" action="{{ route('admin.sales.pharmacy-pickup') }}" class="flex flex-wrap items-end gap-3">
|
||
<div class="flex flex-col gap-1">
|
||
<label class="text-xs font-black text-slate-400 uppercase tracking-widest">{{ __('Select Machine') }}</label>
|
||
<select name="machine_id" onchange="this.form.submit()"
|
||
class="luxury-input min-w-[260px] rounded-xl border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200 py-2.5 px-3 text-sm">
|
||
<option value="">{{ __('-- Select Machine --') }}</option>
|
||
@foreach($machines as $m)
|
||
<option value="{{ $m->id }}" @selected($selectedMachine && $selectedMachine->id === $m->id)>{{ $m->name }} ({{ $m->serial_no }})</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
</form>
|
||
|
||
@if($selectedMachine)
|
||
<form method="POST" action="{{ route('admin.sales.pharmacy-pickup.store') }}" class="space-y-4">
|
||
@csrf
|
||
<input type="hidden" name="machine_id" value="{{ $selectedMachine->id }}">
|
||
|
||
<div class="flex flex-col gap-1 max-w-xs">
|
||
<label class="text-xs font-black text-slate-400 uppercase tracking-widest">{{ __('Pricing Slip No.') }} <span class="text-slate-300 normal-case">({{ __('optional') }})</span></label>
|
||
<input type="text" name="pricing_slip_no" value="{{ old('pricing_slip_no') }}" maxlength="64"
|
||
class="luxury-input rounded-xl border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200 py-2.5 px-3 text-sm"
|
||
placeholder="{{ __('e.g. the hospital pricing slip number') }}">
|
||
</div>
|
||
|
||
@if($products->isEmpty())
|
||
<p class="text-sm text-slate-500">{{ __('This machine currently has no medicine in stock.') }}</p>
|
||
@else
|
||
<div class="overflow-x-auto rounded-xl border border-slate-200 dark:border-slate-700/50">
|
||
<table class="w-full text-sm">
|
||
<thead class="bg-slate-50 dark:bg-slate-900/40 text-slate-500 dark:text-slate-400">
|
||
<tr>
|
||
<th class="py-2.5 px-3 text-left w-10"></th>
|
||
<th class="py-2.5 px-3 text-left">{{ __('Medicine') }}</th>
|
||
<th class="py-2.5 px-3 text-left">{{ __('Spec') }}</th>
|
||
<th class="py-2.5 px-3 text-center">{{ __('Available') }}<br><span class="text-[10px] font-normal text-slate-400">{{ __('(deducted by issued pickup orders)') }}</span></th>
|
||
<th class="py-2.5 px-3 text-center w-32">{{ __('Quantity') }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-slate-100 dark:divide-slate-800">
|
||
@foreach($products as $p)
|
||
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/40">
|
||
<td class="py-2 px-3 text-center">
|
||
<input type="checkbox" name="products[]" value="{{ $p->product_id }}"
|
||
class="w-4 h-4 text-cyan-500 rounded border-slate-300 dark:border-slate-600">
|
||
</td>
|
||
<td class="py-2 px-3 font-bold text-slate-700 dark:text-slate-200">{{ $p->name }}</td>
|
||
<td class="py-2 px-3 text-slate-500">{{ $p->spec ?: '-' }}</td>
|
||
<td class="py-2 px-3 text-center text-slate-600 dark:text-slate-300">
|
||
<span class="font-bold">{{ $p->available }}</span>
|
||
@if(($p->reserved ?? 0) > 0)
|
||
<span class="text-[11px] text-slate-400">({{ __('physical') }} {{ $p->physical }} − {{ __('reserved') }} {{ $p->reserved }})</span>
|
||
@endif
|
||
</td>
|
||
<td class="py-2 px-3 text-center">
|
||
<input type="number" name="qty[{{ $p->product_id }}]" value="1" min="1" max="{{ $p->available }}"
|
||
class="w-20 text-center rounded-lg border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200 py-1.5 px-2 text-sm">
|
||
</td>
|
||
</tr>
|
||
@endforeach
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<button type="submit" class="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl bg-cyan-500 hover:bg-cyan-600 text-white font-bold text-sm transition-colors">
|
||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||
{{ __('Create & Generate QR') }}
|
||
</button>
|
||
@endif
|
||
</form>
|
||
@endif
|
||
@endif
|
||
</div>
|
||
|
||
{{-- 領藥單列表 --}}
|
||
<div class="bg-white dark:bg-slate-800/60 rounded-2xl border border-slate-200 dark:border-slate-700/50 p-6 space-y-4">
|
||
<h2 class="text-lg font-black text-slate-700 dark:text-slate-200">{{ __('Pharmacy Pickup Orders') }}</h2>
|
||
|
||
<div class="overflow-x-auto rounded-xl border border-slate-200 dark:border-slate-700/50">
|
||
<table class="w-full text-sm">
|
||
<thead class="bg-slate-50 dark:bg-slate-900/40 text-slate-500 dark:text-slate-400">
|
||
<tr>
|
||
<th class="py-2.5 px-3 text-left">{{ __('Order No.') }}</th>
|
||
<th class="py-2.5 px-3 text-left">{{ __('Pricing Slip No.') }}</th>
|
||
<th class="py-2.5 px-3 text-left">{{ __('Machine') }}</th>
|
||
<th class="py-2.5 px-3 text-left">{{ __('Items') }}</th>
|
||
<th class="py-2.5 px-3 text-center">{{ __('Pickup Code') }}</th>
|
||
<th class="py-2.5 px-3 text-center">{{ __('Status') }}</th>
|
||
<th class="py-2.5 px-3 text-left">{{ __('Created') }}</th>
|
||
<th class="py-2.5 px-3 text-center">{{ __('Actions') }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-slate-100 dark:divide-slate-800">
|
||
@forelse($orders as $order)
|
||
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/40 {{ session('created_order_id') == $order->id ? 'bg-emerald-50 dark:bg-emerald-500/5' : '' }}">
|
||
<td class="py-2.5 px-3 font-mono font-bold text-slate-700 dark:text-slate-200">{{ $order->order_no }}</td>
|
||
<td class="py-2.5 px-3 text-slate-500">{{ $order->pricing_slip_no ?: '-' }}</td>
|
||
<td class="py-2.5 px-3 text-slate-600 dark:text-slate-300">{{ $order->machine?->name ?? '-' }}</td>
|
||
<td class="py-2.5 px-3 text-slate-600 dark:text-slate-300">
|
||
@foreach($order->items as $it)
|
||
<div>{{ $it->product_name }} × {{ $it->quantity }}</div>
|
||
@endforeach
|
||
</td>
|
||
<td class="py-2.5 px-3 text-center font-mono font-bold text-cyan-600 dark:text-cyan-400">{{ $order->pickupCode?->code ?? '-' }}</td>
|
||
<td class="py-2.5 px-3 text-center">
|
||
<span class="inline-block px-2.5 py-1 rounded-full text-xs font-bold {{ $statusColors[$order->status] ?? 'bg-slate-100 text-slate-600' }}">{{ $statusLabels[$order->status] ?? $order->status }}</span>
|
||
</td>
|
||
<td class="py-2.5 px-3 text-slate-500 text-xs">
|
||
<div>{{ $order->created_at?->format('Y-m-d H:i') }}</div>
|
||
<div class="text-slate-400">{{ $order->creator?->name }}</div>
|
||
</td>
|
||
<td class="py-2.5 px-3 text-center">
|
||
<div class="flex items-center justify-center gap-3">
|
||
@if($order->status !== 'failed' && $order->pickupCode)
|
||
<a href="{{ route('admin.sales.pharmacy-pickup.print', $order) }}" target="_blank" class="text-xs font-bold text-cyan-600 hover:text-cyan-700">{{ __('Print') }}</a>
|
||
@endif
|
||
@if($order->status === 'awaiting_pickup')
|
||
<form method="POST" action="{{ route('admin.sales.pharmacy-pickup.cancel', $order) }}" onsubmit="return confirm('{{ __('Cancel this pharmacy pickup order?') }}')">
|
||
@csrf
|
||
<button type="submit" class="text-xs font-bold text-rose-500 hover:text-rose-600">{{ __('Cancel') }}</button>
|
||
</form>
|
||
@endif
|
||
@if($order->status === 'failed' && !($order->pickupCode))
|
||
<span class="text-slate-300">-</span>
|
||
@endif
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
@empty
|
||
<tr><td colspan="8" class="py-8 text-center text-slate-400">{{ __('No pharmacy pickup orders yet.') }}</td></tr>
|
||
@endforelse
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div>{{ $orders->links() }}</div>
|
||
</div>
|
||
</div>
|
||
@endsection
|