[RELEASE] demo -> main: 機台貨道明細匯出 + 出貨失敗補償取貨碼 + 機台庫存概覽批次匯出
1. 機台貨道明細匯出(單機台):機台詳細視圖矩陣/表格旁「匯出」下拉(CSV/Excel) 2. 出貨失敗補償取貨碼:銷售紀錄「取貨碼」欄,付款成功但出貨失敗的單「+」→確認→產碼(七天有效)→待使用 3. 機台庫存概覽批次匯出:列表頁「匯出」鈕→全選/勾選→Excel 多分頁(每台一分頁)
This commit is contained in:
commit
07684bbb90
@ -164,6 +164,11 @@ class SalesController extends Controller
|
|||||||
$data['invoices'] = $invoicesQuery->latest()->paginate($perPage, ['*'], 'invoices_page')->withQueryString();
|
$data['invoices'] = $invoicesQuery->latest()->paginate($perPage, ['*'], 'invoices_page')->withQueryString();
|
||||||
$data['dispenseLogs'] = $dispenseQuery->latest()->paginate($perPage, ['*'], 'dispense_page')->withQueryString();
|
$data['dispenseLogs'] = $dispenseQuery->latest()->paginate($perPage, ['*'], 'dispense_page')->withQueryString();
|
||||||
|
|
||||||
|
// 出貨失敗補償取貨碼:僅掛載既有碼供顯示;產碼由 + 按鈕手動觸發 generateOrderPickupCode()
|
||||||
|
if ($tab === 'orders') {
|
||||||
|
$this->attachOrderPickupCodes($data['orders']->getCollection());
|
||||||
|
}
|
||||||
|
|
||||||
if ($isAjax) {
|
if ($isAjax) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
@ -175,6 +180,98 @@ class SalesController extends Controller
|
|||||||
return view('admin.sales.index', $data);
|
return view('admin.sales.index', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 掛載訂單的補償取貨碼(batch_no='COMP{id}')供銷售頁顯示。只讀不產碼。
|
||||||
|
*/
|
||||||
|
private function attachOrderPickupCodes($orders): void
|
||||||
|
{
|
||||||
|
if ($orders->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$batchNos = $orders->map(fn ($o) => 'COMP' . $o->id)->all();
|
||||||
|
$codesByBatch = PickupCode::whereIn('batch_no', $batchNos)->get()->groupBy('batch_no');
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
$order->setRelation('compensationCodes', $codesByBatch->get('COMP' . $order->id, collect()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手動產生「出貨失敗補償取貨碼」:由銷售頁 + 按鈕、前端確認後 POST 觸發。
|
||||||
|
* 僅限「付款成功(completed/paid)但出貨失敗/部分(delivery 0/2)」的訂單;依每商品缺量各產一碼
|
||||||
|
* (綁機台+商品、7 天有效、usage_limit=缺量);冪等(batch_no='COMP{id}')。
|
||||||
|
* 不設 order_id——取貨碼核銷時 order_id 會被回填為「新出貨單」。
|
||||||
|
*/
|
||||||
|
public function generateOrderPickupCode(Request $request, Order $order)
|
||||||
|
{
|
||||||
|
if (!Auth::user()->isSystemAdmin() && $order->machine_id && !Machine::whereKey($order->machine_id)->exists()) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$isPaid = in_array($order->status, [Order::STATUS_COMPLETED, 'paid'], true);
|
||||||
|
$failedDelivery = in_array((int) $order->delivery_status, [Order::DELIVERY_STATUS_FAILED, Order::DELIVERY_STATUS_PARTIAL], true);
|
||||||
|
if (!$isPaid || !$failedDelivery || !$order->machine_id) {
|
||||||
|
return response()->json(['success' => false, 'message' => __('This order is not eligible for a pickup code.')], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$batchNo = 'COMP' . $order->id;
|
||||||
|
$existing = PickupCode::where('batch_no', $batchNo)->get();
|
||||||
|
if ($existing->isNotEmpty()) {
|
||||||
|
return response()->json(['success' => true, 'message' => __('Pickup code already generated.'), 'codes' => $existing->pluck('code')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$machine = $order->machine ?: Machine::find($order->machine_id);
|
||||||
|
if (!$machine) {
|
||||||
|
return response()->json(['success' => false, 'message' => __('Machine not found.')], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ordered = $order->items->groupBy('product_id')->map(fn ($g) => (int) $g->sum('quantity'));
|
||||||
|
$dispensedOk = DispenseRecord::where('order_id', $order->id)
|
||||||
|
->where('dispense_status', 1)
|
||||||
|
->get()->groupBy('product_id')->map(fn ($g) => (int) $g->sum('amount'));
|
||||||
|
|
||||||
|
$created = [];
|
||||||
|
foreach ($ordered as $productId => $orderedQty) {
|
||||||
|
if (!$productId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$shortfall = $orderedQty - (int) ($dispensedOk[$productId] ?? 0);
|
||||||
|
if ($shortfall <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = PickupCode::create([
|
||||||
|
'company_id' => $order->company_id ?? $machine->company_id,
|
||||||
|
'machine_id' => $order->machine_id,
|
||||||
|
'product_id' => $productId,
|
||||||
|
'code' => PickupCode::generateUniqueCode($order->machine_id),
|
||||||
|
'slug' => Str::random(16),
|
||||||
|
'batch_no' => $batchNo,
|
||||||
|
'status' => 'active',
|
||||||
|
'usage_limit' => $shortfall,
|
||||||
|
'usage_count' => 0,
|
||||||
|
'expires_at' => now()->addDays(7),
|
||||||
|
'created_by' => Auth::id(),
|
||||||
|
]);
|
||||||
|
$created[] = $code->code;
|
||||||
|
|
||||||
|
SystemOperationLog::create([
|
||||||
|
'company_id' => $machine->company_id,
|
||||||
|
'user_id' => Auth::id(),
|
||||||
|
'module' => 'pickup_code',
|
||||||
|
'action' => 'create',
|
||||||
|
'target_id' => $code->id,
|
||||||
|
'target_type' => PickupCode::class,
|
||||||
|
'new_values' => $code->toArray() + ['source' => 'failed_delivery_compensation', 'source_order_id' => $order->id],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($created)) {
|
||||||
|
return response()->json(['success' => false, 'message' => __('No undelivered items to compensate.')], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(['success' => true, 'message' => __('Pickup code generated.'), 'codes' => $created]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取得單筆交易詳情 (用於 Slide-over)
|
* 取得單筆交易詳情 (用於 Slide-over)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -722,10 +722,204 @@ class WarehouseController extends Controller
|
|||||||
? \App\Models\System\Company::active()->orderBy('name')->get()
|
? \App\Models\System\Company::active()->orderBy('name')->get()
|
||||||
: collect();
|
: collect();
|
||||||
|
|
||||||
return view('admin.warehouses.machine-inventory', compact('machines', 'companies'));
|
// 供批次匯出 modal 勾選用的完整機台清單(全域 scope 已限可存取機台)
|
||||||
|
$exportMachines = Machine::select('id', 'name', 'serial_no')->orderBy('name')->get();
|
||||||
|
|
||||||
|
return view('admin.warehouses.machine-inventory', compact('machines', 'companies', 'exportMachines'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 匯出單一機台的「每個貨道」明細(CSV / Excel)——供機台詳細視圖(矩陣/表格)旁的匯出鈕使用。
|
||||||
|
* 參考 ProductController::export / SalesController::handleExport 的串流下載模式。
|
||||||
|
*/
|
||||||
|
public function machineInventoryExport(Request $request, Machine $machine)
|
||||||
|
{
|
||||||
|
$type = $request->input('export', 'csv');
|
||||||
|
$isExcel = ($type === 'excel');
|
||||||
|
$ext = $isExcel ? 'xls' : 'csv';
|
||||||
|
$contentType = $isExcel ? 'application/vnd.ms-excel; charset=utf-8' : 'text/csv; charset=utf-8';
|
||||||
|
|
||||||
|
$slots = $machine->slots()
|
||||||
|
->with('product:id,name,barcode')
|
||||||
|
->orderByRaw('CAST(slot_no AS UNSIGNED) ASC')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$headers = ['貨道號', '商品名稱', '商品條碼', '類型', '目前庫存', '滿庫容量', '庫存率(%)', '效期', '批號', '啟用', '鎖定'];
|
||||||
|
|
||||||
|
$safeSerial = preg_replace('/[^A-Za-z0-9_-]/', '', (string) $machine->serial_no);
|
||||||
|
$filename = '貨道明細_' . ($safeSerial ?: $machine->id) . '_' . now()->format('YmdHis') . '.' . $ext;
|
||||||
|
|
||||||
|
$callback = function () use ($slots, $headers, $isExcel) {
|
||||||
|
$file = fopen('php://output', 'w');
|
||||||
|
if ($isExcel) {
|
||||||
|
fwrite($file, '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40">');
|
||||||
|
fwrite($file, '<head><meta charset="utf-8"/><style>table { border-collapse: collapse; } th, td { border: 1px solid #cbd5e1; padding: 8px 12px; text-align: left; } th { background-color: #f1f5f9; font-weight: bold; }</style></head><body>');
|
||||||
|
fwrite($file, '<table><thead><tr>');
|
||||||
|
foreach ($headers as $header) {
|
||||||
|
fwrite($file, '<th>' . htmlspecialchars($header) . '</th>');
|
||||||
|
}
|
||||||
|
fwrite($file, '</tr></thead><tbody>');
|
||||||
|
} else {
|
||||||
|
fwrite($file, "\xEF\xBB\xBF"); // UTF-8 BOM
|
||||||
|
fputcsv($file, $headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($slots as $slot) {
|
||||||
|
$stock = (int) $slot->stock;
|
||||||
|
$capacity = (int) $slot->max_stock;
|
||||||
|
$rate = $capacity > 0 ? round($stock / $capacity * 100) : 0;
|
||||||
|
|
||||||
|
$row = [
|
||||||
|
$slot->slot_no,
|
||||||
|
$slot->product->name ?? '',
|
||||||
|
$slot->product->barcode ?? '',
|
||||||
|
$slot->type,
|
||||||
|
$stock,
|
||||||
|
$capacity,
|
||||||
|
$rate,
|
||||||
|
$slot->expiry_date ? $slot->expiry_date->format('Y-m-d') : '',
|
||||||
|
$slot->batch_no,
|
||||||
|
$slot->is_active ? '是' : '否',
|
||||||
|
$slot->is_locked ? '是' : '否',
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($isExcel) {
|
||||||
|
fwrite($file, '<tr>');
|
||||||
|
foreach ($row as $cell) {
|
||||||
|
fwrite($file, '<td>' . htmlspecialchars((string) $cell) . '</td>');
|
||||||
|
}
|
||||||
|
fwrite($file, '</tr>');
|
||||||
|
} else {
|
||||||
|
fputcsv($file, $row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isExcel) {
|
||||||
|
fwrite($file, '</tbody></table></body></html>');
|
||||||
|
}
|
||||||
|
fclose($file);
|
||||||
|
};
|
||||||
|
|
||||||
|
return response()->streamDownload($callback, $filename, ['Content-Type' => $contentType]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批次匯出「機台庫存概覽」——每台機台一個分頁(Excel sheet)/一個檔(CSV),內容為該機台每貨道明細。
|
||||||
|
* Excel = 多分頁 xlsx(OpenSpout);CSV 無分頁概念,故每台一個 .csv 打包成 ZIP。
|
||||||
|
* ids:逗號字串或陣列;空則匯出全部可存取機台。全域 scope 確保只含可存取機台。
|
||||||
|
*/
|
||||||
|
public function machineInventoryExportBatch(Request $request)
|
||||||
|
{
|
||||||
|
$isExcel = ($request->input('export', 'csv') === 'excel');
|
||||||
|
|
||||||
|
$ids = $request->input('ids');
|
||||||
|
if (is_string($ids)) {
|
||||||
|
$ids = array_filter(array_map('intval', explode(',', $ids)));
|
||||||
|
} elseif (is_array($ids)) {
|
||||||
|
$ids = array_filter(array_map('intval', $ids));
|
||||||
|
} else {
|
||||||
|
$ids = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = Machine::with(['slots' => fn ($q) => $q->with('product:id,name,barcode')->orderByRaw('CAST(slot_no AS UNSIGNED) ASC')])
|
||||||
|
->orderBy('name');
|
||||||
|
if (!empty($ids)) {
|
||||||
|
$query->whereIn('id', $ids);
|
||||||
|
}
|
||||||
|
$machines = $query->get();
|
||||||
|
|
||||||
|
$headers = ['貨道號', '商品名稱', '商品條碼', '類型', '目前庫存', '滿庫容量', '庫存率(%)', '效期', '批號', '啟用', '鎖定'];
|
||||||
|
$ts = now()->format('YmdHis');
|
||||||
|
|
||||||
|
// 單台的資料列
|
||||||
|
$rowsOf = function ($machine) {
|
||||||
|
$rows = [];
|
||||||
|
foreach ($machine->slots as $slot) {
|
||||||
|
$stock = (int) $slot->stock;
|
||||||
|
$capacity = (int) $slot->max_stock;
|
||||||
|
$rate = $capacity > 0 ? round($stock / $capacity * 100) : 0;
|
||||||
|
$rows[] = [
|
||||||
|
(string) $slot->slot_no,
|
||||||
|
$slot->product->name ?? '',
|
||||||
|
$slot->product->barcode ?? '',
|
||||||
|
(string) $slot->type,
|
||||||
|
$stock,
|
||||||
|
$capacity,
|
||||||
|
$rate,
|
||||||
|
$slot->expiry_date ? $slot->expiry_date->format('Y-m-d') : '',
|
||||||
|
(string) $slot->batch_no,
|
||||||
|
$slot->is_active ? '是' : '否',
|
||||||
|
$slot->is_locked ? '是' : '否',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $rows;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 分頁/檔名(機台名稱_序號,去非法字元、限長、去重;Excel sheet 名上限 31 字)
|
||||||
|
$labelOf = function ($machine, array &$used) {
|
||||||
|
$b = trim(($machine->name ?? '機台') . '_' . ($machine->serial_no ?? $machine->id));
|
||||||
|
$b = preg_replace('/[\\\\\\/\\?\\*\\[\\]:]/u', '_', $b);
|
||||||
|
$b = mb_substr($b, 0, 28);
|
||||||
|
$name = $b;
|
||||||
|
$i = 1;
|
||||||
|
while (in_array($name, $used, true)) {
|
||||||
|
$name = mb_substr($b, 0, 24) . '_' . (++$i);
|
||||||
|
}
|
||||||
|
$used[] = $name;
|
||||||
|
return $name;
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($isExcel) {
|
||||||
|
$tmp = tempnam(sys_get_temp_dir(), 'invxlsx');
|
||||||
|
$writer = new \OpenSpout\Writer\XLSX\Writer();
|
||||||
|
$writer->openToFile($tmp);
|
||||||
|
$used = [];
|
||||||
|
$first = true;
|
||||||
|
foreach ($machines as $machine) {
|
||||||
|
$sheet = $first ? $writer->getCurrentSheet() : $writer->addNewSheetAndMakeItCurrent();
|
||||||
|
$first = false;
|
||||||
|
$sheet->setName($labelOf($machine, $used));
|
||||||
|
$writer->addRow(\OpenSpout\Common\Entity\Row::fromValues($headers));
|
||||||
|
foreach ($rowsOf($machine) as $row) {
|
||||||
|
$writer->addRow(\OpenSpout\Common\Entity\Row::fromValues($row));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($first) {
|
||||||
|
$writer->getCurrentSheet()->setName('機台庫存');
|
||||||
|
$writer->addRow(\OpenSpout\Common\Entity\Row::fromValues($headers));
|
||||||
|
}
|
||||||
|
$writer->close();
|
||||||
|
|
||||||
|
return response()->download($tmp, "機台庫存_{$ts}.xlsx", [
|
||||||
|
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
])->deleteFileAfterSend(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSV:每台一個 .csv 打包成 ZIP
|
||||||
|
$tmp = tempnam(sys_get_temp_dir(), 'invzip');
|
||||||
|
$zip = new \ZipArchive();
|
||||||
|
$zip->open($tmp, \ZipArchive::OVERWRITE);
|
||||||
|
$used = [];
|
||||||
|
foreach ($machines as $machine) {
|
||||||
|
$fh = fopen('php://temp', 'r+');
|
||||||
|
fputcsv($fh, $headers);
|
||||||
|
foreach ($rowsOf($machine) as $row) {
|
||||||
|
fputcsv($fh, $row);
|
||||||
|
}
|
||||||
|
rewind($fh);
|
||||||
|
$content = "\xEF\xBB\xBF" . stream_get_contents($fh);
|
||||||
|
fclose($fh);
|
||||||
|
$zip->addFromString($labelOf($machine, $used) . '.csv', $content);
|
||||||
|
}
|
||||||
|
if ($machines->isEmpty()) {
|
||||||
|
$zip->addFromString('機台庫存.csv', "\xEF\xBB\xBF" . implode(',', $headers) . "\n");
|
||||||
|
}
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
return response()->download($tmp, "機台庫存_{$ts}.zip")->deleteFileAfterSend(true);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AJAX:取得單台機台貨道詳情
|
* AJAX:取得單台機台貨道詳情
|
||||||
*/
|
*/
|
||||||
|
|||||||
10
lang/en.json
10
lang/en.json
@ -779,6 +779,9 @@
|
|||||||
"Export Report": "Export Report",
|
"Export Report": "Export Report",
|
||||||
"Export": "Export",
|
"Export": "Export",
|
||||||
"Export to CSV": "Export to CSV",
|
"Export to CSV": "Export to CSV",
|
||||||
|
"Export Machine Inventory": "Export Machine Inventory",
|
||||||
|
"Please select at least one machine": "Please select at least one machine",
|
||||||
|
"No machines": "No machines",
|
||||||
"Export to Excel": "Export to Excel",
|
"Export to Excel": "Export to Excel",
|
||||||
"External URL": "External URL",
|
"External URL": "External URL",
|
||||||
"Failed": "Failed",
|
"Failed": "Failed",
|
||||||
@ -1620,6 +1623,13 @@
|
|||||||
"Picked up": "Picked up",
|
"Picked up": "Picked up",
|
||||||
"Picked up Time": "Picked up Time",
|
"Picked up Time": "Picked up Time",
|
||||||
"Pickup Code": "Pickup Code",
|
"Pickup Code": "Pickup Code",
|
||||||
|
"Pending Use": "Pending Use",
|
||||||
|
"Generate pickup code? (Valid for pickup within 7 days)": "Generate pickup code? (Valid for pickup within 7 days)",
|
||||||
|
"Pickup code generated.": "Pickup code generated.",
|
||||||
|
"Pickup code already generated.": "Pickup code already generated.",
|
||||||
|
"This order is not eligible for a pickup code.": "This order is not eligible for a pickup code.",
|
||||||
|
"No undelivered items to compensate.": "No undelivered items to compensate.",
|
||||||
|
"Machine not found.": "Machine not found.",
|
||||||
"Pickup Code (8 Digits)": "Pickup Code (8 Digits)",
|
"Pickup Code (8 Digits)": "Pickup Code (8 Digits)",
|
||||||
"Pickup Codes": "Pickup Codes",
|
"Pickup Codes": "Pickup Codes",
|
||||||
"Pickup Module": "Pickup Module",
|
"Pickup Module": "Pickup Module",
|
||||||
|
|||||||
@ -780,6 +780,9 @@
|
|||||||
"Export Report": "匯出報表",
|
"Export Report": "匯出報表",
|
||||||
"Export": "匯出",
|
"Export": "匯出",
|
||||||
"Export to CSV": "匯出成 CSV",
|
"Export to CSV": "匯出成 CSV",
|
||||||
|
"Export Machine Inventory": "匯出機台庫存",
|
||||||
|
"Please select at least one machine": "請至少選擇一台機台",
|
||||||
|
"No machines": "沒有機台",
|
||||||
"Export to Excel": "匯出成 Excel",
|
"Export to Excel": "匯出成 Excel",
|
||||||
"External URL": "外連 URL",
|
"External URL": "外連 URL",
|
||||||
"Failed": "失敗",
|
"Failed": "失敗",
|
||||||
@ -1621,6 +1624,13 @@
|
|||||||
"Picked up": "領取",
|
"Picked up": "領取",
|
||||||
"Picked up Time": "領取時間",
|
"Picked up Time": "領取時間",
|
||||||
"Pickup Code": "取貨碼",
|
"Pickup Code": "取貨碼",
|
||||||
|
"Pending Use": "待使用",
|
||||||
|
"Generate pickup code? (Valid for pickup within 7 days)": "是否產生取貨碼?(取貨 7 天內有效)",
|
||||||
|
"Pickup code generated.": "取貨碼已產生。",
|
||||||
|
"Pickup code already generated.": "此訂單已產生過取貨碼。",
|
||||||
|
"This order is not eligible for a pickup code.": "此訂單不符合產生取貨碼的條件。",
|
||||||
|
"No undelivered items to compensate.": "沒有需要補償的未出貨商品。",
|
||||||
|
"Machine not found.": "找不到機台。",
|
||||||
"Pickup Code (8 Digits)": "取貨碼 (8 位數)",
|
"Pickup Code (8 Digits)": "取貨碼 (8 位數)",
|
||||||
"Pickup Codes": "取貨碼",
|
"Pickup Codes": "取貨碼",
|
||||||
"Pickup Module": "取貨模組",
|
"Pickup Module": "取貨模組",
|
||||||
|
|||||||
@ -233,6 +233,24 @@ function salesCenter() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async generatePickupCode(orderId) {
|
||||||
|
if (!confirm('{{ __('Generate pickup code? (Valid for pickup within 7 days)') }}')) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/admin/sales/orders/${orderId}/pickup-code`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
window.dispatchEvent(new CustomEvent('toast', { detail: { message: data.message || '', type: data.success ? 'success' : 'error' } }));
|
||||||
|
if (data.success) this.fetchTabData('orders');
|
||||||
|
} catch (e) {
|
||||||
|
window.dispatchEvent(new CustomEvent('toast', { detail: { message: '{{ __('Operation failed') }}', type: 'error' } }));
|
||||||
|
}
|
||||||
|
},
|
||||||
openDetail(orderId) {
|
openDetail(orderId) {
|
||||||
this.showPanel = true;
|
this.showPanel = true;
|
||||||
document.getElementById('order-detail-content').innerHTML = `<div class="flex items-center justify-center h-full py-20"><x-luxury-spinner show="true" /></div>`;
|
document.getElementById('order-detail-content').innerHTML = `<div class="flex items-center justify-center h-full py-20"><x-luxury-spinner show="true" /></div>`;
|
||||||
|
|||||||
@ -212,6 +212,10 @@
|
|||||||
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">
|
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">
|
||||||
{{ __('Dispense Status') }}
|
{{ __('Dispense Status') }}
|
||||||
</th>
|
</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">
|
||||||
|
{{ __('Pickup Code') }}
|
||||||
|
</th>
|
||||||
<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">
|
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">
|
||||||
{{ __('Invoice Number') }}
|
{{ __('Invoice Number') }}
|
||||||
@ -346,6 +350,36 @@
|
|||||||
<x-status-badge :color="$ds['color']" :label="$ds['label']" size="xs" />
|
<x-status-badge :color="$ds['color']" :label="$ds['label']" size="xs" />
|
||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-6 py-6 whitespace-nowrap">
|
||||||
|
@php
|
||||||
|
$qualifiesForPickup = in_array($order->status, ['completed','paid'], true) && in_array((int) $order->delivery_status, [0, 2], true);
|
||||||
|
$orderPickupCodes = $order->relationLoaded('compensationCodes') ? $order->compensationCodes : collect();
|
||||||
|
@endphp
|
||||||
|
@if($orderPickupCodes->count())
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
@foreach($orderPickupCodes as $cc)
|
||||||
|
<span class="inline-flex items-center gap-1.5 text-[11px] font-black">
|
||||||
|
<span class="font-mono tracking-tighter px-1.5 py-0.5 rounded bg-emerald-500/10 border border-emerald-500/20 text-slate-700 dark:text-slate-200">{{ $cc->code }}</span>
|
||||||
|
@if($cc->status === 'active' && $cc->usage_count < $cc->usage_limit)
|
||||||
|
<span class="px-1.5 py-0.5 rounded bg-amber-500/10 border border-amber-500/20 text-[9px] text-amber-600 dark:text-amber-400 uppercase tracking-widest">{{ __('Pending Use') }}</span>
|
||||||
|
@else
|
||||||
|
<span class="px-1.5 py-0.5 rounded bg-slate-500/10 border border-slate-500/20 text-[9px] text-slate-500 uppercase tracking-widest">{{ __('Used') }}</span>
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@elseif($qualifiesForPickup)
|
||||||
|
<button type="button" @click="generatePickupCode({{ $order->id }})"
|
||||||
|
title="{{ __('Generate Pickup Code') }}"
|
||||||
|
class="w-8 h-8 rounded-xl bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 hover:bg-emerald-500/20 flex items-center justify-center transition-all active:scale-95 border border-emerald-500/20">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
@else
|
||||||
|
<span class="text-xs font-bold text-slate-300 dark:text-slate-700 tracking-widest">--</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
<td class="px-6 py-6 whitespace-nowrap">
|
<td class="px-6 py-6 whitespace-nowrap">
|
||||||
@php
|
@php
|
||||||
$invoiceSearchValue = $order->invoice ? $order->invoice->invoice_no : $order->order_no;
|
$invoiceSearchValue = $order->invoice ? $order->invoice->invoice_no : $order->order_no;
|
||||||
@ -390,8 +424,8 @@
|
|||||||
</tr>
|
</tr>
|
||||||
@empty
|
@empty
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="9" class="py-20 text-center">
|
<td colspan="10" class="py-20 text-center">
|
||||||
<x-empty-state mode="table" colspan="9" :message="__('No transaction orders found')" />
|
<x-empty-state mode="table" colspan="10" :message="__('No transaction orders found')" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforelse
|
@endforelse
|
||||||
@ -533,6 +567,37 @@
|
|||||||
<x-status-badge :color="$ds['color']" :label="$ds['label']" size="xs" />
|
<x-status-badge :color="$ds['color']" :label="$ds['label']" size="xs" />
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Pickup Code') }}</p>
|
||||||
|
@php
|
||||||
|
$qualifiesForPickup = in_array($order->status, ['completed','paid'], true) && in_array((int) $order->delivery_status, [0, 2], true);
|
||||||
|
$orderPickupCodes = $order->relationLoaded('compensationCodes') ? $order->compensationCodes : collect();
|
||||||
|
@endphp
|
||||||
|
@if($orderPickupCodes->count())
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
@foreach($orderPickupCodes as $cc)
|
||||||
|
<span class="inline-flex items-center gap-1.5 text-[11px] font-black">
|
||||||
|
<span class="font-mono tracking-tighter px-1.5 py-0.5 rounded bg-emerald-500/10 border border-emerald-500/20 text-slate-700 dark:text-slate-200">{{ $cc->code }}</span>
|
||||||
|
@if($cc->status === 'active' && $cc->usage_count < $cc->usage_limit)
|
||||||
|
<span class="px-1.5 py-0.5 rounded bg-amber-500/10 border border-amber-500/20 text-[9px] text-amber-600 dark:text-amber-400 uppercase tracking-widest">{{ __('Pending Use') }}</span>
|
||||||
|
@else
|
||||||
|
<span class="px-1.5 py-0.5 rounded bg-slate-500/10 border border-slate-500/20 text-[9px] text-slate-500 uppercase tracking-widest">{{ __('Used') }}</span>
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@elseif($qualifiesForPickup)
|
||||||
|
<button type="button" @click="generatePickupCode({{ $order->id }})"
|
||||||
|
class="inline-flex items-center gap-1 px-3 py-1.5 rounded-xl bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 hover:bg-emerald-500/20 text-xs font-black uppercase tracking-widest transition-all active:scale-95 border border-emerald-500/20">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||||
|
</svg>
|
||||||
|
{{ __('Generate Pickup Code') }}
|
||||||
|
</button>
|
||||||
|
@else
|
||||||
|
<span class="text-xs font-bold text-slate-300 dark:text-slate-700 tracking-widest">--</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Remark --}}
|
{{-- Remark --}}
|
||||||
|
|||||||
@ -85,6 +85,13 @@
|
|||||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" @click="openExportModal()"
|
||||||
|
class="p-2.5 rounded-xl bg-emerald-500 text-white hover:bg-emerald-600 shadow-lg shadow-emerald-500/25 group transition-all active:scale-95"
|
||||||
|
title="{{ __('Export Machine Inventory') }}">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -96,6 +103,60 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Detail Mode --}}
|
{{-- Detail Mode --}}
|
||||||
|
{{-- 機台庫存批次匯出 Modal --}}
|
||||||
|
<div x-show="exportModalOpen" class="fixed inset-0 z-[100] overflow-y-auto" x-cloak
|
||||||
|
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">
|
||||||
|
<div class="flex items-center justify-center min-h-screen px-4 py-8">
|
||||||
|
<div class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm" @click="exportModalOpen = false"></div>
|
||||||
|
<div class="relative bg-white dark:bg-slate-900 rounded-3xl shadow-2xl w-full max-w-lg border border-slate-100 dark:border-slate-800 max-h-[85vh] flex flex-col">
|
||||||
|
<div class="px-8 pt-8 pb-4 border-b border-slate-50 dark:border-slate-800/50 flex justify-between items-center">
|
||||||
|
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight">{{ __('Export Machine Inventory') }}</h3>
|
||||||
|
<button @click="exportModalOpen = 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" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="px-8 py-4 border-b border-slate-50 dark:border-slate-800/50">
|
||||||
|
<label class="flex items-center gap-3 cursor-pointer">
|
||||||
|
<input type="checkbox" :checked="exportAllChecked" @change="toggleExportAll($event)"
|
||||||
|
class="w-4 h-4 text-emerald-500 rounded border-slate-300 focus:ring-emerald-500 dark:bg-slate-800 dark:border-slate-700">
|
||||||
|
<span class="text-sm font-black text-slate-700 dark:text-slate-200">{{ __('Select All') }}
|
||||||
|
<span class="text-slate-400 font-bold">(<span x-text="exportSelectedIds.length"></span>/{{ $exportMachines->count() }})</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="px-6 py-3 overflow-y-auto flex-1 space-y-1">
|
||||||
|
@forelse($exportMachines as $m)
|
||||||
|
<label class="flex items-center gap-3 py-2 px-3 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-800 cursor-pointer transition-colors">
|
||||||
|
<input type="checkbox" value="{{ $m->id }}" x-model.number="exportSelectedIds"
|
||||||
|
class="w-4 h-4 text-emerald-500 rounded border-slate-300 focus:ring-emerald-500 dark:bg-slate-800 dark:border-slate-700">
|
||||||
|
<span class="flex flex-col">
|
||||||
|
<span class="text-sm font-bold text-slate-700 dark:text-slate-200">{{ $m->name }}</span>
|
||||||
|
<span class="text-[10px] font-mono font-bold text-slate-400 uppercase tracking-widest">{{ $m->serial_no }}</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
@empty
|
||||||
|
<p class="text-sm text-slate-400 text-center py-8">{{ __('No machines') }}</p>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
<div class="px-8 py-6 bg-slate-50 dark:bg-slate-900/50 flex flex-wrap justify-end gap-3 rounded-b-3xl border-t border-slate-100 dark:border-slate-800">
|
||||||
|
<button @click="exportModalOpen = false" class="btn-luxury-ghost">{{ __('Cancel') }}</button>
|
||||||
|
{{-- 暫時隱藏 CSV 匯出(保留備用;後端仍支援 csv/zip) --}}
|
||||||
|
{{--
|
||||||
|
<button @click="doExport('csv')"
|
||||||
|
class="px-5 py-2.5 rounded-xl bg-emerald-500 text-white font-black text-sm uppercase tracking-widest hover:bg-emerald-600 transition-all active:scale-95 flex items-center gap-2">
|
||||||
|
<span>📄</span> {{ __('Export to CSV') }}
|
||||||
|
</button>
|
||||||
|
--}}
|
||||||
|
<button @click="doExport('excel')"
|
||||||
|
class="px-5 py-2.5 rounded-xl bg-emerald-600 text-white font-black text-sm uppercase tracking-widest hover:bg-emerald-700 transition-all active:scale-95 flex items-center gap-2">
|
||||||
|
<span>📊</span> {{ __('Export to Excel') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div x-show="viewMode === 'detail'" class="space-y-8 animate-luxury-in" x-cloak>
|
<div x-show="viewMode === 'detail'" class="space-y-8 animate-luxury-in" x-cloak>
|
||||||
<!-- Statistics Dashboard -->
|
<!-- Statistics Dashboard -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-stretch">
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-stretch">
|
||||||
@ -260,6 +321,32 @@
|
|||||||
</svg>
|
</svg>
|
||||||
{{ __('Grid View') }}
|
{{ __('Grid View') }}
|
||||||
</button>
|
</button>
|
||||||
|
<div class="relative" x-data="{ exportOpen: false }" x-show="slots.length > 0" x-cloak>
|
||||||
|
<button type="button" @click="exportOpen = !exportOpen" @click.away="exportOpen = false"
|
||||||
|
class="flex items-center gap-2 px-4 py-2 rounded-xl transition-all font-black text-sm uppercase tracking-widest text-emerald-500 hover:bg-emerald-500/10">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||||
|
</svg>
|
||||||
|
{{ __('Export') }}
|
||||||
|
<svg class="w-3 h-3 transition-transform duration-200" :class="exportOpen ? 'rotate-180' : ''" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div x-show="exportOpen" x-transition
|
||||||
|
class="absolute left-0 top-full mt-2 bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 shadow-xl rounded-2xl p-2 z-30 min-w-[160px]"
|
||||||
|
x-cloak>
|
||||||
|
<a :href="`/admin/warehouses/machine-inventory/${selectedMachine.id}/export?export=csv`"
|
||||||
|
download @click="exportOpen = false"
|
||||||
|
class="w-full text-left py-2 px-3 hover:bg-slate-50 dark:hover:bg-slate-800 text-xs font-bold text-slate-700 dark:text-slate-300 rounded-xl transition-all flex items-center gap-2">
|
||||||
|
📄 {{ __('Export to CSV') }}
|
||||||
|
</a>
|
||||||
|
<a :href="`/admin/warehouses/machine-inventory/${selectedMachine.id}/export?export=excel`"
|
||||||
|
download @click="exportOpen = false"
|
||||||
|
class="w-full text-left py-2 px-3 hover:bg-slate-50 dark:hover:bg-slate-800 text-xs font-bold text-slate-700 dark:text-slate-300 rounded-xl transition-all flex items-center gap-2">
|
||||||
|
📊 {{ __('Export to Excel') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
@ -818,6 +905,35 @@ document.addEventListener('alpine:init', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
hasReplenishPermission: {{ auth()->user()->can('menu.warehouses.replenishments') ? 'true' : 'false' }},
|
hasReplenishPermission: {{ auth()->user()->can('menu.warehouses.replenishments') ? 'true' : 'false' }},
|
||||||
|
|
||||||
|
// 機台庫存批次匯出
|
||||||
|
exportModalOpen: false,
|
||||||
|
exportSelectedIds: [],
|
||||||
|
get exportAllChecked() {
|
||||||
|
const total = {{ $exportMachines->count() }};
|
||||||
|
return total > 0 && this.exportSelectedIds.length === total;
|
||||||
|
},
|
||||||
|
openExportModal() {
|
||||||
|
this.exportSelectedIds = @js($exportMachines->pluck('id'));
|
||||||
|
this.exportModalOpen = true;
|
||||||
|
},
|
||||||
|
toggleExportAll(e) {
|
||||||
|
this.exportSelectedIds = e.target.checked ? @js($exportMachines->pluck('id')) : [];
|
||||||
|
},
|
||||||
|
doExport(format) {
|
||||||
|
if (this.exportSelectedIds.length === 0) {
|
||||||
|
window.dispatchEvent(new CustomEvent('toast', { detail: { message: '{{ __('Please select at least one machine') }}', type: 'error' } }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = `/admin/warehouses/machine-inventory/export?export=${format}&ids=${this.exportSelectedIds.join(',')}`;
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.setAttribute('download', '');
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
this.exportModalOpen = false;
|
||||||
|
},
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const machineId = urlParams.get('machine_id');
|
const machineId = urlParams.get('machine_id');
|
||||||
|
|||||||
@ -123,7 +123,9 @@ Route::middleware(['auth', 'auth.session', 'verified', 'tenant.access'])->prefix
|
|||||||
|
|
||||||
// 模組 4:機台庫存總覽 (Force Route Refresh)
|
// 模組 4:機台庫存總覽 (Force Route Refresh)
|
||||||
Route::get('/machine-inventory', [App\Http\Controllers\Admin\WarehouseController::class, 'machineInventory'])->name('machine-inventory');
|
Route::get('/machine-inventory', [App\Http\Controllers\Admin\WarehouseController::class, 'machineInventory'])->name('machine-inventory');
|
||||||
|
Route::get('/machine-inventory/export', [App\Http\Controllers\Admin\WarehouseController::class, 'machineInventoryExportBatch'])->name('machine-inventory.export-batch');
|
||||||
Route::get('/machine-inventory/{machine}/slots', [App\Http\Controllers\Admin\WarehouseController::class, 'machineSlots'])->name('machine-inventory.slots');
|
Route::get('/machine-inventory/{machine}/slots', [App\Http\Controllers\Admin\WarehouseController::class, 'machineSlots'])->name('machine-inventory.slots');
|
||||||
|
Route::get('/machine-inventory/{machine}/export', [App\Http\Controllers\Admin\WarehouseController::class, 'machineInventoryExport'])->name('machine-inventory.export');
|
||||||
Route::get('/machine-inventory/{machine}/movements', [App\Http\Controllers\Admin\WarehouseController::class, 'machineStockMovements'])->name('machine-inventory.movements');
|
Route::get('/machine-inventory/{machine}/movements', [App\Http\Controllers\Admin\WarehouseController::class, 'machineStockMovements'])->name('machine-inventory.movements');
|
||||||
|
|
||||||
// 模組 5:機台補貨
|
// 模組 5:機台補貨
|
||||||
@ -158,6 +160,7 @@ Route::middleware(['auth', 'auth.session', 'verified', 'tenant.access'])->prefix
|
|||||||
|
|
||||||
// 管理者備註(訂單/發票各自獨立)
|
// 管理者備註(訂單/發票各自獨立)
|
||||||
Route::patch('/orders/{order}/remark', [App\Http\Controllers\Admin\SalesController::class, 'updateOrderRemark'])->name('orders.remark');
|
Route::patch('/orders/{order}/remark', [App\Http\Controllers\Admin\SalesController::class, 'updateOrderRemark'])->name('orders.remark');
|
||||||
|
Route::post('/orders/{order}/pickup-code', [App\Http\Controllers\Admin\SalesController::class, 'generateOrderPickupCode'])->name('orders.pickup-code');
|
||||||
Route::patch('/invoices/{invoice}/remark', [App\Http\Controllers\Admin\SalesController::class, 'updateInvoiceRemark'])->name('invoices.remark');
|
Route::patch('/invoices/{invoice}/remark', [App\Http\Controllers\Admin\SalesController::class, 'updateInvoiceRemark'])->name('invoices.remark');
|
||||||
|
|
||||||
// 取貨碼
|
// 取貨碼
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user