[RELEASE] demo -> main:副櫃系統授權+交易total_amount修正+demo累積功能上正式
This commit is contained in:
commit
b77165d7b3
@ -95,6 +95,7 @@ class AnalysisController extends Controller
|
||||
7 => __('Welcome Gift'),
|
||||
8 => __('Questionnaire'),
|
||||
9 => __('Coin Acceptor'),
|
||||
10 => __('Mobile Pay'), // NFC 手機感應(手機支付)
|
||||
11 => __('QR Code Payment - TayPay'), // 掃碼支付-TayPay
|
||||
41 => __('Staff Card'),
|
||||
42 => __('Pickup Voucher'),
|
||||
|
||||
@ -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) {
|
||||
@ -405,6 +409,9 @@ class MachineSettingController extends AdminController
|
||||
|
||||
$jsonSettings['pickup_code_enabled'] = false;
|
||||
$jsonSettings['pass_code_enabled'] = false;
|
||||
|
||||
// 副櫃系統(格子櫃功能)僅基礎版可用,非基礎版一律關閉
|
||||
$jsonSettings['subcabinet_enabled'] = false;
|
||||
} else {
|
||||
$jsonSettings['credit_card_enabled'] = isset($settings['credit_card_enabled']) ? (bool)$settings['credit_card_enabled'] : false;
|
||||
$jsonSettings['mobile_pay_enabled'] = isset($settings['mobile_pay_enabled']) ? (bool)$settings['mobile_pay_enabled'] : false;
|
||||
@ -428,12 +435,20 @@ class MachineSettingController extends AdminController
|
||||
|
||||
$jsonSettings['pickup_code_enabled'] = isset($settings['pickup_code_enabled']) ? (bool)$settings['pickup_code_enabled'] : false;
|
||||
$jsonSettings['pass_code_enabled'] = isset($settings['pass_code_enabled']) ? (bool)$settings['pass_code_enabled'] : false;
|
||||
|
||||
// 副櫃系統(格子櫃功能):基礎版可設定
|
||||
$jsonSettings['subcabinet_enabled'] = isset($settings['subcabinet_enabled']) ? (bool)$settings['subcabinet_enabled'] : false;
|
||||
}
|
||||
|
||||
foreach ($allowedFields as $field) {
|
||||
$jsonSettings[$field] = $data[$field];
|
||||
}
|
||||
|
||||
// 領藥單模組:僅在「取物單(pickup_sheet)」模式下可啟用,其他模式一律關閉
|
||||
$jsonSettings['pharmacy_pickup_enabled'] = ($shoppingMode === 'pickup_sheet')
|
||||
? (bool) ($settings['pharmacy_pickup_enabled'] ?? false)
|
||||
: false;
|
||||
|
||||
// 顯示語系 (languages):機台螢幕可切換的語系,最多 N 種,僅系統管理員可設定。
|
||||
// 商品翻譯範圍由「公司機台語系聯集」反推,故此處異動需失效聯集快取並重建商品目錄。
|
||||
// 非系統管理員即使送出 languages 亦一律忽略(不報錯,讓其仍可儲存其他設定)。
|
||||
|
||||
154
app/Http/Controllers/Admin/PharmacyPickupController.php
Normal file
154
app/Http/Controllers/Admin/PharmacyPickupController.php
Normal file
@ -0,0 +1,154 @@
|
||||
<?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,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX:回傳某機台目前可領藥品(建立領藥單彈窗用,對齊取貨碼 slots-ajax 模式)。
|
||||
*/
|
||||
public function productsAjax(Machine $machine)
|
||||
{
|
||||
$enabled = ($machine->settings['shopping_mode'] ?? null) === 'pickup_sheet'
|
||||
&& (bool) ($machine->settings['pharmacy_pickup_enabled'] ?? false);
|
||||
abort_unless($enabled, 403);
|
||||
|
||||
return response()->json([
|
||||
'products' => $this->service->availableProducts($machine)->values(),
|
||||
]);
|
||||
}
|
||||
|
||||
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.'));
|
||||
}
|
||||
}
|
||||
@ -507,6 +507,8 @@ 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), // 領藥單(雲端建單,取物單模式下開關)
|
||||
'Subcabinet' => (bool) ($s['subcabinet_enabled'] ?? false), // 副櫃系統(格子櫃功能,基礎版授權開關)
|
||||
];
|
||||
|
||||
// 5-4 ShoppingMode:購物方式(頂層字串)
|
||||
@ -696,17 +698,92 @@ 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);
|
||||
}
|
||||
|
||||
// 不在「驗證/掃碼」當下核銷領藥碼。核銷統一改由機台實際出貨後的
|
||||
// MQTT finalize(TransactionService::finalizePharmacyDispense)標記為 used,
|
||||
// 這樣顧客掃碼後若未按「確定取貨」(逾時/取消、未出貨),領藥碼仍維持 active 可再次掃碼領取。
|
||||
// (原本在此處 usage_count++/轉 used 會在尚未取貨時就把碼鎖死,導致再掃顯示「已領」。)
|
||||
|
||||
$data['order_type'] = \App\Models\Transaction\Order::TYPE_PHARMACY_PICKUP;
|
||||
$data['order_no'] = $order->order_no;
|
||||
$data['pricing_slip_no'] = $order->pricing_slip_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,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -79,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 = 無到期限制
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -406,10 +406,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;
|
||||
|
||||
304
app/Services/Transaction/PharmacyPickupService.php
Normal file
304
app/Services/Transaction/PharmacyPickupService.php
Normal file
@ -0,0 +1,304 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Transaction;
|
||||
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\Machine\MachineSlot;
|
||||
use App\Models\Product\Product;
|
||||
use App\Models\System\SystemOperationLog;
|
||||
use App\Models\Transaction\Order;
|
||||
use App\Models\Transaction\PickupCode;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/**
|
||||
* 領藥單建立服務。
|
||||
*
|
||||
* 設計重點(對齊交叉檢查修正):
|
||||
* - 領藥單序號 = orders.flow_id(唯一),讓後續出貨 finalize 能套用既有終態冪等保護。
|
||||
* - 建單時「不」預扣庫存;庫存僅在機台出貨 finalize 時扣一次(避免雙重扣減)。
|
||||
* - 以「商品」為單位建單,貨道於 B660 掃碼時再由 product_id × machine_slots 解析(一張單可多貨道)。
|
||||
*/
|
||||
class PharmacyPickupService
|
||||
{
|
||||
/** 領藥單支付類型碼:沿用 42 = 領藥憑證 (Pickup Voucher)。 */
|
||||
public const PAYMENT_TYPE_PHARMACY = 42;
|
||||
|
||||
/**
|
||||
* 取得某機台「已生成但尚未領取(awaiting_pickup)」的領藥單預留量(依商品彙總)。
|
||||
* 用於建單時「預扣」顯示與防超領:可用 = 實體庫存 − 預留量。
|
||||
*/
|
||||
public function reservedQtyMap(Machine $machine): array
|
||||
{
|
||||
$map = [];
|
||||
$orders = Order::where('machine_id', $machine->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<array{product_id:int, quantity:int}>, 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;
|
||||
}
|
||||
}
|
||||
@ -118,6 +118,11 @@ class TransactionService
|
||||
'payment_request' => $data['payment_request'] ?? $order->payment_request,
|
||||
'payment_response' => $data['payment_response'] ?? $order->payment_response,
|
||||
'pay_amount' => $data['pay_amount'] ?? $order->pay_amount,
|
||||
// 商品總額/原始金額:completed finalize 才帶正確金額(pending 階段機台送 0),
|
||||
// 須比照 pay_amount 在轉移時一併更新,否則後台銷售紀錄金額會停在建立時的 0。
|
||||
'total_amount' => $data['total_amount'] ?? $order->total_amount,
|
||||
'original_amount' => $data['original_amount'] ?? $order->original_amount,
|
||||
'discount_amount' => $data['discount_amount'] ?? $order->discount_amount,
|
||||
'change_amount' => $data['change_amount'] ?? $order->change_amount,
|
||||
'points_used' => $data['points_used'] ?? $order->points_used,
|
||||
'invoice_info' => $data['invoice_info'] ?? $order->invoice_info,
|
||||
@ -303,18 +308,40 @@ 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;
|
||||
// 售完(庫存歸 0)即清空效期與批號,避免舊批貨效期殘留。
|
||||
// 場景:日後管理員只補 stock 未改 expiry_date 時,update_inventory 才不會
|
||||
// 把過期日一起下發、把新批貨誤鎖成「暫不販售」。與機台端「賣完清效期」對稱。
|
||||
// 純加法:僅在仍有殘留值時才寫,不影響既有扣庫存行為。
|
||||
if ($slot->stock <= 0 && ($slot->expiry_date !== null || $slot->batch_no !== null)) {
|
||||
$slot->update([
|
||||
'expiry_date' => null,
|
||||
'batch_no' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
$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]
|
||||
);
|
||||
$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]
|
||||
);
|
||||
} 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 +394,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 +585,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.
|
||||
*/
|
||||
|
||||
@ -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' => [
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 領藥單模組:orders 增加訂單類型/批價單號/建單者欄位。
|
||||
* order_type 預設 sale,既有資料一律視為一般銷售,不影響線上行為。
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('orders', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('orders', 'order_type')) {
|
||||
$table->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');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 領藥單一張單多貨道:slot_no 改為可空。
|
||||
* 一般取貨碼仍寫入 slot_no(單貨道);領藥單的多貨道由 order_items × machine_slots 解析,
|
||||
* 故 slot_no 允許為 null,讀取端依 order_type 區分。
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('pickup_codes', function (Blueprint $table) {
|
||||
$table->string('slot_no')->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('pickup_codes', function (Blueprint $table) {
|
||||
$table->string('slot_no')->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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',
|
||||
|
||||
30
lang/en.json
30
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)",
|
||||
@ -218,9 +220,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",
|
||||
@ -267,6 +271,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",
|
||||
@ -426,10 +431,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",
|
||||
@ -442,6 +449,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",
|
||||
@ -956,6 +964,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",
|
||||
@ -976,6 +985,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",
|
||||
@ -1199,6 +1210,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",
|
||||
@ -1331,6 +1343,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",
|
||||
@ -1344,6 +1357,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",
|
||||
@ -1554,10 +1568,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",
|
||||
@ -1605,6 +1624,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",
|
||||
@ -1625,6 +1645,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",
|
||||
@ -1844,6 +1865,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.",
|
||||
@ -1997,6 +2019,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",
|
||||
@ -2161,8 +2184,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.",
|
||||
@ -2478,6 +2503,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",
|
||||
@ -2553,6 +2579,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",
|
||||
@ -2583,6 +2610,7 @@
|
||||
"name_dictionary_key": "name_dictionary_key",
|
||||
"of": "of",
|
||||
"of items": "items",
|
||||
"optional": "optional",
|
||||
"orders": "orders",
|
||||
"overrides set": "overrides set",
|
||||
"pending": "Pending",
|
||||
@ -2590,6 +2618,7 @@
|
||||
"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",
|
||||
@ -2599,6 +2628,7 @@
|
||||
"product_updated": "product_updated",
|
||||
"remote": "Remote Management",
|
||||
"reservation": "Reservation System",
|
||||
"reserved": "reserved",
|
||||
"roles": "Roles",
|
||||
"s": "s",
|
||||
"sales": "Sales Management",
|
||||
|
||||
30
lang/ja.json
30
lang/ja.json
@ -1,4 +1,6 @@
|
||||
{
|
||||
"(deducted by issued pickup orders)": "(発行済み受取注文の予約分を差し引き済み)",
|
||||
"-- Select Machine --": "-- 機器を選択 --",
|
||||
"10s": "10秒",
|
||||
"15 Seconds": "15秒",
|
||||
"1920x1080 (Max 10MB)": "1920x1080 (最大10MB)",
|
||||
@ -218,9 +220,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": "バッジ設定",
|
||||
@ -267,6 +271,7 @@
|
||||
"Cancel Pass Code": "パスコードキャンセル",
|
||||
"Cancel Pickup Code": "受取コードキャンセル",
|
||||
"Cancel Purchase": "購入キャンセル",
|
||||
"Cancel this pharmacy pickup order?": "この薬品受取注文を取り消しますか?",
|
||||
"Cancelled": "キャンセル済み",
|
||||
"Cannot Delete Role": "ロールを削除できません",
|
||||
"Cannot cancel this order": "この注文はキャンセルできません",
|
||||
@ -426,10 +431,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": "ロール作成",
|
||||
@ -442,6 +449,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": "作成時間",
|
||||
@ -956,6 +964,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": "無効な担当者選択",
|
||||
@ -976,6 +985,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": "商品リスト",
|
||||
@ -1199,6 +1210,7 @@
|
||||
"Max Capacity:": "最大容量:",
|
||||
"Max Stock": "在庫上限",
|
||||
"Maximum file size: 100MB": "Maximum file size: 100MB",
|
||||
"Medicine": "薬品",
|
||||
"Member": "会員価格",
|
||||
"Member & External": "会員および外部システム",
|
||||
"Member + 1": "会員 + 1",
|
||||
@ -1331,6 +1343,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",
|
||||
@ -1344,6 +1357,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": "商品記録が見つかりません",
|
||||
@ -1554,10 +1568,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": "受取コード",
|
||||
@ -1605,6 +1624,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",
|
||||
@ -1625,6 +1645,7 @@
|
||||
"Previous": "前へ",
|
||||
"Price / Member": "価格 / 会員価格",
|
||||
"Pricing Information": "価格情報",
|
||||
"Pricing Slip No.": "請求伝票番号",
|
||||
"Primary Theme Color": "プライマリーテーマカラー",
|
||||
"Print": "印刷",
|
||||
"Print & PDF": "印刷 & PDF",
|
||||
@ -1844,6 +1865,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コードをスキャンすると、このデバイスのメンテナンスフォームに素早くアクセスできます。",
|
||||
@ -1997,6 +2019,7 @@
|
||||
"Source Machine": "移動元自販機",
|
||||
"Source Warehouse": "移動元倉庫",
|
||||
"Source of temperature limit settings": "温度制限設定のソース",
|
||||
"Spec": "規格",
|
||||
"Special Permission": "特別権限",
|
||||
"Specification": "仕様",
|
||||
"Specifications": "仕様",
|
||||
@ -2161,8 +2184,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.": "このスロットには保留中の更新があります。前のコマンドが完了するまでお待ちください。",
|
||||
@ -2478,6 +2503,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": "例:会社標準決済",
|
||||
@ -2553,6 +2579,7 @@
|
||||
"menu.sales": "販売管理",
|
||||
"menu.sales.orders": "購入注文",
|
||||
"menu.sales.pass-codes": "パスコード",
|
||||
"menu.sales.pharmacy-pickup": "薬品受取",
|
||||
"menu.sales.pickup-codes": "受取コード",
|
||||
"menu.sales.promotions": "プロモーション期間",
|
||||
"menu.sales.records": "販売記録",
|
||||
@ -2583,6 +2610,7 @@
|
||||
"name_dictionary_key": "name_dictionary_key",
|
||||
"of": "/全",
|
||||
"of items": "件中",
|
||||
"optional": "任意",
|
||||
"orders": "件",
|
||||
"overrides set": "件のマシン価格を設定済み",
|
||||
"pending": "機器受取待機中",
|
||||
@ -2590,6 +2618,7 @@
|
||||
"permissions.accounts": "アカウント管理",
|
||||
"permissions.companies": "顧客管理",
|
||||
"permissions.roles": "ロール権限管理",
|
||||
"physical": "実在庫",
|
||||
"price": "price",
|
||||
"product": "product",
|
||||
"product_catalog_synced_to_all_machines": "product_catalog_synced_to_all_machines",
|
||||
@ -2599,6 +2628,7 @@
|
||||
"product_updated": "product_updated",
|
||||
"remote": "遠隔管理",
|
||||
"reservation": "予約システム",
|
||||
"reserved": "予約",
|
||||
"roles": "ロール権限",
|
||||
"s": "秒",
|
||||
"sales": "販売管理",
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
{
|
||||
"(deducted by issued pickup orders)": "(已扣除已生成領藥單的預留量)",
|
||||
"-- Select Machine --": "-- 選擇機台 --",
|
||||
"10s": "10秒",
|
||||
"15 Seconds": "15 秒",
|
||||
"1920x1080 (Max 10MB)": "1920x1080 (最大 10MB)",
|
||||
@ -218,9 +220,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": "識別證",
|
||||
@ -267,6 +271,7 @@
|
||||
"Cancel Pass Code": "取消通行碼",
|
||||
"Cancel Pickup Code": "取消取貨碼",
|
||||
"Cancel Purchase": "取消購買",
|
||||
"Cancel this pharmacy pickup order?": "確定要作廢這張領藥單嗎?",
|
||||
"Cancelled": "已取消",
|
||||
"Cannot Delete Role": "無法刪除該角色",
|
||||
"Cannot cancel this order": "無法取消此訂單",
|
||||
@ -426,10 +431,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": "建立角色",
|
||||
@ -442,6 +449,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": "建立時間",
|
||||
@ -956,6 +964,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": "無效的人員選擇",
|
||||
@ -976,6 +985,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": "商品清單",
|
||||
@ -1199,6 +1210,7 @@
|
||||
"Max Capacity:": "最大容量:",
|
||||
"Max Stock": "庫存上限",
|
||||
"Maximum file size: 100MB": "檔案大小上限:100MB",
|
||||
"Medicine": "藥品",
|
||||
"Member": "會員價",
|
||||
"Member & External": "會員與外部系統",
|
||||
"Member + 1": "會員 + 1",
|
||||
@ -1331,6 +1343,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": "沒有符合條件的機台",
|
||||
@ -1344,6 +1357,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": "無商品紀錄",
|
||||
@ -1554,10 +1568,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": "取貨碼",
|
||||
@ -1605,6 +1624,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",
|
||||
@ -1625,6 +1645,7 @@
|
||||
"Previous": "上一頁",
|
||||
"Price / Member": "售價 / 會員價",
|
||||
"Pricing Information": "價格資訊",
|
||||
"Pricing Slip No.": "批價單號",
|
||||
"Primary Theme Color": "主題色系設定",
|
||||
"Print": "列印",
|
||||
"Print & PDF": "列印與 PDF",
|
||||
@ -1844,6 +1865,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 即可快速進入此設備的維修單填寫頁面。",
|
||||
@ -1997,6 +2019,7 @@
|
||||
"Source Machine": "來源機台",
|
||||
"Source Warehouse": "來源倉庫",
|
||||
"Source of temperature limit settings": "溫度限制設定來源",
|
||||
"Spec": "規格",
|
||||
"Special Permission": "特殊權限",
|
||||
"Specification": "規格",
|
||||
"Specifications": "規格",
|
||||
@ -2161,8 +2184,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.": "此貨道已有更新指令在執行中,請等候前一個指令完成。",
|
||||
@ -2478,6 +2503,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": "例如:公司標準支付",
|
||||
@ -2553,6 +2579,7 @@
|
||||
"menu.sales": "銷售管理",
|
||||
"menu.sales.orders": "購買單",
|
||||
"menu.sales.pass-codes": "通行碼",
|
||||
"menu.sales.pharmacy-pickup": "領藥單",
|
||||
"menu.sales.pickup-codes": "取貨碼",
|
||||
"menu.sales.promotions": "促銷時段",
|
||||
"menu.sales.records": "銷售紀錄",
|
||||
@ -2583,6 +2610,7 @@
|
||||
"name_dictionary_key": "多語系鍵值",
|
||||
"of": "/共",
|
||||
"of items": "筆項目",
|
||||
"optional": "選填",
|
||||
"orders": "筆",
|
||||
"overrides set": "項已設定機台價",
|
||||
"pending": "等待機台領取",
|
||||
@ -2590,6 +2618,7 @@
|
||||
"permissions.accounts": "帳號管理",
|
||||
"permissions.companies": "客戶管理",
|
||||
"permissions.roles": "角色權限管理",
|
||||
"physical": "實體",
|
||||
"price": "售價",
|
||||
"product": "商品",
|
||||
"product_catalog_synced_to_all_machines": "商品目錄已同步至所有機台",
|
||||
@ -2599,6 +2628,7 @@
|
||||
"product_updated": "商品資訊已更新",
|
||||
"remote": "遠端管理",
|
||||
"reservation": "預約系統",
|
||||
"reserved": "預留",
|
||||
"roles": "角色權限",
|
||||
"s": "秒",
|
||||
"sales": "銷售管理",
|
||||
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -204,6 +204,12 @@
|
||||
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',
|
||||
|
||||
// 副櫃系統(格子櫃功能授權,基礎版專用)
|
||||
subcabinet_enabled: settings.subcabinet_enabled === true || settings.subcabinet_enabled === 1 || settings.subcabinet_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 +625,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 取物單模式專屬:領藥單開關 (toggle switch,比照會員系統/稅務系統) -->
|
||||
<div x-show="machineSettings.shopping_mode === 'pickup_sheet'" x-transition class="grid grid-cols-1 md:grid-cols-4 gap-4 items-center p-4 rounded-xl bg-slate-50/50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-700/50 border-l-4 border-l-emerald-500 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors">
|
||||
<div class="md:col-span-1">
|
||||
<h4 class="text-sm font-black text-slate-700 dark:text-slate-200">{{ __('Pharmacy Pickup (Rx)') }}</h4>
|
||||
<p class="text-xs text-slate-400 dark:text-slate-500 mt-0.5">{{ __('Issue pharmacy pickup orders from the backend; patient scans QR to dispense.') }}</p>
|
||||
</div>
|
||||
<div class="md:col-span-3 flex flex-wrap gap-x-8 gap-y-4">
|
||||
<label class="flex items-center gap-3 cursor-pointer group">
|
||||
<div class="relative inline-flex items-center">
|
||||
<input type="hidden" name="settings[pharmacy_pickup_enabled]" value="0">
|
||||
<input type="checkbox" name="settings[pharmacy_pickup_enabled]" value="1"
|
||||
x-model="machineSettings.pharmacy_pickup_enabled" class="peer sr-only">
|
||||
<div class="w-9 h-5 bg-slate-200 peer-focus:outline-none rounded-full peer dark:bg-slate-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-cyan-500"></div>
|
||||
</div>
|
||||
<span class="text-sm font-bold text-slate-600 dark:text-slate-300 group-hover:text-slate-900 dark:group-hover:text-white transition-colors">{{ __('Pharmacy Pickup (Rx)') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 零售/金流展開部分 (僅基礎版顯示) -->
|
||||
<div x-show="machineSettings.shopping_mode === 'basic'" x-transition class="space-y-3 rounded-2xl border border-slate-200 dark:border-slate-700/60 border-l-4 border-l-cyan-500 bg-slate-50/40 dark:bg-slate-800/20 p-4">
|
||||
|
||||
@ -933,6 +958,24 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 副櫃系統(格子櫃功能授權;機台端據此顯示副櫃分頁/鎖控板設定/貨道管理) -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 items-center p-4 rounded-xl bg-slate-50/50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-700/50 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors">
|
||||
<div class="md:col-span-1">
|
||||
<h4 class="text-sm font-black text-slate-700 dark:text-slate-200">副櫃系統</h4>
|
||||
</div>
|
||||
<div class="md:col-span-3 flex flex-wrap gap-x-8 gap-y-4">
|
||||
<label class="flex items-center gap-3 cursor-pointer group">
|
||||
<div class="relative inline-flex items-center">
|
||||
<input type="hidden" name="settings[subcabinet_enabled]" value="0">
|
||||
<input type="checkbox" name="settings[subcabinet_enabled]" value="1"
|
||||
x-model="machineSettings.subcabinet_enabled" class="peer sr-only">
|
||||
<div class="w-9 h-5 bg-slate-200 peer-focus:outline-none rounded-full peer dark:bg-slate-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-cyan-500"></div>
|
||||
</div>
|
||||
<span class="text-sm font-bold text-slate-600 dark:text-slate-300 group-hover:text-slate-900 dark:group-hover:text-white transition-colors">格子櫃功能</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<!-- 顯示語系(最多 N 種,第一個為預設)。僅系統管理員可設定。 -->
|
||||
<div class="p-4 rounded-xl bg-slate-50/50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-700/50">
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -172,6 +175,7 @@
|
||||
|
||||
if ($machine->shopping_cart_enabled) $activeFeatures[] = __('Shopping Cart');
|
||||
if ($machine->welcome_gift_enabled) $activeFeatures[] = __('Welcome Gift');
|
||||
if ($machine->settings['subcabinet_enabled'] ?? false) $activeFeatures[] = '副櫃系統';
|
||||
}
|
||||
|
||||
if ($machine->member_system_enabled) $activeFeatures[] = __('Member System');
|
||||
@ -242,6 +246,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');
|
||||
|
||||
@ -315,6 +322,7 @@
|
||||
|
||||
if ($machine->shopping_cart_enabled) $activeFeatures[] = __('Shopping Cart');
|
||||
if ($machine->welcome_gift_enabled) $activeFeatures[] = __('Welcome Gift');
|
||||
if ($machine->settings['subcabinet_enabled'] ?? false) $activeFeatures[] = '副櫃系統';
|
||||
}
|
||||
|
||||
if ($machine->member_system_enabled) $activeFeatures[] = __('Member System');
|
||||
|
||||
459
resources/views/admin/sales/pharmacy-pickup/index.blade.php
Normal file
459
resources/views/admin/sales/pharmacy-pickup/index.blade.php
Normal file
@ -0,0 +1,459 @@
|
||||
@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-500/10 border border-amber-500/20 text-amber-600 dark:text-amber-400',
|
||||
'completed' => 'bg-emerald-500/10 border border-emerald-500/20 text-emerald-600 dark:text-emerald-400',
|
||||
'failed' => 'bg-rose-500/10 border border-rose-500/20 text-rose-600 dark:text-rose-400',
|
||||
'pending' => 'bg-slate-500/10 border border-slate-500/20 text-slate-500',
|
||||
'abandoned' => 'bg-slate-500/10 border border-slate-500/20 text-slate-500',
|
||||
];
|
||||
@endphp
|
||||
|
||||
<div class="space-y-4 pb-20" x-data="pharmacyPickupManager()">
|
||||
|
||||
{{-- Page Header --}}
|
||||
<x-page-header
|
||||
:title="__('menu.sales.pharmacy-pickup')"
|
||||
:subtitle="__('Issue a pharmacy pickup order, print the QR ticket, patient scans at the machine to dispense.')">
|
||||
@if($machines->isNotEmpty())
|
||||
<button @click="openCreateModal()"
|
||||
class="btn-luxury-primary whitespace-nowrap flex items-center gap-2 transition-all duration-300">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14M5 12h14" />
|
||||
</svg>
|
||||
<span class="text-sm sm:text-base">{{ __('Create Pharmacy Pickup Order') }}</span>
|
||||
</button>
|
||||
@endif
|
||||
</x-page-header>
|
||||
|
||||
{{-- Flash / Errors --}}
|
||||
@if(session('success'))
|
||||
<div class="p-5 bg-emerald-500/10 border border-emerald-500/20 text-emerald-600 dark:text-emerald-400 rounded-2xl flex items-center gap-3 font-bold text-sm animate-luxury-in">
|
||||
<svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div class="p-5 bg-rose-500/10 border border-rose-500/20 text-rose-600 dark:text-rose-400 rounded-2xl flex items-center gap-3 font-bold text-sm animate-luxury-in">
|
||||
<svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 9v2m0 4h.01M5.07 19h13.86a2 2 0 001.74-3L13.74 4a2 2 0 00-3.48 0L3.33 16a2 2 0 001.74 3z"/></svg>
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
@if($errors->any())
|
||||
<div class="p-5 bg-rose-500/10 border border-rose-500/20 text-rose-600 dark:text-rose-400 rounded-2xl text-sm animate-luxury-in">
|
||||
<ul class="list-disc ps-5 space-y-1 font-bold">
|
||||
@foreach($errors->all() as $err)<li>{{ $err }}</li>@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($machines->isEmpty())
|
||||
<div class="p-5 bg-amber-500/10 border border-amber-500/20 text-amber-600 dark:text-amber-400 rounded-2xl flex items-start gap-4 font-bold text-sm">
|
||||
<svg class="w-5 h-5 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 9v2m0 4h.01M5.07 19h13.86a2 2 0 001.74-3L13.74 4a2 2 0 00-3.48 0L3.33 16a2 2 0 001.74 3z"/></svg>
|
||||
{{ __('No machines enabled for pharmacy pickup. Enable it under Machine System Settings (Pickup Sheet mode → Pharmacy Pickup switch).') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 領藥單列表 --}}
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in relative overflow-hidden">
|
||||
|
||||
{{-- Desktop Table --}}
|
||||
<div class="hidden xl:block overflow-x-auto">
|
||||
<table class="w-full text-left border-separate border-spacing-0">
|
||||
<thead>
|
||||
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Order No.') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Machine') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Items') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Pickup Code') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Status') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Created') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100 dark:divide-slate-800/80">
|
||||
@forelse($orders as $order)
|
||||
<tr class="group hover:bg-slate-50/50 dark:hover:bg-white/[0.02] transition-colors duration-200 {{ session('created_order_id') == $order->id ? 'bg-emerald-50/60 dark:bg-emerald-500/5' : '' }}">
|
||||
<td class="px-6 py-6 whitespace-nowrap">
|
||||
<div class="text-base font-black font-mono text-slate-800 dark:text-slate-100 tracking-tight">{{ $order->order_no }}</div>
|
||||
@if($order->pricing_slip_no)
|
||||
<div class="text-[10px] font-bold text-slate-400 uppercase tracking-[0.2em] mt-0.5">{{ __('Pricing Slip No.') }}: {{ $order->pricing_slip_no }}</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
<div class="text-base font-extrabold text-slate-800 dark:text-slate-100 tracking-tight">{{ $order->machine?->name ?? '-' }}</div>
|
||||
<div class="text-[10px] font-mono font-bold text-slate-400 uppercase tracking-[0.2em]">{{ $order->machine?->serial_no }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
<div class="space-y-0.5">
|
||||
@foreach($order->items as $it)
|
||||
<div class="text-sm font-bold text-slate-700 dark:text-slate-300">{{ $it->product_name }} <span class="text-cyan-600 dark:text-cyan-400 font-black">× {{ $it->quantity }}</span></div>
|
||||
@endforeach
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-center whitespace-nowrap">
|
||||
@if($order->pickupCode?->code)
|
||||
<span class="text-base font-black font-mono text-cyan-600 dark:text-cyan-400 tracking-widest bg-cyan-50 dark:bg-cyan-500/10 px-3 py-1.5 rounded-xl border border-cyan-100 dark:border-cyan-500/20">{{ $order->pickupCode->code }}</span>
|
||||
@else
|
||||
<span class="text-slate-300 dark:text-slate-600">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-6 text-center whitespace-nowrap">
|
||||
<span class="inline-block px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest {{ $statusColors[$order->status] ?? 'bg-slate-500/10 border border-slate-500/20 text-slate-500' }}">{{ $statusLabels[$order->status] ?? $order->status }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-6 whitespace-nowrap">
|
||||
<div class="text-sm font-black text-slate-600 dark:text-slate-300 font-mono">{{ $order->created_at?->format('Y-m-d H:i') }}</div>
|
||||
<div class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-0.5">{{ $order->creator?->name }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 whitespace-nowrap text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
@if($order->status !== 'failed' && $order->pickupCode)
|
||||
<a href="{{ route('admin.sales.pharmacy-pickup.print', $order) }}" target="_blank"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all"
|
||||
title="{{ __('Print') }}">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0110.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0l.229 2.523a1.125 1.125 0 01-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0021 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 00-1.913-.247M6.34 18H5.25A2.25 2.25 0 013 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 011.913-.247m10.5 0a48.536 48.536 0 00-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5zm-3 0h.008v.008H15V10.5z"/></svg>
|
||||
</a>
|
||||
@endif
|
||||
@if($order->status === 'awaiting_pickup')
|
||||
<form id="pp-cancel-desktop-{{ $order->id }}" method="POST" action="{{ route('admin.sales.pharmacy-pickup.cancel', $order) }}">
|
||||
@csrf
|
||||
<button type="button" @click="confirmCancel('pp-cancel-desktop-{{ $order->id }}')"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 hover:bg-rose-500/5 border border-transparent hover:border-rose-500/20 transition-all"
|
||||
title="{{ __('Cancel') }}">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-3.38a2.25 2.25 0 00-2.25-2.25h-3.51a2.25 2.25 0 00-2.25 2.25v3.38"/></svg>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
@if($order->status !== 'awaiting_pickup' && !($order->status !== 'failed' && $order->pickupCode))
|
||||
<span class="text-slate-300 dark:text-slate-600 pr-2">-</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<x-empty-state mode="table" :colspan="7" :message="__('No pharmacy pickup orders yet.')" />
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- Mobile Card Grid --}}
|
||||
<div class="xl:hidden grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
@forelse($orders as $order)
|
||||
<div class="luxury-card p-6 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group {{ session('created_order_id') == $order->id ? 'ring-2 ring-emerald-500/30' : '' }}">
|
||||
{{-- Header --}}
|
||||
<div class="flex items-start justify-between gap-4 mb-6">
|
||||
<div class="flex items-center gap-4 min-w-0">
|
||||
<div class="w-14 h-14 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300 shadow-sm shrink-0">
|
||||
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h3 class="text-base font-extrabold text-slate-800 dark:text-slate-100 truncate tracking-tight font-mono">{{ $order->order_no }}</h3>
|
||||
<p class="text-xs font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest truncate">{{ $order->machine?->name ?? '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="inline-block px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest shrink-0 {{ $statusColors[$order->status] ?? 'bg-slate-500/10 border border-slate-500/20 text-slate-500' }}">{{ $statusLabels[$order->status] ?? $order->status }}</span>
|
||||
</div>
|
||||
|
||||
{{-- Info Grid --}}
|
||||
<div class="grid grid-cols-2 gap-y-4 mb-6 border-y border-slate-100 dark:border-slate-800/50 py-4">
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Pickup Code') }}</p>
|
||||
<p class="text-sm font-black font-mono text-cyan-600 dark:text-cyan-400 tracking-widest">{{ $order->pickupCode?->code ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Pricing Slip No.') }}</p>
|
||||
<p class="text-sm font-bold text-slate-700 dark:text-slate-300">{{ $order->pricing_slip_no ?: '-' }}</p>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Items') }}</p>
|
||||
<div class="space-y-0.5">
|
||||
@foreach($order->items as $it)
|
||||
<p class="text-sm font-bold text-slate-700 dark:text-slate-300">{{ $it->product_name }} <span class="text-cyan-600 dark:text-cyan-400 font-black">× {{ $it->quantity }}</span></p>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Created') }}</p>
|
||||
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 font-mono">{{ $order->created_at?->format('Y-m-d H:i') }} <span class="text-slate-400 font-sans">· {{ $order->creator?->name }}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Actions --}}
|
||||
@if(($order->status !== 'failed' && $order->pickupCode) || $order->status === 'awaiting_pickup')
|
||||
<div class="flex items-center gap-3">
|
||||
@if($order->status !== 'failed' && $order->pickupCode)
|
||||
<a href="{{ route('admin.sales.pharmacy-pickup.print', $order) }}" target="_blank"
|
||||
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-black text-xs uppercase tracking-widest border border-slate-100 dark:border-slate-800 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all duration-300">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0110.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0l.229 2.523a1.125 1.125 0 01-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0021 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 00-1.913-.247M6.34 18H5.25A2.25 2.25 0 013 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 011.913-.247"/></svg>
|
||||
{{ __('Print') }}
|
||||
</a>
|
||||
@endif
|
||||
@if($order->status === 'awaiting_pickup')
|
||||
<form id="pp-cancel-mobile-{{ $order->id }}" method="POST" action="{{ route('admin.sales.pharmacy-pickup.cancel', $order) }}" class="flex-1">
|
||||
@csrf
|
||||
<button type="button" @click="confirmCancel('pp-cancel-mobile-{{ $order->id }}')"
|
||||
class="w-full flex items-center justify-center gap-2 py-3 rounded-xl bg-rose-500/5 text-rose-500 text-xs font-black uppercase tracking-widest border border-rose-500/20 hover:bg-rose-500 hover:text-white transition-all duration-300">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79"/></svg>
|
||||
{{ __('Cancel') }}
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<div class="col-span-full">
|
||||
<x-empty-state :message="__('No pharmacy pickup orders yet.')" />
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
{{-- Pagination --}}
|
||||
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
|
||||
{{ $orders->appends(request()->query())->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Create Pharmacy Pickup Order Modal --}}
|
||||
<div x-show="showCreateModal" class="fixed inset-0 z-50 overflow-y-auto"
|
||||
x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak style="display:none;">
|
||||
<div class="flex items-center justify-center min-h-screen px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div class="fixed inset-0 transition-opacity bg-slate-900/60 backdrop-blur-sm" @click="showCreateModal = false"></div>
|
||||
|
||||
<div x-show="showCreateModal" x-transition:enter="ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 translate-y-4 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 translate-y-4 sm:scale-95"
|
||||
class="inline-block px-8 py-10 text-left align-bottom transition-all transform luxury-card rounded-3xl dark:bg-slate-900 border-slate-200/50 dark:border-slate-700/50 shadow-2xl sm:my-8 sm:align-middle sm:max-w-3xl sm:w-full overflow-visible">
|
||||
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Create Pharmacy Pickup Order') }}</h3>
|
||||
<button @click="showCreateModal = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('admin.sales.pharmacy-pickup.store') }}" method="POST" class="space-y-6"
|
||||
@submit.prevent="validateAndSubmit" x-ref="createForm">
|
||||
@csrf
|
||||
<input type="hidden" name="machine_id" :value="selectedMachine">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{{-- Machine --}}
|
||||
<div class="space-y-2 relative focus-within:z-[60]">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Select Machine') }} <span class="text-rose-500">*</span></label>
|
||||
<x-searchable-select name="machine_select" id="pp-machine"
|
||||
placeholder="{{ __('-- Select Machine --') }}" x-model="selectedMachine"
|
||||
@change="selectedMachine = $event.target.value; fetchProducts()">
|
||||
@foreach($machines as $m)
|
||||
<option value="{{ $m->id }}" data-title="{{ $m->name }} ({{ $m->serial_no }})">{{ $m->name }} ({{ $m->serial_no }})</option>
|
||||
@endforeach
|
||||
</x-searchable-select>
|
||||
</div>
|
||||
|
||||
{{-- Pricing Slip No. --}}
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Pricing Slip No.') }} <span class="text-slate-300 normal-case font-bold">({{ __('optional') }})</span></label>
|
||||
<input type="text" name="pricing_slip_no" x-model="pricingSlipNo" maxlength="64"
|
||||
@keydown.enter.prevent
|
||||
class="luxury-input w-full py-3 px-4 text-sm" placeholder="{{ __('e.g. the hospital pricing slip number') }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Medicine selection --}}
|
||||
<div class="space-y-3">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Medicine') }} <span class="text-rose-500">*</span></label>
|
||||
|
||||
{{-- Empty: no machine selected --}}
|
||||
<div x-show="!selectedMachine" class="rounded-[2rem] border border-dashed border-slate-200 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-900/30 p-10 text-center">
|
||||
<p class="text-sm font-bold text-slate-400">{{ __('Please select a machine') }}</p>
|
||||
</div>
|
||||
|
||||
{{-- Loading --}}
|
||||
<div x-show="selectedMachine && loadingProducts" class="rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/30 p-10 flex items-center justify-center">
|
||||
<x-luxury-spinner show="loadingProducts" />
|
||||
</div>
|
||||
|
||||
{{-- No products --}}
|
||||
<div x-show="selectedMachine && !loadingProducts && products.length === 0" class="rounded-[2rem] border border-amber-500/20 bg-amber-500/5 p-8 text-center">
|
||||
<p class="text-sm font-bold text-amber-600 dark:text-amber-400">{{ __('This machine currently has no medicine in stock.') }}</p>
|
||||
</div>
|
||||
|
||||
{{-- Product table --}}
|
||||
<div x-show="selectedMachine && !loadingProducts && products.length > 0"
|
||||
class="overflow-hidden rounded-[2rem] border border-slate-100 dark:border-slate-800">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-slate-50/70 dark:bg-slate-900/40">
|
||||
<tr>
|
||||
<th class="py-3 px-4 text-left w-10"></th>
|
||||
<th class="py-3 px-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Medicine') }}</th>
|
||||
<th class="py-3 px-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Spec') }}</th>
|
||||
<th class="py-3 px-4 text-center text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Available') }}</th>
|
||||
<th class="py-3 px-4 text-center text-[10px] font-black text-slate-400 uppercase tracking-widest w-40">{{ __('Quantity') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100 dark:divide-slate-800/80">
|
||||
<template x-for="p in products" :key="p.product_id">
|
||||
<tr class="transition-colors duration-200"
|
||||
:class="p.checked ? 'bg-cyan-50/40 dark:bg-cyan-500/5' : 'hover:bg-slate-50/50 dark:hover:bg-white/[0.02]'">
|
||||
<td class="py-3 px-4 text-center">
|
||||
<input type="checkbox" x-model="p.checked"
|
||||
class="w-4 h-4 text-cyan-500 rounded border-slate-300 dark:border-slate-600 focus:ring-cyan-500/30 cursor-pointer">
|
||||
</td>
|
||||
<td class="py-3 px-4 font-bold text-slate-700 dark:text-slate-200" x-text="p.name"></td>
|
||||
<td class="py-3 px-4 text-slate-500" x-text="p.spec || '-'"></td>
|
||||
<td class="py-3 px-4 text-center">
|
||||
<span class="font-black text-slate-700 dark:text-slate-200" x-text="p.available"></span>
|
||||
<template x-if="p.reserved > 0">
|
||||
<span class="block text-[10px] font-bold text-slate-400" x-text="'({{ __('physical') }} ' + p.physical + ' − {{ __('reserved') }} ' + p.reserved + ')'"></span>
|
||||
</template>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
{{-- Luxury +/- counter --}}
|
||||
<div class="flex items-center h-10 mx-auto rounded-xl border border-slate-200/70 dark:border-slate-700/50 bg-white dark:bg-slate-900/50 overflow-hidden w-[140px] transition-all"
|
||||
:class="{ 'opacity-40 pointer-events-none': !p.checked }">
|
||||
<button type="button" @click="p.qty > 1 ? p.qty-- : 1"
|
||||
class="shrink-0 w-10 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M20 12H4"/></svg>
|
||||
</button>
|
||||
<div class="h-5 w-px bg-slate-200 dark:bg-slate-700/50"></div>
|
||||
<input type="text" x-model.number="p.qty" readonly
|
||||
class="flex-1 w-full min-w-0 bg-transparent border-none text-center text-base font-black text-slate-800 dark:text-white focus:ring-0 p-0">
|
||||
<div class="h-5 w-px bg-slate-200 dark:bg-slate-700/50"></div>
|
||||
<button type="button" @click="p.qty < p.available ? p.qty++ : p.available"
|
||||
class="shrink-0 w-10 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 5v14M5 12h14"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
{{-- Submitted only when checked (empty name = skipped) --}}
|
||||
<input type="hidden" :name="p.checked ? 'products[]' : ''" :value="p.product_id">
|
||||
<input type="hidden" :name="p.checked ? `qty[${p.product_id}]` : ''" :value="p.qty">
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="text-[10px] font-bold text-slate-400 pl-1">{{ __('(deducted by issued pickup orders)') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-x-4 pt-4">
|
||||
<button type="button" @click="showCreateModal = false" class="btn-luxury-ghost px-8">{{ __('Cancel') }}</button>
|
||||
<button type="submit" class="btn-luxury-primary px-12 flex items-center gap-2">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
{{ __('Create & Generate QR') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Confirm Cancel Modal --}}
|
||||
<x-confirm-modal alpineVar="showCancelModal" :title="__('Cancel Pickup Code')"
|
||||
:message="__('Cancel this pharmacy pickup order?')"
|
||||
:confirmText="__('Yes, Cancel')" confirmAction="submitCancel()" confirmColor="rose" iconType="danger" />
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('pharmacyPickupManager', () => ({
|
||||
showCreateModal: false,
|
||||
selectedMachine: '',
|
||||
pricingSlipNo: '',
|
||||
products: [],
|
||||
loadingProducts: false,
|
||||
showCancelModal: false,
|
||||
cancelTargetForm: null,
|
||||
|
||||
openCreateModal() {
|
||||
this.selectedMachine = '';
|
||||
this.pricingSlipNo = '';
|
||||
this.products = [];
|
||||
this.showCreateModal = true;
|
||||
this.syncSelect('pp-machine', ' ');
|
||||
},
|
||||
|
||||
async fetchProducts() {
|
||||
if (!this.selectedMachine) {
|
||||
this.products = [];
|
||||
return;
|
||||
}
|
||||
this.loadingProducts = true;
|
||||
try {
|
||||
const res = await fetch(`/admin/sales/pharmacy-pickup/${this.selectedMachine}/products-ajax`, {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
|
||||
});
|
||||
const data = await res.json();
|
||||
this.products = (data.products || []).map(p => ({
|
||||
...p,
|
||||
checked: false,
|
||||
qty: 1,
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error('Error fetching products:', e);
|
||||
window.showToast?.('{{ __("Loading failed") }}', 'error');
|
||||
this.products = [];
|
||||
} finally {
|
||||
this.loadingProducts = false;
|
||||
}
|
||||
},
|
||||
|
||||
validateAndSubmit() {
|
||||
if (!this.selectedMachine || this.selectedMachine.toString().trim() === '') {
|
||||
window.showToast?.('{{ __("Please select a machine") }}', 'error');
|
||||
return;
|
||||
}
|
||||
if (!this.products.some(p => p.checked)) {
|
||||
window.showToast?.('{{ __("Please select at least one medicine with quantity.") }}', 'error');
|
||||
return;
|
||||
}
|
||||
this.$refs.createForm.submit();
|
||||
},
|
||||
|
||||
confirmCancel(formId) {
|
||||
this.cancelTargetForm = formId;
|
||||
this.showCancelModal = true;
|
||||
},
|
||||
|
||||
submitCancel() {
|
||||
if (this.cancelTargetForm) {
|
||||
const form = document.getElementById(this.cancelTargetForm);
|
||||
if (form) form.submit();
|
||||
}
|
||||
this.showCancelModal = false;
|
||||
},
|
||||
|
||||
syncSelect(id, value) {
|
||||
this.$nextTick(() => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
const valStr = (value !== undefined && value !== null && value.toString().trim() !== '') ? value.toString() : ' ';
|
||||
el.value = valStr;
|
||||
window.HSSelect?.getInstance(el)?.setValue(valStr);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
init() {
|
||||
this.$watch('selectedMachine', (val) => {
|
||||
this.syncSelect('pp-machine', val);
|
||||
});
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
79
resources/views/admin/sales/pharmacy-pickup/print.blade.php
Normal file
79
resources/views/admin/sales/pharmacy-pickup/print.blade.php
Normal file
@ -0,0 +1,79 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ __('menu.sales.pharmacy-pickup') }} {{ $order->order_no }}</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: "Microsoft JhengHei", "PingFang TC", "Noto Sans TC", sans-serif; margin: 0; background: #f3f4f6; color: #111827; }
|
||||
.actions { text-align: center; margin: 16px auto; }
|
||||
.btn { display: inline-block; padding: 9px 22px; background: #06b6d4; color: #fff; border: none; border-radius: 8px; font-size: 14px; font-weight: 700; cursor: pointer; text-decoration: none; }
|
||||
.ticket { width: 360px; margin: 12px auto 28px; background: #fff; padding: 22px 24px; border: 1px solid #e5e7eb; border-radius: 8px; }
|
||||
.head { text-align: center; border-bottom: 2px dashed #9ca3af; padding-bottom: 12px; margin-bottom: 12px; }
|
||||
.head h1 { font-size: 22px; margin: 0 0 4px; letter-spacing: 4px; }
|
||||
.head .sub { font-size: 12px; color: #6b7280; }
|
||||
.row { display: flex; justify-content: space-between; gap: 12px; font-size: 13px; margin: 4px 0; }
|
||||
.row .k { color: #6b7280; white-space: nowrap; }
|
||||
.row .v { font-weight: 600; text-align: right; word-break: break-all; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 12px 0 6px; font-size: 13px; }
|
||||
th, td { text-align: left; padding: 6px 4px; border-bottom: 1px solid #f0f0f0; }
|
||||
th { color: #6b7280; font-weight: 700; border-bottom: 1px solid #d1d5db; }
|
||||
th.qty, td.qty { text-align: center; width: 56px; }
|
||||
.code-box { text-align: center; margin: 16px 0 4px; }
|
||||
.code-box .label { font-size: 12px; color: #6b7280; }
|
||||
.code-box .code { font-size: 32px; font-weight: 800; letter-spacing: 8px; font-family: "Courier New", monospace; }
|
||||
.qr { text-align: center; margin: 8px 0; }
|
||||
.qr svg { width: 200px; height: 200px; }
|
||||
.note { font-size: 12px; color: #4b5563; text-align: center; margin-top: 12px; border-top: 2px dashed #9ca3af; padding-top: 12px; line-height: 1.6; }
|
||||
@media print {
|
||||
body { background: #fff; }
|
||||
.no-print { display: none !important; }
|
||||
.ticket { border: none; margin: 0 auto; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="actions no-print">
|
||||
<button class="btn" onclick="window.print()">{{ __('Print') }}</button>
|
||||
</div>
|
||||
|
||||
<div class="ticket">
|
||||
<div class="head">
|
||||
<h1>{{ __('menu.sales.pharmacy-pickup') }}</h1>
|
||||
<div class="sub">{{ $order->machine?->name }}@if($order->machine?->location) · {{ $order->machine->location }}@endif</div>
|
||||
</div>
|
||||
|
||||
<div class="row"><span class="k">{{ __('Order No.') }}</span><span class="v">{{ $order->order_no }}</span></div>
|
||||
@if($order->pricing_slip_no)
|
||||
<div class="row"><span class="k">{{ __('Pricing Slip No.') }}</span><span class="v">{{ $order->pricing_slip_no }}</span></div>
|
||||
@endif
|
||||
<div class="row"><span class="k">{{ __('Created') }}</span><span class="v">{{ $order->created_at?->format('Y-m-d H:i') }}</span></div>
|
||||
@if($order->pickupCode?->expires_at)
|
||||
<div class="row"><span class="k">{{ __('Valid Until') }}</span><span class="v">{{ $order->pickupCode->expires_at->format('Y-m-d H:i') }}</span></div>
|
||||
@endif
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>{{ __('Medicine') }}</th><th class="qty">{{ __('Quantity') }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($order->items as $it)
|
||||
<tr><td>{{ $it->product_name }}</td><td class="qty">{{ $it->quantity }}</td></tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="code-box">
|
||||
<div class="label">{{ __('Pickup Code') }}</div>
|
||||
<div class="code">{{ $order->pickupCode?->code ?? '--------' }}</div>
|
||||
</div>
|
||||
|
||||
@if($qrSvg)
|
||||
<div class="qr">{!! $qrSvg !!}</div>
|
||||
@endif
|
||||
|
||||
<div class="note">{{ __('Scan the QR code or enter the pickup code at the machine to collect your medicine.') }}</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -121,6 +121,7 @@
|
||||
'promotions' => __('Promotions'),
|
||||
'pass-codes' => __('Pass Codes'),
|
||||
'store-gifts' => __('Store Gifts'),
|
||||
'pharmacy-pickup' => __('menu.sales.pharmacy-pickup'),
|
||||
'change-stock' => __('Change Stock'),
|
||||
'machine-reports' => __('Machine Reports'),
|
||||
'product-reports' => __('Product Reports'),
|
||||
|
||||
@ -216,6 +216,12 @@
|
||||
<span class="whitespace-nowrap overflow-hidden transition-all duration-300" :class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">{{ __('Store Gifts') }}</span>
|
||||
</a></li>
|
||||
@endcan
|
||||
@can('menu.sales.pharmacy-pickup')
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.pharmacy-pickup') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.pharmacy-pickup') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-3-3v6m-7 4h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v11a2 2 0 002 2z" /></svg>
|
||||
<span class="whitespace-nowrap overflow-hidden transition-all duration-300" :class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">{{ __('menu.sales.pharmacy-pickup') }}</span>
|
||||
</a></li>
|
||||
@endcan
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@ -174,7 +174,14 @@ 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/{machine}/products-ajax', [App\Http\Controllers\Admin\PharmacyPickupController::class, 'productsAjax'])->name('pharmacy-pickup.products-ajax')->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');
|
||||
});
|
||||
|
||||
101
tests/Feature/Transaction/ExpiryClearOnSelloutMysqlTest.php
Normal file
101
tests/Feature/Transaction/ExpiryClearOnSelloutMysqlTest.php
Normal file
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Transaction;
|
||||
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\System\Company;
|
||||
use App\Services\Transaction\TransactionService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* 售完(庫存歸 0)自動清空效期/批號 —— 需 MySQL(recordDispense 內部寫
|
||||
* machine_stock_movements,其 type enum 為 MySQL-only DDL,sqlite :memory: 無法驗)。執行:
|
||||
*
|
||||
* ./vendor/bin/sail test -c phpunit.mysql.xml --filter ExpiryClearOnSelloutMysqlTest
|
||||
*
|
||||
* 在預設 sqlite 套件中自動 skip,不影響主套件綠燈。
|
||||
*
|
||||
* 動機:機台端「賣完清效期」已做,但 B009 補貨上報不回傳 expiry,後台若殘留舊效期,
|
||||
* 日後管理員只補 stock 未改效期 → update_inventory 把過期日下發 → 新批貨被誤鎖。
|
||||
* 故後台扣庫存歸 0 時亦須清效期,與機台端對稱。
|
||||
*/
|
||||
class ExpiryClearOnSelloutMysqlTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private Company $company;
|
||||
private TransactionService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
if (DB::connection()->getDriverName() !== 'mysql') {
|
||||
$this->markTestSkipped('需 MySQL:machine_stock_movements.type enum 為 MySQL-only DDL。');
|
||||
}
|
||||
|
||||
$this->company = Company::create(['name' => 'Test Company']);
|
||||
$this->service = app(TransactionService::class);
|
||||
}
|
||||
|
||||
private function dispense(string $serial, string $slotNo): array
|
||||
{
|
||||
return [
|
||||
'serial_no' => $serial,
|
||||
'slot_no' => $slotNo,
|
||||
'product_id' => 101,
|
||||
'amount' => 50,
|
||||
'dispense_status' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
/** 庫存賣到 0 → 效期與批號被清空。 */
|
||||
public function test_clears_expiry_and_batch_when_stock_hits_zero(): void
|
||||
{
|
||||
$serial = '1234567890';
|
||||
$machine = Machine::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'serial_no' => $serial,
|
||||
]);
|
||||
$slot = $machine->slots()->create([
|
||||
'slot_no' => '1',
|
||||
'stock' => 1,
|
||||
'max_stock' => 10,
|
||||
'expiry_date' => '2030-12-31',
|
||||
'batch_no' => 'B-001',
|
||||
]);
|
||||
|
||||
$this->service->recordDispense($this->dispense($serial, '1'));
|
||||
|
||||
$fresh = $slot->fresh();
|
||||
$this->assertEquals(0, $fresh->stock, '庫存應扣為 0');
|
||||
$this->assertNull($fresh->expiry_date, '售完應清空效期');
|
||||
$this->assertNull($fresh->batch_no, '售完應清空批號');
|
||||
}
|
||||
|
||||
/** 庫存尚未歸 0 → 效期與批號保留(同批貨還在販售)。 */
|
||||
public function test_keeps_expiry_when_stock_remains(): void
|
||||
{
|
||||
$serial = '1234567891';
|
||||
$machine = Machine::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'serial_no' => $serial,
|
||||
]);
|
||||
$slot = $machine->slots()->create([
|
||||
'slot_no' => '2',
|
||||
'stock' => 5,
|
||||
'max_stock' => 10,
|
||||
'expiry_date' => '2030-12-31',
|
||||
'batch_no' => 'B-001',
|
||||
]);
|
||||
|
||||
$this->service->recordDispense($this->dispense($serial, '2'));
|
||||
|
||||
$fresh = $slot->fresh();
|
||||
$this->assertEquals(4, $fresh->stock, '庫存應扣為 4');
|
||||
$this->assertEquals('2030-12-31', $fresh->expiry_date->format('Y-m-d'), '未售完應保留效期');
|
||||
$this->assertEquals('B-001', $fresh->batch_no, '未售完應保留批號');
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user