[FEAT] 優化一鍵補貨 UI 與 RWD 響應式佈局
1. 實作一鍵補貨智慧重新計算邏輯:切換倉庫或機台時自動更新預覽。 2. 新增多語系支援:補齊繁中、日文、英文語系檔中的庫存/上限、重新計算等關鍵字。 3. 優化 RWD 響應式佈局:在手機版自動切換為卡片式 (Card) 佈局,提升行動裝置操作體驗。 4. 新增刪除功能:預覽清單中每一行貨道皆可獨立移除,並配合唯一辨識碼 (slot_no) 修正 state 殘留 bug。 5. 完善一鍵補貨 Modal 腳底控制項:新增手動「重新計算」按鈕與樣式優化。 6. 完成補貨單管理功能:包含新增、取消、分配人員等後台邏輯與多租戶隔離。 7. 優化庫存轉移 (Transfers) 介面與詳情面板 RWD 表現。
This commit is contained in:
parent
b0e523067f
commit
2012c5a321
@ -29,6 +29,12 @@ class DashboardController extends Controller
|
|||||||
->orWhere('serial_no', 'like', "%{$search}%");
|
->orWhere('serial_no', 'like', "%{$search}%");
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
->withSum('slots', 'stock')
|
||||||
|
->withSum('slots', 'max_stock')
|
||||||
|
->withSum(['orders as today_sales_sum' => function($query) {
|
||||||
|
$query->whereDate('payment_at', now()->toDateString())
|
||||||
|
->where('payment_status', 'paid');
|
||||||
|
}], 'pay_amount')
|
||||||
->orderByDesc('last_heartbeat_at')
|
->orderByDesc('last_heartbeat_at')
|
||||||
->paginate($perPage)
|
->paginate($perPage)
|
||||||
->withQueryString();
|
->withQueryString();
|
||||||
|
|||||||
@ -181,6 +181,7 @@ class WarehouseController extends Controller
|
|||||||
// 2. Stock-In Orders (stock-in)
|
// 2. Stock-In Orders (stock-in)
|
||||||
if (!$isAjax || $tab === 'stock-in') {
|
if (!$isAjax || $tab === 'stock-in') {
|
||||||
$query = StockInOrder::with(['warehouse:id,name', 'creator:id,name'])
|
$query = StockInOrder::with(['warehouse:id,name', 'creator:id,name'])
|
||||||
|
->withCount('items')
|
||||||
->latest();
|
->latest();
|
||||||
|
|
||||||
if ($request->filled('status')) {
|
if ($request->filled('status')) {
|
||||||
@ -193,13 +194,14 @@ class WarehouseController extends Controller
|
|||||||
$query->whereBetween('created_at', [$request->start_date, $request->end_date]);
|
$query->whereBetween('created_at', [$request->start_date, $request->end_date]);
|
||||||
}
|
}
|
||||||
$data['orders'] = $query->paginate($request->input('per_page', 10), ['*'], 'order_page')->withQueryString();
|
$data['orders'] = $query->paginate($request->input('per_page', 10), ['*'], 'order_page')->withQueryString();
|
||||||
$data['stock_in_products'] = Product::orderBy('name')->get(['id', 'name', 'image_url']);
|
$data['stock_in_products'] = Product::with('translations')->orderBy('name')->get(['id', 'name', 'image_url', 'name_dictionary_key']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Movement History (movements)
|
// 3. Movement History (movements)
|
||||||
if (!$isAjax || $tab === 'movements') {
|
if (!$isAjax || $tab === 'movements') {
|
||||||
$query = StockMovement::with(['warehouse:id,name', 'product:id,name', 'creator:id,name'])
|
$query = StockMovement::with(['warehouse:id,name', 'product.translations', 'creator:id,name'])
|
||||||
->orderBy('created_at', 'desc');
|
->orderBy('created_at', 'desc')
|
||||||
|
->orderBy('id', 'desc');
|
||||||
|
|
||||||
if ($request->filled('warehouse_id')) {
|
if ($request->filled('warehouse_id')) {
|
||||||
$query->where('warehouse_id', $request->input('warehouse_id'));
|
$query->where('warehouse_id', $request->input('warehouse_id'));
|
||||||
@ -235,13 +237,28 @@ class WarehouseController extends Controller
|
|||||||
'items' => 'required|array|min:1',
|
'items' => 'required|array|min:1',
|
||||||
'items.*.product_id' => 'required|exists:products,id',
|
'items.*.product_id' => 'required|exists:products,id',
|
||||||
'items.*.quantity' => 'required|integer|min:1',
|
'items.*.quantity' => 'required|integer|min:1',
|
||||||
|
], [
|
||||||
|
'warehouse_id.required' => __('Please select target warehouse'),
|
||||||
|
'items.required' => __('Please add at least one item'),
|
||||||
|
'items.*.product_id.required' => __('Please select product for item :num', ['num' => ':position']),
|
||||||
|
'items.*.quantity.min' => __('Please enter valid quantity for item :num', ['num' => ':position']),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
DB::transaction(function () use ($validated) {
|
DB::transaction(function () use ($validated) {
|
||||||
|
$prefix = 'SI-' . now()->format('Ymd') . '-';
|
||||||
|
$lastOrder = StockInOrder::withoutGlobalScopes()
|
||||||
|
->withTrashed()
|
||||||
|
->where('order_no', 'like', $prefix . '%')
|
||||||
|
->orderBy('order_no', 'desc')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$nextNumber = $lastOrder ? (int)substr($lastOrder->order_no, -4) + 1 : 1;
|
||||||
|
$order_no = $prefix . str_pad($nextNumber, 4, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
$order = StockInOrder::create([
|
$order = StockInOrder::create([
|
||||||
'company_id' => Auth::user()->company_id,
|
'company_id' => Auth::user()->company_id,
|
||||||
'warehouse_id' => $validated['warehouse_id'],
|
'warehouse_id' => $validated['warehouse_id'],
|
||||||
'order_no' => 'SI-' . now()->format('Ymd') . '-' . str_pad(StockInOrder::whereDate('created_at', today())->count() + 1, 4, '0', STR_PAD_LEFT),
|
'order_no' => $order_no,
|
||||||
'status' => 'draft',
|
'status' => 'draft',
|
||||||
'note' => $validated['note'] ?? null,
|
'note' => $validated['note'] ?? null,
|
||||||
'created_by' => Auth::id(),
|
'created_by' => Auth::id(),
|
||||||
@ -253,14 +270,15 @@ class WarehouseController extends Controller
|
|||||||
});
|
});
|
||||||
|
|
||||||
if ($request->ajax()) {
|
if ($request->ajax()) {
|
||||||
|
session()->flash('success', __('Stock-in order created successfully'));
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => __('Stock-in order created')
|
'message' => __('Stock-in order created successfully')
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->route('admin.warehouses.inventory', ['tab' => 'stock-in'])
|
return redirect()->route('admin.warehouses.inventory', ['tab' => 'stock-in'])
|
||||||
->with('success', __('Stock-in order created'));
|
->with('success', __('Stock-in order created successfully'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -268,13 +286,16 @@ class WarehouseController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function confirmStockIn(StockInOrder $stockInOrder)
|
public function confirmStockIn(StockInOrder $stockInOrder)
|
||||||
{
|
{
|
||||||
if ($stockInOrder->status !== 'draft') {
|
if ($stockInOrder->status !== StockInOrder::STATUS_DRAFT) {
|
||||||
|
if (request()->ajax()) {
|
||||||
|
return response()->json(['success' => false, 'message' => __('Order already processed')], 422);
|
||||||
|
}
|
||||||
return back()->with('error', __('Order already processed'));
|
return back()->with('error', __('Order already processed'));
|
||||||
}
|
}
|
||||||
|
|
||||||
DB::transaction(function () use ($stockInOrder) {
|
DB::transaction(function () use ($stockInOrder) {
|
||||||
$stockInOrder->update([
|
$stockInOrder->update([
|
||||||
'status' => 'completed',
|
'status' => StockInOrder::STATUS_COMPLETED,
|
||||||
'completed_at' => now(),
|
'completed_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -301,19 +322,50 @@ class WarehouseController extends Controller
|
|||||||
'reference_id' => $stockInOrder->id,
|
'reference_id' => $stockInOrder->id,
|
||||||
'note' => __('Stock-in order') . ' #' . $stockInOrder->order_no,
|
'note' => __('Stock-in order') . ' #' . $stockInOrder->order_no,
|
||||||
'created_by' => Auth::id(),
|
'created_by' => Auth::id(),
|
||||||
'created_at' => now(),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (request()->ajax()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => __('Stock-in order confirmed and inventory updated')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
return back()->with('success', __('Stock-in order confirmed and inventory updated'));
|
return back()->with('success', __('Stock-in order confirmed and inventory updated'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刪除入庫單(僅限草稿)
|
||||||
|
*/
|
||||||
|
public function destroyStockIn(StockInOrder $stockInOrder)
|
||||||
|
{
|
||||||
|
if ($stockInOrder->status !== StockInOrder::STATUS_DRAFT) {
|
||||||
|
if (request()->ajax()) {
|
||||||
|
return response()->json(['success' => false, 'message' => __('Only draft orders can be deleted')], 422);
|
||||||
|
}
|
||||||
|
return back()->with('error', __('Only draft orders can be deleted'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$stockInOrder->delete(); // Soft delete
|
||||||
|
|
||||||
|
if (request()->ajax()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => __('Stock-in order deleted successfully')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('success', __('Stock-in order deleted successfully'));
|
||||||
|
}
|
||||||
|
|
||||||
// ─── 模組 3:調撥單 ───
|
// ─── 模組 3:調撥單 ───
|
||||||
|
|
||||||
public function transfers(Request $request)
|
public function transfers(Request $request)
|
||||||
{
|
{
|
||||||
$query = TransferOrder::with(['fromWarehouse:id,name', 'toWarehouse:id,name', 'fromMachine:id,name,serial_no', 'creator:id,name', 'items'])
|
$query = TransferOrder::with(['fromWarehouse:id,name', 'toWarehouse:id,name', 'fromMachine:id,name,serial_no', 'creator:id,name'])
|
||||||
|
->withCount('items')
|
||||||
->latest();
|
->latest();
|
||||||
|
|
||||||
if ($request->filled('type')) {
|
if ($request->filled('type')) {
|
||||||
@ -322,11 +374,25 @@ class WarehouseController extends Controller
|
|||||||
if ($request->filled('status')) {
|
if ($request->filled('status')) {
|
||||||
$query->where('status', $request->input('status'));
|
$query->where('status', $request->input('status'));
|
||||||
}
|
}
|
||||||
|
if ($search = $request->input('search_order')) {
|
||||||
|
$query->where('order_no', 'like', "%{$search}%");
|
||||||
|
}
|
||||||
|
if ($request->filled('start_date') && $request->filled('end_date')) {
|
||||||
|
$query->whereBetween('created_at', [$request->start_date, $request->end_date]);
|
||||||
|
}
|
||||||
|
|
||||||
$orders = $query->paginate(10)->withQueryString();
|
$orders = $query->paginate($request->input('per_page', 15))->withQueryString();
|
||||||
$warehouses = Warehouse::active()->orderBy('name')->get(['id', 'name', 'type']);
|
|
||||||
|
if ($request->ajax()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'html' => view('admin.warehouses.partials.table-transfers', compact('orders'))->render()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$warehouses = Warehouse::active()->orderBy('name')->get(['id', 'name']);
|
||||||
$machines = Machine::orderBy('name')->get(['id', 'name', 'serial_no']);
|
$machines = Machine::orderBy('name')->get(['id', 'name', 'serial_no']);
|
||||||
$products = Product::orderBy('name')->get(['id', 'name', 'image_url']);
|
$products = Product::with('translations')->orderBy('name')->get(['id', 'name', 'image_url', 'name_dictionary_key']);
|
||||||
|
|
||||||
return view('admin.warehouses.transfers', compact('orders', 'warehouses', 'machines', 'products'));
|
return view('admin.warehouses.transfers', compact('orders', 'warehouses', 'machines', 'products'));
|
||||||
}
|
}
|
||||||
@ -345,12 +411,29 @@ class WarehouseController extends Controller
|
|||||||
'items' => 'required|array|min:1',
|
'items' => 'required|array|min:1',
|
||||||
'items.*.product_id' => 'required|exists:products,id',
|
'items.*.product_id' => 'required|exists:products,id',
|
||||||
'items.*.quantity' => 'required|integer|min:1',
|
'items.*.quantity' => 'required|integer|min:1',
|
||||||
|
], [
|
||||||
|
'to_warehouse_id.required' => __('Please select target warehouse'),
|
||||||
|
'from_warehouse_id.required_if' => __('Please select source warehouse'),
|
||||||
|
'from_machine_id.required_if' => __('Please select source machine'),
|
||||||
|
'items.required' => __('Please add at least one item'),
|
||||||
|
'items.*.product_id.required' => __('Please select product for item :num', ['num' => ':position']),
|
||||||
|
'items.*.quantity.min' => __('Please enter valid quantity for item :num', ['num' => ':position']),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
DB::transaction(function () use ($validated) {
|
DB::transaction(function () use ($validated) {
|
||||||
|
$prefix = 'TR-' . now()->format('Ymd') . '-';
|
||||||
|
$lastOrder = TransferOrder::withoutGlobalScopes()
|
||||||
|
->withTrashed()
|
||||||
|
->where('order_no', 'like', $prefix . '%')
|
||||||
|
->orderBy('order_no', 'desc')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$nextNumber = $lastOrder ? (int)substr($lastOrder->order_no, -4) + 1 : 1;
|
||||||
|
$order_no = $prefix . str_pad($nextNumber, 4, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
$order = TransferOrder::create([
|
$order = TransferOrder::create([
|
||||||
'company_id' => Auth::user()->company_id,
|
'company_id' => Auth::user()->company_id,
|
||||||
'order_no' => 'TR-' . now()->format('Ymd') . '-' . str_pad(TransferOrder::whereDate('created_at', today())->count() + 1, 4, '0', STR_PAD_LEFT),
|
'order_no' => $order_no,
|
||||||
'type' => $validated['type'],
|
'type' => $validated['type'],
|
||||||
'from_warehouse_id' => $validated['from_warehouse_id'] ?? null,
|
'from_warehouse_id' => $validated['from_warehouse_id'] ?? null,
|
||||||
'from_machine_id' => $validated['from_machine_id'] ?? null,
|
'from_machine_id' => $validated['from_machine_id'] ?? null,
|
||||||
@ -366,14 +449,15 @@ class WarehouseController extends Controller
|
|||||||
});
|
});
|
||||||
|
|
||||||
if ($request->ajax()) {
|
if ($request->ajax()) {
|
||||||
|
session()->flash('success', __('Transfer order created successfully'));
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => __('Transfer order created')
|
'message' => __('Transfer order created successfully')
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->route('admin.warehouses.transfers')
|
return redirect()->route('admin.warehouses.transfers')
|
||||||
->with('success', __('Transfer order created'));
|
->with('success', __('Transfer order created successfully'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -382,6 +466,9 @@ class WarehouseController extends Controller
|
|||||||
public function confirmTransfer(TransferOrder $transferOrder)
|
public function confirmTransfer(TransferOrder $transferOrder)
|
||||||
{
|
{
|
||||||
if ($transferOrder->status !== TransferOrder::STATUS_DRAFT) {
|
if ($transferOrder->status !== TransferOrder::STATUS_DRAFT) {
|
||||||
|
if (request()->ajax()) {
|
||||||
|
return response()->json(['success' => false, 'message' => __('Order already processed')], 422);
|
||||||
|
}
|
||||||
return back()->with('error', __('Order already processed'));
|
return back()->with('error', __('Order already processed'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -413,7 +500,6 @@ class WarehouseController extends Controller
|
|||||||
'reference_id' => $transferOrder->id,
|
'reference_id' => $transferOrder->id,
|
||||||
'note' => __('Transfer out') . ' #' . $transferOrder->order_no,
|
'note' => __('Transfer out') . ' #' . $transferOrder->order_no,
|
||||||
'created_by' => Auth::id(),
|
'created_by' => Auth::id(),
|
||||||
'created_at' => now(),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -437,7 +523,6 @@ class WarehouseController extends Controller
|
|||||||
'reference_id' => $transferOrder->id,
|
'reference_id' => $transferOrder->id,
|
||||||
'note' => __('Transfer in') . ' #' . $transferOrder->order_no,
|
'note' => __('Transfer in') . ' #' . $transferOrder->order_no,
|
||||||
'created_by' => Auth::id(),
|
'created_by' => Auth::id(),
|
||||||
'created_at' => now(),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -447,7 +532,38 @@ class WarehouseController extends Controller
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
return back()->with('success', __('Transfer completed'));
|
if (request()->ajax()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => __('Transfer order confirmed and inventory updated')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('success', __('Transfer order confirmed and inventory updated'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刪除調撥單(僅限草稿)
|
||||||
|
*/
|
||||||
|
public function destroyTransfer(TransferOrder $transferOrder)
|
||||||
|
{
|
||||||
|
if ($transferOrder->status !== TransferOrder::STATUS_DRAFT) {
|
||||||
|
if (request()->ajax()) {
|
||||||
|
return response()->json(['success' => false, 'message' => __('Only draft orders can be deleted')], 422);
|
||||||
|
}
|
||||||
|
return back()->with('error', __('Only draft orders can be deleted'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$transferOrder->delete(); // Soft delete
|
||||||
|
|
||||||
|
if (request()->ajax()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => __('Transfer order deleted successfully')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('success', __('Transfer order deleted successfully'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 模組 4:機台庫存總覽 ───
|
// ─── 模組 4:機台庫存總覽 ───
|
||||||
@ -478,11 +594,15 @@ class WarehouseController extends Controller
|
|||||||
public function machineSlots(Machine $machine)
|
public function machineSlots(Machine $machine)
|
||||||
{
|
{
|
||||||
$slots = $machine->slots()
|
$slots = $machine->slots()
|
||||||
->with('product:id,name,image_url')
|
->with(['product:id,name,image_url,barcode'])
|
||||||
->orderByRaw('CAST(slot_no AS UNSIGNED) ASC')
|
->orderByRaw('CAST(slot_no AS UNSIGNED) ASC')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
return response()->json(['success' => true, 'slots' => $slots]);
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'machine' => $machine,
|
||||||
|
'slots' => $slots
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 模組 5:機台補貨 ───
|
// ─── 模組 5:機台補貨 ───
|
||||||
@ -496,16 +616,34 @@ class WarehouseController extends Controller
|
|||||||
if ($request->filled('status')) {
|
if ($request->filled('status')) {
|
||||||
$query->where('status', $request->input('status'));
|
$query->where('status', $request->input('status'));
|
||||||
}
|
}
|
||||||
|
if ($search = $request->input('search_order')) {
|
||||||
|
$query->where('order_no', 'like', "%{$search}%");
|
||||||
|
}
|
||||||
|
|
||||||
$orders = $query->paginate(10)->withQueryString();
|
$orders = $query->paginate(10)->withQueryString();
|
||||||
|
|
||||||
|
if ($request->ajax()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'html' => view('admin.warehouses.partials.table-replenishments', compact('orders'))->render()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$warehouses = Warehouse::active()->orderBy('name')->get(['id', 'name']);
|
$warehouses = Warehouse::active()->orderBy('name')->get(['id', 'name']);
|
||||||
$machines = Machine::orderBy('name')->get(['id', 'name', 'serial_no']);
|
$machines = Machine::orderBy('name')->get(['id', 'name', 'serial_no']);
|
||||||
|
$products = Product::with('translations')->orderBy('name')->get(['id', 'name', 'image_url', 'name_dictionary_key']);
|
||||||
|
|
||||||
return view('admin.warehouses.replenishments', compact('orders', 'warehouses', 'machines'));
|
// 同租戶帳號(用於指派人員)
|
||||||
|
$users = \App\Models\System\User::where('company_id', Auth::user()->company_id)
|
||||||
|
->where('status', 'active')
|
||||||
|
->orderBy('name')
|
||||||
|
->get(['id', 'name']);
|
||||||
|
|
||||||
|
return view('admin.warehouses.replenishments', compact('orders', 'warehouses', 'machines', 'products', 'users'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 建立補貨單
|
* 建立補貨單(含 current_stock/max_stock 快照)
|
||||||
*/
|
*/
|
||||||
public function storeReplenishment(Request $request)
|
public function storeReplenishment(Request $request)
|
||||||
{
|
{
|
||||||
@ -517,12 +655,30 @@ class WarehouseController extends Controller
|
|||||||
'items.*.product_id' => 'required|exists:products,id',
|
'items.*.product_id' => 'required|exists:products,id',
|
||||||
'items.*.slot_no' => 'required|integer',
|
'items.*.slot_no' => 'required|integer',
|
||||||
'items.*.quantity' => 'required|integer|min:1',
|
'items.*.quantity' => 'required|integer|min:1',
|
||||||
|
'items.*.current_stock' => 'nullable|integer|min:0',
|
||||||
|
'items.*.max_stock' => 'nullable|integer|min:0',
|
||||||
|
], [
|
||||||
|
'warehouse_id.required' => __('Please select source warehouse'),
|
||||||
|
'machine_id.required' => __('Please select target machine'),
|
||||||
|
'items.required' => __('Please add at least one item'),
|
||||||
|
'items.*.product_id.required' => __('Please select product for item :num', ['num' => ':position']),
|
||||||
|
'items.*.quantity.min' => __('Please enter valid quantity for item :num', ['num' => ':position']),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
DB::transaction(function () use ($validated) {
|
DB::transaction(function () use ($validated) {
|
||||||
|
$prefix = 'RP-' . now()->format('Ymd') . '-';
|
||||||
|
$lastOrder = ReplenishmentOrder::withoutGlobalScopes()
|
||||||
|
->withTrashed()
|
||||||
|
->where('order_no', 'like', $prefix . '%')
|
||||||
|
->orderBy('order_no', 'desc')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$nextNumber = $lastOrder ? (int)substr($lastOrder->order_no, -4) + 1 : 1;
|
||||||
|
$order_no = $prefix . str_pad($nextNumber, 4, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
$order = ReplenishmentOrder::create([
|
$order = ReplenishmentOrder::create([
|
||||||
'company_id' => Auth::user()->company_id,
|
'company_id' => Auth::user()->company_id,
|
||||||
'order_no' => 'RP-' . now()->format('Ymd') . '-' . str_pad(ReplenishmentOrder::whereDate('created_at', today())->count() + 1, 4, '0', STR_PAD_LEFT),
|
'order_no' => $order_no,
|
||||||
'warehouse_id' => $validated['warehouse_id'],
|
'warehouse_id' => $validated['warehouse_id'],
|
||||||
'machine_id' => $validated['machine_id'],
|
'machine_id' => $validated['machine_id'],
|
||||||
'status' => ReplenishmentOrder::STATUS_PENDING,
|
'status' => ReplenishmentOrder::STATUS_PENDING,
|
||||||
@ -530,40 +686,194 @@ class WarehouseController extends Controller
|
|||||||
'created_by' => Auth::id(),
|
'created_by' => Auth::id(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 批量寫入明細(遵循 DB Best Practice: 不在迴圈中逐筆 create)
|
||||||
|
$itemsData = [];
|
||||||
foreach ($validated['items'] as $item) {
|
foreach ($validated['items'] as $item) {
|
||||||
$order->items()->create($item);
|
$itemsData[] = [
|
||||||
|
'replenishment_order_id' => $order->id,
|
||||||
|
'product_id' => $item['product_id'],
|
||||||
|
'slot_no' => $item['slot_no'],
|
||||||
|
'quantity' => $item['quantity'],
|
||||||
|
'current_stock' => $item['current_stock'] ?? 0,
|
||||||
|
'max_stock' => $item['max_stock'] ?? 0,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
ReplenishmentOrderItem::insert($itemsData);
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($request->ajax()) {
|
if ($request->ajax()) {
|
||||||
|
session()->flash('success', __('Replenishment order created successfully'));
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => __('Replenishment order created')
|
'message' => __('Replenishment order created successfully')
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->route('admin.warehouses.replenishments')
|
return redirect()->route('admin.warehouses.replenishments')
|
||||||
->with('success', __('Replenishment order created'));
|
->with('success', __('Replenishment order created successfully'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 確認補貨完成(扣倉庫庫存 + 增加機台貨道庫存)
|
* 一鍵補貨 — 自動讀取機台所有貨道,計算 max_stock - stock
|
||||||
*/
|
*/
|
||||||
public function confirmReplenishment(ReplenishmentOrder $replenishmentOrder)
|
public function autoReplenishment(Request $request)
|
||||||
{
|
{
|
||||||
if ($replenishmentOrder->status === ReplenishmentOrder::STATUS_COMPLETED) {
|
$validated = $request->validate([
|
||||||
return back()->with('error', __('Order already completed'));
|
'warehouse_id' => 'required|exists:warehouses,id',
|
||||||
|
'machine_id' => 'required|exists:machines,id',
|
||||||
|
'note' => 'nullable|string|max:500',
|
||||||
|
'items' => 'nullable|array',
|
||||||
|
'items.*.slot_no' => 'required_with:items|integer',
|
||||||
|
'items.*.product_id' => 'required_with:items|exists:products,id',
|
||||||
|
'items.*.quantity' => 'required_with:items|integer|min:1',
|
||||||
|
'items.*.current_stock' => 'nullable|integer|min:0',
|
||||||
|
'items.*.max_stock' => 'nullable|integer|min:0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$machine = Machine::findOrFail($validated['machine_id']);
|
||||||
|
|
||||||
|
// 如果前端未提供 items(首次請求),自動計算補貨清單
|
||||||
|
if (empty($validated['items'])) {
|
||||||
|
$slots = MachineSlot::with('product:id,name,image_url,name_dictionary_key')
|
||||||
|
->where('machine_id', $machine->id)
|
||||||
|
->whereNotNull('product_id')
|
||||||
|
->whereColumn('stock', '<', 'max_stock')
|
||||||
|
->orderByRaw('CAST(slot_no AS UNSIGNED) ASC')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($slots->isEmpty()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => __('All slots are fully stocked')
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查倉庫庫存
|
||||||
|
$warehouse = Warehouse::findOrFail($validated['warehouse_id']);
|
||||||
|
$warehouseStocks = WarehouseStock::where('warehouse_id', $warehouse->id)
|
||||||
|
->pluck('quantity', 'product_id');
|
||||||
|
|
||||||
|
// 按商品彙總需求量
|
||||||
|
$demandByProduct = [];
|
||||||
|
foreach ($slots as $slot) {
|
||||||
|
$pid = $slot->product_id;
|
||||||
|
$need = $slot->max_stock - $slot->stock;
|
||||||
|
$demandByProduct[$pid] = ($demandByProduct[$pid] ?? 0) + $need;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查是否有庫存不足的商品
|
||||||
|
$insufficientProducts = [];
|
||||||
|
foreach ($demandByProduct as $pid => $totalNeed) {
|
||||||
|
$available = $warehouseStocks[$pid] ?? 0;
|
||||||
|
if ($available < $totalNeed) {
|
||||||
|
$insufficientProducts[$pid] = [
|
||||||
|
'available' => $available,
|
||||||
|
'needed' => $totalNeed,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$slotData = $slots->map(fn($s) => [
|
||||||
|
'slot_no' => $s->slot_no,
|
||||||
|
'product_id' => $s->product_id,
|
||||||
|
'product_name' => $s->product?->localized_name ?? 'Unknown',
|
||||||
|
'image_url' => $s->product?->image_url,
|
||||||
|
'current_stock' => $s->stock,
|
||||||
|
'max_stock' => $s->max_stock,
|
||||||
|
'quantity' => $s->max_stock - $s->stock,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'preview' => true,
|
||||||
|
'machine_name' => $machine->name,
|
||||||
|
'slots' => $slotData,
|
||||||
|
'insufficient' => $insufficientProducts,
|
||||||
|
'warehouse_stocks' => $warehouseStocks,
|
||||||
|
'has_warning' => !empty($insufficientProducts),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
DB::transaction(function () use ($replenishmentOrder) {
|
// 有 items = 確認建單
|
||||||
|
DB::transaction(function () use ($validated) {
|
||||||
|
$prefix = 'RP-' . now()->format('Ymd') . '-';
|
||||||
|
$lastOrder = ReplenishmentOrder::withoutGlobalScopes()
|
||||||
|
->withTrashed()
|
||||||
|
->where('order_no', 'like', $prefix . '%')
|
||||||
|
->orderBy('order_no', 'desc')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$nextNumber = $lastOrder ? (int)substr($lastOrder->order_no, -4) + 1 : 1;
|
||||||
|
$order_no = $prefix . str_pad($nextNumber, 4, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
$order = ReplenishmentOrder::create([
|
||||||
|
'company_id' => Auth::user()->company_id,
|
||||||
|
'order_no' => $order_no,
|
||||||
|
'warehouse_id' => $validated['warehouse_id'],
|
||||||
|
'machine_id' => $validated['machine_id'],
|
||||||
|
'status' => ReplenishmentOrder::STATUS_PENDING,
|
||||||
|
'note' => $validated['note'] ?? __('Auto Replenishment'),
|
||||||
|
'created_by' => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$itemsData = [];
|
||||||
|
foreach ($validated['items'] as $item) {
|
||||||
|
$itemsData[] = [
|
||||||
|
'replenishment_order_id' => $order->id,
|
||||||
|
'product_id' => $item['product_id'],
|
||||||
|
'slot_no' => $item['slot_no'],
|
||||||
|
'quantity' => $item['quantity'],
|
||||||
|
'current_stock' => $item['current_stock'] ?? 0,
|
||||||
|
'max_stock' => $item['max_stock'] ?? 0,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
ReplenishmentOrderItem::insert($itemsData);
|
||||||
|
});
|
||||||
|
|
||||||
|
session()->flash('success', __('Replenishment order created successfully'));
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => __('Replenishment order created successfully')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 狀態推進 (pending → prepared → delivering → completed)
|
||||||
|
*/
|
||||||
|
public function updateReplenishmentStatus(Request $request, ReplenishmentOrder $replenishmentOrder)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'status' => 'required|in:prepared,delivering,completed',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$newStatus = $validated['status'];
|
||||||
|
|
||||||
|
if (!$replenishmentOrder->canTransitionTo($newStatus)) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => __('Invalid status transition')
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::transaction(function () use ($replenishmentOrder, $newStatus) {
|
||||||
$replenishmentOrder->loadMissing('items');
|
$replenishmentOrder->loadMissing('items');
|
||||||
|
|
||||||
foreach ($replenishmentOrder->items as $item) {
|
// pending → prepared:扣減倉庫庫存
|
||||||
// 扣除倉庫庫存
|
if ($newStatus === ReplenishmentOrder::STATUS_PREPARED) {
|
||||||
$stock = WarehouseStock::where('warehouse_id', $replenishmentOrder->warehouse_id)
|
foreach ($replenishmentOrder->items as $item) {
|
||||||
->where('product_id', $item->product_id)->first();
|
$stock = WarehouseStock::where('warehouse_id', $replenishmentOrder->warehouse_id)
|
||||||
|
->where('product_id', $item->product_id)
|
||||||
|
->lockForUpdate()
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$stock || $stock->quantity < $item->quantity) {
|
||||||
|
throw new \Exception(__('Warehouse stock insufficient') . " ({$item->product?->name})");
|
||||||
|
}
|
||||||
|
|
||||||
if ($stock && $stock->quantity >= $item->quantity) {
|
|
||||||
$before = $stock->quantity;
|
$before = $stock->quantity;
|
||||||
$stock->decrement('quantity', $item->quantity);
|
$stock->decrement('quantity', $item->quantity);
|
||||||
|
|
||||||
@ -577,26 +887,208 @@ class WarehouseController extends Controller
|
|||||||
'after_qty' => $before - $item->quantity,
|
'after_qty' => $before - $item->quantity,
|
||||||
'reference_type' => ReplenishmentOrder::class,
|
'reference_type' => ReplenishmentOrder::class,
|
||||||
'reference_id' => $replenishmentOrder->id,
|
'reference_id' => $replenishmentOrder->id,
|
||||||
'note' => __('Replenishment') . ' #' . $replenishmentOrder->order_no,
|
'note' => __('Replenishment prepare') . ' #' . $replenishmentOrder->order_no,
|
||||||
'created_by' => Auth::id(),
|
'created_by' => Auth::id(),
|
||||||
'created_at' => now(),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 增加機台貨道庫存
|
// delivering → completed:增加機台貨道庫存
|
||||||
MachineSlot::where('machine_id', $replenishmentOrder->machine_id)
|
if ($newStatus === ReplenishmentOrder::STATUS_COMPLETED) {
|
||||||
->where('slot_no', $item->slot_no)
|
foreach ($replenishmentOrder->items as $item) {
|
||||||
->increment('stock', $item->quantity);
|
MachineSlot::where('machine_id', $replenishmentOrder->machine_id)
|
||||||
|
->where('slot_no', $item->slot_no)
|
||||||
|
->increment('stock', $item->quantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
$replenishmentOrder->completed_at = now();
|
||||||
|
}
|
||||||
|
|
||||||
|
$replenishmentOrder->status = $newStatus;
|
||||||
|
$replenishmentOrder->save();
|
||||||
|
});
|
||||||
|
|
||||||
|
$statusLabels = [
|
||||||
|
'prepared' => __('Confirm Prepare'),
|
||||||
|
'delivering' => __('Start Delivery'),
|
||||||
|
'completed' => __('Confirm Complete'),
|
||||||
|
];
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => __('Status updated successfully')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消補貨單 — 若已備貨/配送中則退回倉庫庫存
|
||||||
|
*/
|
||||||
|
public function cancelReplenishment(ReplenishmentOrder $replenishmentOrder)
|
||||||
|
{
|
||||||
|
if (!$replenishmentOrder->canTransitionTo(ReplenishmentOrder::STATUS_CANCELLED)) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => __('Cannot cancel this order')
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::transaction(function () use ($replenishmentOrder) {
|
||||||
|
// 若已備貨(prepared)或配送中(delivering),需退回倉庫庫存
|
||||||
|
if (in_array($replenishmentOrder->status, [ReplenishmentOrder::STATUS_PREPARED, ReplenishmentOrder::STATUS_DELIVERING])) {
|
||||||
|
$replenishmentOrder->loadMissing('items');
|
||||||
|
|
||||||
|
foreach ($replenishmentOrder->items as $item) {
|
||||||
|
$stock = WarehouseStock::where('warehouse_id', $replenishmentOrder->warehouse_id)
|
||||||
|
->where('product_id', $item->product_id)
|
||||||
|
->lockForUpdate()
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($stock) {
|
||||||
|
$before = $stock->quantity;
|
||||||
|
$stock->increment('quantity', $item->quantity);
|
||||||
|
|
||||||
|
StockMovement::create([
|
||||||
|
'company_id' => $replenishmentOrder->company_id,
|
||||||
|
'warehouse_id' => $replenishmentOrder->warehouse_id,
|
||||||
|
'product_id' => $item->product_id,
|
||||||
|
'type' => 'in',
|
||||||
|
'quantity' => $item->quantity,
|
||||||
|
'before_qty' => $before,
|
||||||
|
'after_qty' => $before + $item->quantity,
|
||||||
|
'reference_type' => ReplenishmentOrder::class,
|
||||||
|
'reference_id' => $replenishmentOrder->id,
|
||||||
|
'note' => __('Replenishment cancelled, stock returned') . ' #' . $replenishmentOrder->order_no,
|
||||||
|
'created_by' => Auth::id(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$replenishmentOrder->update([
|
$replenishmentOrder->update([
|
||||||
'status' => ReplenishmentOrder::STATUS_COMPLETED,
|
'status' => ReplenishmentOrder::STATUS_CANCELLED,
|
||||||
'completed_at' => now(),
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
return back()->with('success', __('Replenishment completed'));
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => __('Order has been cancelled'),
|
||||||
|
'stock_returned' => in_array($replenishmentOrder->getOriginal('status'), [ReplenishmentOrder::STATUS_PREPARED, ReplenishmentOrder::STATUS_DELIVERING]),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指派補貨人員(僅限同租戶帳號)
|
||||||
|
*/
|
||||||
|
public function assignReplenishment(Request $request, ReplenishmentOrder $replenishmentOrder)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'assigned_to' => 'required|exists:users,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 確保指派人員屬於同租戶
|
||||||
|
$user = \App\Models\System\User::where('id', $validated['assigned_to'])
|
||||||
|
->where('company_id', Auth::user()->company_id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => __('Invalid personnel selection')
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$replenishmentOrder->update(['assigned_to' => $user->id]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => __('Personnel assigned successfully'),
|
||||||
|
'assignee_name' => $user->name,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX:取得特定機台的貨道資訊供補貨選擇
|
||||||
|
*/
|
||||||
|
public function getMachineSlotsForReplenishment(Machine $machine)
|
||||||
|
{
|
||||||
|
$slots = $machine->slots()
|
||||||
|
->with(['product' => fn($q) => $q->select('id', 'name', 'image_url', 'name_dictionary_key')->with('translations')])
|
||||||
|
->orderByRaw('CAST(slot_no AS UNSIGNED) ASC')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'machine' => [
|
||||||
|
'id' => $machine->id,
|
||||||
|
'name' => $machine->name,
|
||||||
|
'serial_no' => $machine->serial_no,
|
||||||
|
],
|
||||||
|
'slots' => $slots->map(fn($s) => [
|
||||||
|
'slot_no' => $s->slot_no,
|
||||||
|
'product_id' => $s->product_id,
|
||||||
|
'product_name' => $s->product?->localized_name ?? null,
|
||||||
|
'image_url' => $s->product?->image_url,
|
||||||
|
'stock' => $s->stock,
|
||||||
|
'max_stock' => $s->max_stock,
|
||||||
|
'needs_replenishment' => $s->product_id && $s->stock < $s->max_stock,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX:取得補貨單詳情
|
||||||
|
*/
|
||||||
|
public function replenishmentOrderDetails(Request $request, $id)
|
||||||
|
{
|
||||||
|
$order = ReplenishmentOrder::with(['machine', 'warehouse', 'creator', 'assignee'])
|
||||||
|
->where('id', $id)
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$items = ReplenishmentOrderItem::with('product.translations')
|
||||||
|
->where('replenishment_order_id', $id)
|
||||||
|
->get()
|
||||||
|
->map(function ($item) {
|
||||||
|
return [
|
||||||
|
'product_id' => $item->product_id,
|
||||||
|
'product_name' => $item->product?->localized_name ?? 'Unknown',
|
||||||
|
'image_url' => $item->product?->image_url,
|
||||||
|
'quantity' => $item->quantity,
|
||||||
|
'slot_no' => $item->slot_no,
|
||||||
|
'current_stock' => $item->current_stock,
|
||||||
|
'max_stock' => $item->max_stock,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'order' => [
|
||||||
|
'id' => $order->id,
|
||||||
|
'order_no' => $order->order_no,
|
||||||
|
'status' => $order->status,
|
||||||
|
'machine_name' => $order->machine?->name,
|
||||||
|
'machine_serial' => $order->machine?->serial_no,
|
||||||
|
'warehouse_name' => $order->warehouse?->name,
|
||||||
|
'creator_name' => $order->creator?->name,
|
||||||
|
'assignee_name' => $order->assignee?->name,
|
||||||
|
'note' => $order->note,
|
||||||
|
'created_at' => $order->created_at->format('Y-m-d H:i:s'),
|
||||||
|
'completed_at' => $order->completed_at?->format('Y-m-d H:i:s'),
|
||||||
|
],
|
||||||
|
'items' => $items
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 舊版 confirmReplenishment 已重構為 updateReplenishmentStatus
|
||||||
|
* 保留向下相容
|
||||||
|
*/
|
||||||
|
public function confirmReplenishment(ReplenishmentOrder $replenishmentOrder)
|
||||||
|
{
|
||||||
|
// 模擬直接完成流程:pending → completed
|
||||||
|
request()->merge(['status' => 'completed']);
|
||||||
|
return $this->updateReplenishmentStatus(request(), $replenishmentOrder);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AJAX:取得來源庫存(倉庫或機台)
|
* AJAX:取得來源庫存(倉庫或機台)
|
||||||
*/
|
*/
|
||||||
@ -609,21 +1101,21 @@ class WarehouseController extends Controller
|
|||||||
if ($warehouseId) {
|
if ($warehouseId) {
|
||||||
// Warehouse uses TenantScoped, findOrFail will respect it
|
// Warehouse uses TenantScoped, findOrFail will respect it
|
||||||
$warehouse = Warehouse::findOrFail($warehouseId);
|
$warehouse = Warehouse::findOrFail($warehouseId);
|
||||||
$stocks = WarehouseStock::with('product:id,name,image_url')
|
$stocks = WarehouseStock::with(['product' => fn($q) => $q->select('id', 'name', 'image_url', 'name_dictionary_key')->with('translations')])
|
||||||
->where('warehouse_id', $warehouse->id)
|
->where('warehouse_id', $warehouse->id)
|
||||||
->where('quantity', '>', 0)
|
->where('quantity', '>', 0)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$data = $stocks->map(fn($s) => [
|
$data = $stocks->map(fn($s) => [
|
||||||
'id' => $s->product_id,
|
'id' => $s->product_id,
|
||||||
'name' => $s->product?->name ?? 'Unknown',
|
'name' => $s->product?->localized_name ?? 'Unknown',
|
||||||
'quantity' => $s->quantity,
|
'quantity' => $s->quantity,
|
||||||
'image' => $s->product?->image_url
|
'image' => $s->product?->image_url
|
||||||
]);
|
]);
|
||||||
} elseif ($machineId) {
|
} elseif ($machineId) {
|
||||||
// Machine uses TenantScoped and machine_access scope
|
// Machine uses TenantScoped and machine_access scope
|
||||||
$machine = Machine::findOrFail($machineId);
|
$machine = Machine::findOrFail($machineId);
|
||||||
$slots = MachineSlot::with('product:id,name,image_url')
|
$slots = MachineSlot::with(['product' => fn($q) => $q->select('id', 'name', 'image_url', 'name_dictionary_key')->with('translations')])
|
||||||
->where('machine_id', $machine->id)
|
->where('machine_id', $machine->id)
|
||||||
->where('stock', '>', 0)
|
->where('stock', '>', 0)
|
||||||
->get()
|
->get()
|
||||||
@ -633,13 +1125,13 @@ class WarehouseController extends Controller
|
|||||||
$p = $group->first()->product;
|
$p = $group->first()->product;
|
||||||
return [
|
return [
|
||||||
'id' => $group->first()->product_id,
|
'id' => $group->first()->product_id,
|
||||||
'name' => $p?->name ?? 'Unknown',
|
'name' => $p?->localized_name ?? 'Unknown',
|
||||||
'quantity' => $group->sum('stock'),
|
'quantity' => $group->sum('stock'),
|
||||||
'image' => $p?->image_url
|
'image' => $p?->image_url
|
||||||
];
|
];
|
||||||
})->values();
|
})->values();
|
||||||
} else {
|
} else {
|
||||||
return response()->json(['success' => false, 'message' => 'No source provided']);
|
return response()->json(['success' => false, 'message' => __('No source provided')]);
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return response()->json(['success' => false, 'message' => __('Unauthorized or not found')]);
|
return response()->json(['success' => false, 'message' => __('Unauthorized or not found')]);
|
||||||
@ -653,7 +1145,7 @@ class WarehouseController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function warehouseStocks(Warehouse $warehouse, Request $request)
|
public function warehouseStocks(Warehouse $warehouse, Request $request)
|
||||||
{
|
{
|
||||||
$query = $warehouse->stocks()->with('product:id,name,image_url');
|
$query = $warehouse->stocks()->with(['product' => fn($q) => $q->select('id', 'name', 'image_url', 'name_dictionary_key')->with('translations')]);
|
||||||
|
|
||||||
if ($search = $request->input('search')) {
|
if ($search = $request->input('search')) {
|
||||||
$query->whereHas('product', fn($q) => $q->where('name', 'like', "%{$search}%"));
|
$query->whereHas('product', fn($q) => $q->where('name', 'like', "%{$search}%"));
|
||||||
@ -671,7 +1163,7 @@ class WarehouseController extends Controller
|
|||||||
'stocks' => $stocks->map(fn($s) => [
|
'stocks' => $stocks->map(fn($s) => [
|
||||||
'id' => $s->id,
|
'id' => $s->id,
|
||||||
'product_id' => $s->product_id,
|
'product_id' => $s->product_id,
|
||||||
'product_name' => $s->product?->name ?? 'Unknown',
|
'product_name' => $s->product?->localized_name ?? 'Unknown',
|
||||||
'image_url' => $s->product?->image_url,
|
'image_url' => $s->product?->image_url,
|
||||||
'quantity' => (int)$s->quantity,
|
'quantity' => (int)$s->quantity,
|
||||||
'safety_stock' => (int)$s->safety_stock,
|
'safety_stock' => (int)$s->safety_stock,
|
||||||
@ -684,7 +1176,7 @@ class WarehouseController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function stockInOrderDetails(StockInOrder $order)
|
public function stockInOrderDetails(StockInOrder $order)
|
||||||
{
|
{
|
||||||
$order->load(['warehouse', 'creator', 'items.product']);
|
$order->load(['warehouse', 'creator', 'items.product.translations']);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
@ -699,69 +1191,32 @@ class WarehouseController extends Controller
|
|||||||
'note' => $order->note,
|
'note' => $order->note,
|
||||||
],
|
],
|
||||||
'items' => $order->items->map(fn($item) => [
|
'items' => $order->items->map(fn($item) => [
|
||||||
'product_name' => $item->product?->name ?? 'Unknown',
|
'product_name' => $item->product?->localized_name ?? 'Unknown',
|
||||||
'image_url' => $item->product?->image_url,
|
'image_url' => $item->product?->image_url,
|
||||||
'quantity' => $item->quantity,
|
'quantity' => $item->quantity,
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Get replenishment order details for AJAX.
|
|
||||||
*/
|
|
||||||
public function replenishmentOrderDetails(Request $request, $id)
|
|
||||||
{
|
|
||||||
$order = \App\Models\WarehouseReplenishment::with(['machine', 'warehouse', 'creator'])
|
|
||||||
->where('id', $id)
|
|
||||||
->firstOrFail();
|
|
||||||
|
|
||||||
$items = \App\Models\WarehouseReplenishmentItem::with('product')
|
|
||||||
->where('replenishment_id', $id)
|
|
||||||
->get()
|
|
||||||
->map(function($item) {
|
|
||||||
return [
|
|
||||||
'product_id' => $item->product_id,
|
|
||||||
'product_name' => $item->product?->name ?? 'Unknown',
|
|
||||||
'image_url' => $item->product?->image_url,
|
|
||||||
'quantity' => $item->quantity,
|
|
||||||
'slot_no' => $item->slot_no,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'success' => true,
|
|
||||||
'order' => [
|
|
||||||
'id' => $order->id,
|
|
||||||
'order_no' => $order->order_no,
|
|
||||||
'status' => $order->status,
|
|
||||||
'machine_name' => $order->machine?->name,
|
|
||||||
'warehouse_name' => $order->warehouse?->name,
|
|
||||||
'creator_name' => $order->creator?->name,
|
|
||||||
'note' => $order->note,
|
|
||||||
'created_at' => $order->created_at->format('Y-m-d H:i:s'),
|
|
||||||
'completed_at' => $order->completed_at?->format('Y-m-d H:i:s'),
|
|
||||||
],
|
|
||||||
'items' => $items
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get transfer order details for AJAX.
|
* Get transfer order details for AJAX.
|
||||||
*/
|
*/
|
||||||
public function transferOrderDetails(Request $request, $id)
|
public function transferOrderDetails(Request $request, $id)
|
||||||
{
|
{
|
||||||
$order = \App\Models\WarehouseTransfer::with(['fromWarehouse', 'toWarehouse', 'fromMachine', 'creator'])
|
$order = TransferOrder::with(['fromWarehouse', 'toWarehouse', 'fromMachine', 'creator'])
|
||||||
->where('id', $id)
|
->where('id', $id)
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
$items = \App\Models\WarehouseTransferItem::with('product')
|
$items = \App\Models\Warehouse\TransferOrderItem::with('product.translations')
|
||||||
->where('transfer_id', $id)
|
->where('transfer_order_id', $id)
|
||||||
->get()
|
->get()
|
||||||
->map(function($item) {
|
->map(function($item) {
|
||||||
return [
|
return [
|
||||||
'product_id' => $item->product_id,
|
'product_id' => $item->product_id,
|
||||||
'product_name' => $item->product?->name ?? 'Unknown',
|
'product_name' => $item->product?->localized_name ?? __('Unknown'),
|
||||||
'image_url' => $item->product?->image_url,
|
'image_url' => $item->product?->image_url,
|
||||||
'quantity' => $item->quantity,
|
'quantity' => (int)$item->quantity,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -770,11 +1225,13 @@ class WarehouseController extends Controller
|
|||||||
'order' => [
|
'order' => [
|
||||||
'id' => $order->id,
|
'id' => $order->id,
|
||||||
'order_no' => $order->order_no,
|
'order_no' => $order->order_no,
|
||||||
'status' => $order->status,
|
|
||||||
'type' => $order->type,
|
'type' => $order->type,
|
||||||
'from_name' => $order->type === 'warehouse_to_warehouse' ? $order->fromWarehouse?->name : $order->fromMachine?->name,
|
'from_name' => $order->type === 'warehouse_to_warehouse'
|
||||||
'to_warehouse_name' => $order->toWarehouse?->name,
|
? ($order->fromWarehouse?->name ?? __('Unknown'))
|
||||||
'creator_name' => $order->creator?->name,
|
: ($order->fromMachine?->name ?? __('Unknown')),
|
||||||
|
'to_name' => $order->toWarehouse?->name ?? __('Unknown'),
|
||||||
|
'status' => $order->status,
|
||||||
|
'creator_name' => $order->creator?->name ?? __('System'),
|
||||||
'note' => $order->note,
|
'note' => $order->note,
|
||||||
'created_at' => $order->created_at->format('Y-m-d H:i:s'),
|
'created_at' => $order->created_at->format('Y-m-d H:i:s'),
|
||||||
'completed_at' => $order->completed_at?->format('Y-m-d H:i:s'),
|
'completed_at' => $order->completed_at?->format('Y-m-d H:i:s'),
|
||||||
|
|||||||
@ -274,4 +274,9 @@ class Machine extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsToMany(\App\Models\System\User::class);
|
return $this->belongsToMany(\App\Models\System\User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function orders()
|
||||||
|
{
|
||||||
|
return $this->hasMany(\App\Models\Transaction\Order::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,42 @@ class ReplenishmentOrder extends Model
|
|||||||
public const STATUS_COMPLETED = 'completed';
|
public const STATUS_COMPLETED = 'completed';
|
||||||
public const STATUS_CANCELLED = 'cancelled';
|
public const STATUS_CANCELLED = 'cancelled';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合法狀態轉換定義
|
||||||
|
* key = 當前狀態, value = 可轉換至的狀態陣列
|
||||||
|
*/
|
||||||
|
public const TRANSITIONS = [
|
||||||
|
self::STATUS_PENDING => [self::STATUS_PREPARED, self::STATUS_CANCELLED],
|
||||||
|
self::STATUS_PREPARED => [self::STATUS_DELIVERING, self::STATUS_CANCELLED],
|
||||||
|
self::STATUS_DELIVERING => [self::STATUS_COMPLETED, self::STATUS_CANCELLED],
|
||||||
|
self::STATUS_COMPLETED => [],
|
||||||
|
self::STATUS_CANCELLED => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 檢查是否可轉換至指定狀態
|
||||||
|
*/
|
||||||
|
public function canTransitionTo(string $status): bool
|
||||||
|
{
|
||||||
|
return in_array($status, self::TRANSITIONS[$this->status] ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排除已取消的訂單
|
||||||
|
*/
|
||||||
|
public function scopeActive($query)
|
||||||
|
{
|
||||||
|
return $query->where('status', '!=', self::STATUS_CANCELLED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 補貨總數量
|
||||||
|
*/
|
||||||
|
public function getTotalQuantityAttribute(): int
|
||||||
|
{
|
||||||
|
return $this->items->sum('quantity');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 出貨倉庫
|
* 出貨倉庫
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -14,10 +14,14 @@ class ReplenishmentOrderItem extends Model
|
|||||||
'product_id',
|
'product_id',
|
||||||
'slot_no',
|
'slot_no',
|
||||||
'quantity',
|
'quantity',
|
||||||
|
'current_stock',
|
||||||
|
'max_stock',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'quantity' => 'integer',
|
'quantity' => 'integer',
|
||||||
|
'current_stock' => 'integer',
|
||||||
|
'max_stock' => 'integer',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -21,6 +21,10 @@ class StockInOrder extends Model
|
|||||||
'completed_at',
|
'completed_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public const STATUS_DRAFT = 'draft';
|
||||||
|
public const STATUS_COMPLETED = 'completed';
|
||||||
|
public const STATUS_CANCELLED = 'cancelled';
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'completed_at' => 'datetime',
|
'completed_at' => 'datetime',
|
||||||
];
|
];
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 新增建單時快照欄位,記錄當下機台庫存狀態
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('replenishment_order_items', function (Blueprint $table) {
|
||||||
|
$table->integer('current_stock')->default(0)->after('quantity')->comment('建單時的機台庫存');
|
||||||
|
$table->integer('max_stock')->default(0)->after('current_stock')->comment('貨道最大容量');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('replenishment_order_items', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['current_stock', 'max_stock']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
1571
lang/en.json
1571
lang/en.json
File diff suppressed because it is too large
Load Diff
107
lang/ja.json
107
lang/ja.json
@ -4,56 +4,84 @@
|
|||||||
"Add Warehouse": "倉庫を追加",
|
"Add Warehouse": "倉庫を追加",
|
||||||
"Adjustment": "在庫調整",
|
"Adjustment": "在庫調整",
|
||||||
"All Companies": "すべての会社",
|
"All Companies": "すべての会社",
|
||||||
|
"All slots are fully stocked": "全スロットが満在庫です",
|
||||||
"All Statuses": "すべてのステータス",
|
"All Statuses": "すべてのステータス",
|
||||||
"All Types": "すべてのタイプ",
|
"All Types": "すべてのタイプ",
|
||||||
"All Warehouses": "すべての倉庫",
|
"All Warehouses": "すべての倉庫",
|
||||||
"Approved At": "承認日時",
|
"Approved At": "承認日時",
|
||||||
|
"Are you sure you want to cancel this order? If the order has been prepared, stock will be returned to the warehouse.": "この注文をキャンセルしますか?準備済みの場合、在庫は倉庫に返却されます。",
|
||||||
"Are you sure you want to confirm this stock-in order? This action will update your inventory levels.": "この入庫伝票を確定してもよろしいですか?この操作により在庫レベルが更新されます。",
|
"Are you sure you want to confirm this stock-in order? This action will update your inventory levels.": "この入庫伝票を確定してもよろしいですか?この操作により在庫レベルが更新されます。",
|
||||||
|
"Are you sure you want to confirm this transfer? This will deduct stock from source and add to target.": "この転送を確定してもよろしいですか?この操作により発送元の在庫が減算され、対象の在庫に加算されます。",
|
||||||
|
"Are you sure you want to delete this draft order? This action cannot be undone.": "この下書き伝票を削除してもよろしいですか?この操作は取り消せません。",
|
||||||
|
"Assign Personnel": "担当者指定",
|
||||||
|
"Assigned To": "担当者",
|
||||||
|
"Auto Replenishment": "一括補充",
|
||||||
|
"Automatically calculate replenishment needs based on machine capacity": "マシン容量に基づいて補充ニーズを自動計算",
|
||||||
"Branch": "分倉庫",
|
"Branch": "分倉庫",
|
||||||
"Branch Warehouses": "分倉庫数",
|
"Branch Warehouses": "分倉庫数",
|
||||||
"CREATE STOCK TRANSFERS BETWEEN WAREHOUSES AND MACHINE RETURNS": "倉庫間の転送および機台からの返品伝票を作成",
|
"Calculate Replenishment": "補充量を計算",
|
||||||
|
"Cancel Order": "注文取消",
|
||||||
"Cancelled": "キャンセル済み",
|
"Cancelled": "キャンセル済み",
|
||||||
|
"Cannot cancel this order": "この注文はキャンセルできません",
|
||||||
"Cannot delete warehouse with existing stock": "在庫がある倉庫は削除できません。",
|
"Cannot delete warehouse with existing stock": "在庫がある倉庫は削除できません。",
|
||||||
"Card System": "カード決済システム",
|
"Card System": "カード決済システム",
|
||||||
"Card Terminal System": "カード端末システム",
|
"Card Terminal System": "カード端末システム",
|
||||||
"Cash Module": "現金決済モジュール",
|
"Cash Module": "現金決済モジュール",
|
||||||
"Cash Module Feature": "現金モジュール機能",
|
"Cash Module Feature": "現金モジュール機能",
|
||||||
"Coin\/Banknote Machine": "硬貨\/紙幣モジュール",
|
"Coin/Banknote Machine": "硬貨/紙幣モジュール",
|
||||||
"Coin\/Bill Acceptor": "コイン\/紙幣機",
|
"Coin/Bill Acceptor": "コイン/紙幣機",
|
||||||
"Completed": "完了",
|
"Completed": "完了",
|
||||||
|
"Completed At": "完了日時",
|
||||||
"Confirm": "確認",
|
"Confirm": "確認",
|
||||||
"Confirm Stock-in Order": "入庫伝票を確認",
|
"Confirm Cancel": "キャンセル確認",
|
||||||
|
"Confirm Complete": "完了を確認",
|
||||||
|
"Confirm Deletion": "削除の確認",
|
||||||
|
"Confirm Prepare": "準備確認",
|
||||||
"Confirm replenishment completed?": "補充が完了したことを確認しますか?",
|
"Confirm replenishment completed?": "補充が完了したことを確認しますか?",
|
||||||
|
"Confirm Stock-In": "入庫を確認",
|
||||||
|
"Confirm Stock-in Order": "入庫伝票を確認",
|
||||||
"Confirm this stock-in order?": "この入庫伝票を確認しますか?",
|
"Confirm this stock-in order?": "この入庫伝票を確認しますか?",
|
||||||
"Confirm this transfer?": "この転送を確認しますか?",
|
"Confirm this transfer?": "この転送を確認しますか?",
|
||||||
"Create Replenishment Order": "補充伝票を作成",
|
"Confirm Transfer": "転送を確認",
|
||||||
"Create Transfer Order": "転送伝票を作成",
|
"Confirm Transfer Order": "転送伝票を確認",
|
||||||
"Create and manage replenishment orders from warehouse to machines": "倉庫から機台への補充伝票の作成と管理",
|
"Create and manage replenishment orders from warehouse to machines": "倉庫から機台への補充伝票の作成と管理",
|
||||||
|
"Create Replenishment Order": "補充伝票を作成",
|
||||||
|
"CREATE STOCK TRANSFERS BETWEEN WAREHOUSES AND MACHINE RETURNS": "倉庫間の転送および機台からの返品伝票を作成",
|
||||||
"Create stock transfers between warehouses and machine returns": "倉庫間の転送および機台からの返品伝票を作成",
|
"Create stock transfers between warehouses and machine returns": "倉庫間の転送および機台からの返品伝票を作成",
|
||||||
|
"Create Transfer Order": "転送伝票を作成",
|
||||||
"Created At": "作成日時",
|
"Created At": "作成日時",
|
||||||
"Created By": "作成者",
|
"Created By": "作成者",
|
||||||
"Credit Card": "クレジットカード",
|
"Credit Card": "クレジットカード",
|
||||||
|
"Current Stock": "現在在庫",
|
||||||
"Damage": "破損",
|
"Damage": "破損",
|
||||||
|
"Delete Permanently": "永久に削除",
|
||||||
"Delivering": "配送中",
|
"Delivering": "配送中",
|
||||||
"Display Material Code": "素材コードを表示",
|
"Display Material Code": "素材コードを表示",
|
||||||
"Draft": "下書き",
|
"Draft": "下書き",
|
||||||
"E.SUN Bank Scan": "玉山銀行スキャン",
|
"E.SUN Bank Scan": "玉山銀行スキャン",
|
||||||
"E.SUN Pay": "E.SUN Pay",
|
"E.SUN Pay": "E.SUN Pay",
|
||||||
"E.SUN QR Pay": "玉山QR決済",
|
"E.SUN QR Pay": "玉山QR決済",
|
||||||
"ESUN": "玉山銀行 (ESUN)",
|
|
||||||
"Electronic Invoice": "電子翻訳",
|
"Electronic Invoice": "電子翻訳",
|
||||||
"Empty": "空",
|
"Empty": "空",
|
||||||
"Enable Points Mechanism": "ポイント機能を有効化",
|
"Enable Points Mechanism": "ポイント機能を有効化",
|
||||||
"Enabled Features": "有効な機能",
|
"Enabled Features": "有効な機能",
|
||||||
|
"Error": "エラー",
|
||||||
|
"ESUN": "玉山銀行 (ESUN)",
|
||||||
|
"Expected Stock": "予想在庫",
|
||||||
"Expiry": "有効期限",
|
"Expiry": "有効期限",
|
||||||
|
"Failed to load preview": "プレビューの読み込みに失敗しました",
|
||||||
|
"Fill Quantity": "補充数量",
|
||||||
"Fill Rate": "充填率",
|
"Fill Rate": "充填率",
|
||||||
"From": "発送元",
|
"From": "発送元",
|
||||||
"From Machine": "発送元機台",
|
"From Machine": "発送元機台",
|
||||||
"From Warehouse": "発送元倉庫",
|
"From Warehouse": "発送元倉庫",
|
||||||
|
"Go Back": "戻る",
|
||||||
"Goods & System Settings": "商品とシステム設定",
|
"Goods & System Settings": "商品とシステム設定",
|
||||||
"Image": "画像",
|
"Image": "画像",
|
||||||
"In Stock": "現在庫",
|
"In Stock": "現在庫",
|
||||||
"Includes Credit Card\/Mobile Pay": "クレジットカード\/モバイル決済を含む",
|
"Includes Credit Card/Mobile Pay": "クレジットカード/モバイル決済を含む",
|
||||||
|
"Invalid personnel selection": "無効な担当者選択",
|
||||||
|
"Invalid status transition": "無効なステータス遷移",
|
||||||
"Inventory Management": "在庫管理",
|
"Inventory Management": "在庫管理",
|
||||||
"LinePay": "LinePay",
|
"LinePay": "LinePay",
|
||||||
"LinePay Payment": "LinePay 決済",
|
"LinePay Payment": "LinePay 決済",
|
||||||
@ -71,8 +99,11 @@
|
|||||||
"Manage stock levels, stock-in orders, and movement history": "在庫レベル、入庫伝票、および移動履歴の管理",
|
"Manage stock levels, stock-in orders, and movement history": "在庫レベル、入庫伝票、および移動履歴の管理",
|
||||||
"Manage stock transfers between warehouses and machine returns": "倉庫間の転送および機台からの返品伝票を作成",
|
"Manage stock transfers between warehouses and machine returns": "倉庫間の転送および機台からの返品伝票を作成",
|
||||||
"Material Code": "素材コード",
|
"Material Code": "素材コード",
|
||||||
|
"Max Capacity": "最大容量",
|
||||||
|
"Max Stock": "最大在庫",
|
||||||
"Movement History": "移動履歴",
|
"Movement History": "移動履歴",
|
||||||
"Movement Logs": "移動ログ",
|
"Movement Logs": "移動ログ",
|
||||||
|
"Needs replenishment": "補充が必要",
|
||||||
"New Replenishment": "新規補充",
|
"New Replenishment": "新規補充",
|
||||||
"New Stock-In Order": "新規入庫伝票",
|
"New Stock-In Order": "新規入庫伝票",
|
||||||
"New Transfer": "新規転送",
|
"New Transfer": "新規転送",
|
||||||
@ -80,42 +111,60 @@
|
|||||||
"No products found in this warehouse": "この倉庫に在庫はありません",
|
"No products found in this warehouse": "この倉庫に在庫はありません",
|
||||||
"No replenishment orders found": "補充伝票が見つかりません",
|
"No replenishment orders found": "補充伝票が見つかりません",
|
||||||
"No slot data": "貨道データなし",
|
"No slot data": "貨道データなし",
|
||||||
|
"No slots need replenishment": "補充が必要なスロットはありません",
|
||||||
"No stock data found": "在庫データが見つかりません",
|
"No stock data found": "在庫データが見つかりません",
|
||||||
"No stock-in orders found": "入庫伝票が見つかりません",
|
"No stock-in orders found": "入庫伝票が見つかりません",
|
||||||
"No transfer orders found": "転送伝票が見つかりません",
|
"No transfer orders found": "転送伝票が見つかりません",
|
||||||
"Normal": "通常",
|
"Normal": "通常",
|
||||||
"Note": "備考",
|
"Note": "備考",
|
||||||
"Note (optional)": "備考 (任意)",
|
"Note (optional)": "備考 (任意)",
|
||||||
|
"Offline": "オフライン",
|
||||||
|
"Online": "オンライン",
|
||||||
|
"Operation failed": "操作に失敗しました",
|
||||||
"Optional": "任意",
|
"Optional": "任意",
|
||||||
"Order Details": "入庫伝票詳細",
|
"Order Details": "入庫伝票詳細",
|
||||||
|
"Order has been cancelled": "注文がキャンセルされました",
|
||||||
"Order No.": "伝票番号",
|
"Order No.": "伝票番号",
|
||||||
"Out of Stock": "在庫切れ",
|
"Out of Stock": "在庫切れ",
|
||||||
"Pending": "保留中",
|
"Pending": "保留中",
|
||||||
|
"Personnel assigned successfully": "担当者の指定が完了しました",
|
||||||
"Please enter configuration name": "設定名を入力してください",
|
"Please enter configuration name": "設定名を入力してください",
|
||||||
"Please select a company": "会社を選択してください",
|
"Please select a company": "会社を選択してください",
|
||||||
|
"Please select warehouse and machine": "倉庫とマシンを選択してください",
|
||||||
"Points": "ポイント",
|
"Points": "ポイント",
|
||||||
"Prepared": "準備済み",
|
"Prepared": "準備済み",
|
||||||
|
"Preparing": "準備中",
|
||||||
"Product": "商品",
|
"Product": "商品",
|
||||||
"Product Details": "商品詳細",
|
"Product Details": "商品詳細",
|
||||||
"Product ID": "商品ID",
|
"Product ID": "商品ID",
|
||||||
"Products": "商品",
|
"Products": "商品",
|
||||||
"Products \/ Stock": "商品 \/ 在庫",
|
"Products / Stock": "商品 / 在庫",
|
||||||
"Publish": "配信開始",
|
"Publish": "配信開始",
|
||||||
"QR Scan Payment": "QRスキャン決済機能",
|
"QR Scan Payment": "QRスキャン決済機能",
|
||||||
"Qty": "数量",
|
"Qty": "数量",
|
||||||
"Qty Change": "数量変更",
|
"Qty Change": "数量変更",
|
||||||
"Quantity": "数量",
|
"Quantity": "数量",
|
||||||
|
"Quick Replenish": "クイック補充",
|
||||||
"Real-time slot-level inventory across all machines": "すべての機台におけるリアルタイムの貨道別在庫",
|
"Real-time slot-level inventory across all machines": "すべての機台におけるリアルタイムの貨道別在庫",
|
||||||
|
"Replenishment cancelled, stock returned": "補充キャンセル、在庫返却",
|
||||||
"Replenishment Items": "補充項目",
|
"Replenishment Items": "補充項目",
|
||||||
|
"Replenishment order confirmed and inventory updated": "補充伝票が確定され、在庫が更新されました",
|
||||||
|
"Replenishment order created successfully": "補充伝票が正常に作成されました",
|
||||||
"Replenishment Orders": "機台補充伝票",
|
"Replenishment Orders": "機台補充伝票",
|
||||||
|
"Replenishment prepare": "補充準備",
|
||||||
"Reset Filters": "フィルターをリセット",
|
"Reset Filters": "フィルターをリセット",
|
||||||
"Safety Stock": "安全在庫",
|
"Safety Stock": "安全在庫",
|
||||||
"Search Product": "商品を検索",
|
"Sale Price": "販売価格",
|
||||||
"Search by name...": "名称で検索...",
|
"Search by name...": "名称で検索...",
|
||||||
"Search machines...": "機台を検索...",
|
"Search machines...": "機台を検索...",
|
||||||
|
"Search Product": "商品を検索",
|
||||||
"Search products...": "商品を検索...",
|
"Search products...": "商品を検索...",
|
||||||
"Search warehouses...": "倉庫を検索...",
|
"Search warehouses...": "倉庫を検索...",
|
||||||
|
"Select a team member to handle this replenishment order": "この補充注文を担当するメンバーを選択してください",
|
||||||
|
"Select all slots": "全スロットを選択",
|
||||||
"Select Machine": "機台を選択",
|
"Select Machine": "機台を選択",
|
||||||
|
"Select Machine First": "先にマシンを選択してください",
|
||||||
|
"Select Personnel": "担当者を選択",
|
||||||
"Select Product": "商品を選択",
|
"Select Product": "商品を選択",
|
||||||
"Select Warehouse": "倉庫を選択",
|
"Select Warehouse": "倉庫を選択",
|
||||||
"Serial No.": "シリアル番号",
|
"Serial No.": "シリアル番号",
|
||||||
@ -123,55 +172,75 @@
|
|||||||
"Shopping Cart": "ショッピングカート",
|
"Shopping Cart": "ショッピングカート",
|
||||||
"Shopping Cart Feature": "ショッピングカート機能",
|
"Shopping Cart Feature": "ショッピングカート機能",
|
||||||
"Slot": "貨道",
|
"Slot": "貨道",
|
||||||
|
"Slots need replenishment": "補充が必要なスロット",
|
||||||
|
"Source": "発送元",
|
||||||
"Source Warehouse": "発送元倉庫",
|
"Source Warehouse": "発送元倉庫",
|
||||||
|
"Start Delivery": "配送開始",
|
||||||
"Status": "ステータス",
|
"Status": "ステータス",
|
||||||
|
"Status Timeline": "ステータスタイムライン",
|
||||||
|
"Status updated successfully": "ステータス更新成功",
|
||||||
"Stock": "在庫",
|
"Stock": "在庫",
|
||||||
|
"Stock flow history": "在庫移動履歴",
|
||||||
"Stock In": "入庫",
|
"Stock In": "入庫",
|
||||||
|
"Stock Level": "在庫量",
|
||||||
"Stock Levels": "在庫レベル",
|
"Stock Levels": "在庫レベル",
|
||||||
"Stock Out": "出庫",
|
"Stock Out": "出庫",
|
||||||
"Stock flow history": "在庫移動履歴",
|
"Stock Rate": "在庫率",
|
||||||
|
"Stock returned to warehouse": "在庫が倉庫に返却されました",
|
||||||
"Stock-In Management": "入庫管理",
|
"Stock-In Management": "入庫管理",
|
||||||
|
"Stock-in order confirmed and inventory updated": "入庫伝票が確定され、在庫が更新されました",
|
||||||
|
"Stock-in order created successfully": "入庫伝票が正常に作成されました",
|
||||||
|
"Stock-in order deleted successfully": "入庫伝票が正常に削除されました",
|
||||||
"Stock-In Orders": "入庫伝票",
|
"Stock-In Orders": "入庫伝票",
|
||||||
"Stock-In Orders Tab": "入庫伝票",
|
"Stock-In Orders Tab": "入庫伝票",
|
||||||
"System settings updated successfully.": "システム設定が正常に更新されました。",
|
"System settings updated successfully.": "システム設定が正常に更新されました。",
|
||||||
|
"Target": "対象",
|
||||||
"Target Machine": "対象機台",
|
"Target Machine": "対象機台",
|
||||||
"Target Warehouse": "対象倉庫",
|
"Target Warehouse": "対象倉庫",
|
||||||
"Taxation System": "税務システム",
|
"Taxation System": "税務システム",
|
||||||
"Time": "時間",
|
"Time": "時間",
|
||||||
"To": "配送先",
|
"To": "配送先",
|
||||||
"To Warehouse": "配送先倉庫",
|
"To Warehouse": "配送先倉庫",
|
||||||
|
"Total Quantity": "合計数量",
|
||||||
"Total Stock": "総在庫",
|
"Total Stock": "総在庫",
|
||||||
"Total Warehouses": "倉庫総数",
|
"Total Warehouses": "倉庫総数",
|
||||||
"Track stock levels, stock-in orders, and movement history": "在庫レベル、入庫伝票、および移動履歴の追跡",
|
"Track stock levels, stock-in orders, and movement history": "在庫レベル、入庫伝票、および移動履歴の追跡",
|
||||||
"Track stock-in orders and movement history": "入庫伝票および移動履歴の追跡",
|
"Track stock-in orders and movement history": "入庫伝票および移動履歴の追跡",
|
||||||
"Transfer Audit": "転送監査",
|
"Transfer Audit": "転送監査",
|
||||||
|
"Transfer Details": "転送伝票詳細",
|
||||||
"Transfer In": "転送入",
|
"Transfer In": "転送入",
|
||||||
|
"Transfer order confirmed and inventory updated": "転送伝票が確定され、在庫が更新されました",
|
||||||
|
"Transfer order created successfully": "転送伝票が正常に作成されました",
|
||||||
|
"Transfer order deleted successfully": "転送伝票が正常に削除されました",
|
||||||
"Transfer Orders": "転送伝票",
|
"Transfer Orders": "転送伝票",
|
||||||
"Transfer Out": "転送出",
|
"Transfer Out": "転送出",
|
||||||
"Transfer Type": "転送タイプ",
|
"Transfer Type": "転送タイプ",
|
||||||
"Transfers": "転送",
|
"Transfers": "転送",
|
||||||
|
"Unassigned": "未割り当て",
|
||||||
"Unpublish": "配信終了",
|
"Unpublish": "配信終了",
|
||||||
"Update Settings": "設定を更新",
|
|
||||||
"Update failed": "更新に失敗しました",
|
"Update failed": "更新に失敗しました",
|
||||||
|
"Update Settings": "設定を更新",
|
||||||
"Video": "動画",
|
"Video": "動画",
|
||||||
"View Details": "詳細を表示",
|
"View Details": "詳細を表示",
|
||||||
"View Inventory": "在庫を表示",
|
"View Inventory": "在庫を表示",
|
||||||
"View Slots": "貨道を表示",
|
"View Slots": "貨道を表示",
|
||||||
|
"Visual overview of all machine locations": "全機器位置のビジュアル概要",
|
||||||
"Warehouse": "倉庫",
|
"Warehouse": "倉庫",
|
||||||
|
"Warehouse created successfully.": "倉庫が正常に作成されました。",
|
||||||
|
"Warehouse deleted successfully.": "倉庫が正常に削除されました。",
|
||||||
"Warehouse Info": "倉庫情報",
|
"Warehouse Info": "倉庫情報",
|
||||||
"Warehouse Inventory": "倉庫在庫状況",
|
"Warehouse Inventory": "倉庫在庫状況",
|
||||||
"Warehouse Management": "倉庫管理",
|
"Warehouse Management": "倉庫管理",
|
||||||
"Warehouse Overview": "倉庫概要",
|
"Warehouse Overview": "倉庫概要",
|
||||||
"Warehouse Transfer": "倉庫転送",
|
|
||||||
"Warehouse created successfully.": "倉庫が正常に作成されました。",
|
|
||||||
"Warehouse deleted successfully.": "倉庫が正常に削除されました。",
|
|
||||||
"Warehouse purchase records": "倉庫購入記録",
|
"Warehouse purchase records": "倉庫購入記録",
|
||||||
|
"Warehouse stock insufficient": "倉庫在庫不足",
|
||||||
|
"Warehouse stock insufficient warning": "倉庫在庫が全スロットを満たすには不足していますが、注文を作成することは可能です",
|
||||||
"Warehouse to Warehouse": "倉庫間転送",
|
"Warehouse to Warehouse": "倉庫間転送",
|
||||||
|
"Warehouse Transfer": "倉庫転送",
|
||||||
"Warehouse updated successfully.": "倉庫が正常に更新されました。",
|
"Warehouse updated successfully.": "倉庫が正常に更新されました。",
|
||||||
"Welcome Gift": "登録特典",
|
"Welcome Gift": "登録特典",
|
||||||
"Welcome Gift Feature": "来店特典機能",
|
"Welcome Gift Feature": "来店特典機能",
|
||||||
"Visual overview of all machine locations": "全機器位置のビジュアル概要",
|
"Stock / Capacity": "在庫 / 上限",
|
||||||
"Online": "オンライン",
|
"Warehouse Stock": "倉庫在庫",
|
||||||
"Offline": "オフライン",
|
"Recalculate": "再計算"
|
||||||
"Error": "エラー"
|
|
||||||
}
|
}
|
||||||
1667
lang/zh_TW.json
1667
lang/zh_TW.json
File diff suppressed because it is too large
Load Diff
@ -192,19 +192,38 @@
|
|||||||
:class="{ 'opacity-30 pointer-events-none transition-opacity duration-300': isLoading }">
|
:class="{ 'opacity-30 pointer-events-none transition-opacity duration-300': isLoading }">
|
||||||
{{-- Filters --}}
|
{{-- Filters --}}
|
||||||
<div class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-4">
|
<div class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-4">
|
||||||
<form action="{{ route('admin.warehouses.index') }}" method="GET" class="relative group"
|
<form action="{{ route('admin.warehouses.index') }}" method="GET" class="flex items-center gap-4"
|
||||||
@submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())">
|
@submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())">
|
||||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
<div class="relative group">
|
||||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
|
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"
|
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
|
||||||
stroke-linejoin="round">
|
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"
|
||||||
<circle cx="11" cy="11" r="8"></circle>
|
stroke-linejoin="round">
|
||||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<input type="text" name="search" value="{{ request('search') }}"
|
||||||
|
class="py-2.5 pl-12 pr-6 block w-64 luxury-input" placeholder="{{ __('Search warehouses...') }}">
|
||||||
|
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group" title="{{ __('Search') }}">
|
||||||
|
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</button>
|
||||||
<input type="text" name="search" value="{{ request('search') }}"
|
|
||||||
class="py-2.5 pl-12 pr-6 block w-64 luxury-input" placeholder="{{ __('Search warehouses...') }}">
|
<button type="button"
|
||||||
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
@click="
|
||||||
|
$el.closest('form').querySelectorAll('input[type=text]').forEach(i => i.value = '');
|
||||||
|
$el.closest('form').dispatchEvent(new Event('submit', { cancelable: true }));
|
||||||
|
"
|
||||||
|
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group" title="{{ __('Reset') }}">
|
||||||
|
<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="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>
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -9,6 +9,10 @@
|
|||||||
loading: false,
|
loading: false,
|
||||||
showStockInModal: false,
|
showStockInModal: false,
|
||||||
showConfirmModal: false,
|
showConfirmModal: false,
|
||||||
|
showDeleteModal: false,
|
||||||
|
deleteUrl: '',
|
||||||
|
stockInWarehouseId: '',
|
||||||
|
note: '',
|
||||||
pendingForm: null,
|
pendingForm: null,
|
||||||
items: [{ product_id: '', quantity: 1 }],
|
items: [{ product_id: '', quantity: 1 }],
|
||||||
|
|
||||||
@ -130,55 +134,79 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
async submitStockIn() {
|
async submitStockIn() {
|
||||||
const form = document.getElementById('stockInForm');
|
console.log('submitStockIn triggered');
|
||||||
const warehouseId = form.querySelector('[name="warehouse_id"]')?.value;
|
|
||||||
|
|
||||||
if (!warehouseId) {
|
const form = document.getElementById('stockInForm');
|
||||||
|
if (!form) {
|
||||||
|
console.error('Form not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const warehouseId = formData.get('warehouse_id');
|
||||||
|
|
||||||
|
if (!warehouseId || warehouseId.trim() === '') {
|
||||||
window.showToast?.('{{ __("Please select target warehouse") }}', 'error');
|
window.showToast?.('{{ __("Please select target warehouse") }}', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.items.length === 0) {
|
let hasItem = false;
|
||||||
|
let allValid = true;
|
||||||
|
|
||||||
|
// 抓取所有商品選擇器和數量框
|
||||||
|
const productSelects = form.querySelectorAll('select[name^="items["][name$="][product_id]"]');
|
||||||
|
const quantityInputs = form.querySelectorAll('input[name^="items["][name$="][quantity]"]');
|
||||||
|
|
||||||
|
if (productSelects.length === 0) {
|
||||||
window.showToast?.('{{ __("Please add at least one item") }}', 'error');
|
window.showToast?.('{{ __("Please add at least one item") }}', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasError = false;
|
productSelects.forEach((select, index) => {
|
||||||
this.items.forEach(item => {
|
hasItem = true;
|
||||||
if (!item.product_id || !item.quantity || item.quantity < 1) {
|
const pId = select.value;
|
||||||
hasError = true;
|
const qty = quantityInputs[index]?.value;
|
||||||
|
|
||||||
|
if (!pId || pId.trim() === '' || pId === ' ') {
|
||||||
|
allValid = false;
|
||||||
|
window.showToast?.('{{ __("Please select product for item :num") }}'.replace(':num', index + 1), 'error');
|
||||||
|
} else if (!qty || parseInt(qty) < 1) {
|
||||||
|
allValid = false;
|
||||||
|
window.showToast?.('{{ __("Please enter valid quantity for item :num") }}'.replace(':num', index + 1), 'error');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hasError) {
|
if (!allValid) return;
|
||||||
window.showToast?.('{{ __("Please select product and valid quantity for all items") }}', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
console.log('Validation passed, submitting to:', form.action);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData(form);
|
|
||||||
const response = await fetch(form.action, {
|
const response = await fetch(form.action, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
|
||||||
headers: {
|
headers: {
|
||||||
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json'
|
||||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
},
|
||||||
}
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
console.log('Submit result:', result);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
window.showToast?.(result.message, 'success');
|
|
||||||
this.showStockInModal = false;
|
this.showStockInModal = false;
|
||||||
this.fetchTabData('stock-in');
|
|
||||||
|
// 跳轉至進貨單管理頁面 (Inbound Tab) - 恢復整頁刷新
|
||||||
|
window.location.href = '{{ route('admin.warehouses.inventory') }}?tab=stock-in';
|
||||||
} else {
|
} else {
|
||||||
window.showToast?.(result.message || '{{ __("Validation failed") }}', 'error');
|
const firstError = result.errors ? Object.values(result.errors).flat()[0] : result.message;
|
||||||
|
window.showToast?.(firstError || '{{ __("Validation failed") }}', 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error('Submit error:', error);
|
||||||
window.showToast?.('{{ __("System Error") }}', 'error');
|
window.showToast?.('{{ __("Operation failed") }}', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
@ -195,35 +223,42 @@
|
|||||||
this.showConfirmModal = true;
|
this.showConfirmModal = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
async doConfirmStockIn() {
|
doConfirmStockIn() {
|
||||||
if (!this.pendingForm) return;
|
if (!this.pendingForm) return;
|
||||||
|
|
||||||
const form = this.pendingForm;
|
|
||||||
this.showConfirmModal = false;
|
this.showConfirmModal = false;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
this.pendingForm.submit();
|
||||||
|
},
|
||||||
|
|
||||||
try {
|
confirmDelete(url) {
|
||||||
const response = await fetch(form.action, {
|
this.deleteUrl = url;
|
||||||
method: 'POST',
|
this.showDeleteModal = true;
|
||||||
headers: {
|
},
|
||||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
doDeleteStockIn() {
|
||||||
'Accept': 'application/json'
|
if (!this.deleteUrl) return;
|
||||||
},
|
this.showDeleteModal = false;
|
||||||
body: new FormData(form)
|
this.loading = true;
|
||||||
});
|
|
||||||
const data = await response.json();
|
// 建立臨時表單以執行 DELETE 請求(整頁刷新)
|
||||||
if (data.success || response.ok) {
|
const form = document.createElement('form');
|
||||||
window.showToast?.('{{ __("Confirmed") }}', 'success');
|
form.method = 'POST';
|
||||||
this.fetchTabData('stock-in');
|
form.action = this.deleteUrl;
|
||||||
}
|
|
||||||
} catch (e) {
|
const csrf = document.createElement('input');
|
||||||
console.error(e);
|
csrf.type = 'hidden';
|
||||||
window.showToast?.('{{ __("Operation failed") }}', 'error');
|
csrf.name = '_token';
|
||||||
} finally {
|
csrf.value = document.querySelector('meta[name="csrf-token"]').content;
|
||||||
this.loading = false;
|
|
||||||
this.pendingForm = null;
|
const method = document.createElement('input');
|
||||||
}
|
method.type = 'hidden';
|
||||||
|
method.name = '_method';
|
||||||
|
method.value = 'DELETE';
|
||||||
|
|
||||||
|
form.appendChild(csrf);
|
||||||
|
form.appendChild(method);
|
||||||
|
document.body.appendChild(form);
|
||||||
|
form.submit();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -431,33 +466,50 @@
|
|||||||
|
|
||||||
<!-- Products Section -->
|
<!-- Products Section -->
|
||||||
<div class="space-y-4 pt-4">
|
<div class="space-y-4 pt-4">
|
||||||
<div class="flex items-center justify-between px-1">
|
<div class="flex items-center justify-between pl-1">
|
||||||
<h4 class="text-sm font-black text-slate-800 dark:text-white uppercase tracking-widest">{{ __('Product List') }}</h4>
|
<label class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em]">
|
||||||
<button type="button" @click="addItem()" class="text-xs font-black text-cyan-600 hover:text-cyan-700 flex items-center gap-1 transition-colors uppercase tracking-widest">
|
{{ __('Product List') }} <span class="text-rose-500">*</span>
|
||||||
<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>
|
</label>
|
||||||
|
<button type="button" @click="addItem()"
|
||||||
|
class="text-xs font-black text-cyan-500 hover:text-cyan-400 uppercase tracking-widest flex items-center gap-1.5 transition-colors">
|
||||||
|
<svg class="w-4 h-4 stroke-[3]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||||
|
</svg>
|
||||||
{{ __('Add Product') }}
|
{{ __('Add Product') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<template x-for="(item, index) in items" :key="index">
|
<template x-for="(item, index) in items" :key="index">
|
||||||
<div class="flex items-end gap-4 p-4 bg-slate-50 dark:bg-slate-800/50 rounded-2xl border border-slate-100 dark:border-slate-800 transition-all">
|
<div class="group flex items-center gap-3 p-3 bg-slate-50/50 dark:bg-slate-900/30 rounded-2xl border border-slate-100 dark:border-slate-800/50 hover:border-cyan-500/30 transition-all duration-300">
|
||||||
<div class="flex-1 space-y-2">
|
<div class="flex-1 min-w-[200px]">
|
||||||
<label class="block text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Product') }}</label>
|
|
||||||
<x-searchable-select
|
<x-searchable-select
|
||||||
x-bind:name="'items['+index+'][product_id]'"
|
x-bind:name="'items['+index+'][product_id]'"
|
||||||
:options="$stock_in_products ?? []"
|
:options="$stock_in_products ?? []"
|
||||||
placeholder="{{ __('Select Product') }}"
|
placeholder="{{ __('Select Product') }}"
|
||||||
|
x-on:change="item.product_id = $event.target.value"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-32 space-y-2">
|
<div class="w-px h-6 bg-slate-200 dark:bg-slate-800/50"></div>
|
||||||
<label class="block text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Quantity') }}</label>
|
<div class="flex items-center gap-1 bg-slate-100/50 dark:bg-slate-900/50 p-1 rounded-xl border border-slate-200/50 dark:border-slate-800/50">
|
||||||
<input type="number" x-model="item.quantity" x-bind:name="'items['+index+'][quantity]'" min="1"
|
<button type="button" @click="item.quantity > 1 ? item.quantity-- : null"
|
||||||
class="luxury-input py-2.5">
|
class="p-2 text-slate-400 hover:text-cyan-500 transition-colors">
|
||||||
|
<svg class="w-3.5 h-3.5 stroke-[3]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14" /></svg>
|
||||||
|
</button>
|
||||||
|
<input type="number" :name="'items['+index+'][quantity]'" x-model.number="item.quantity"
|
||||||
|
min="1" required class="w-12 bg-transparent border-none p-0 text-center font-mono font-bold text-slate-800 dark:text-slate-200 focus:ring-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
|
placeholder="0">
|
||||||
|
<button type="button" @click="item.quantity++"
|
||||||
|
class="p-2 text-slate-400 hover:text-cyan-500 transition-colors">
|
||||||
|
<svg class="w-3.5 h-3.5 stroke-[3]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" @click="removeItem(index)"
|
<button type="button" @click="removeItem(index)"
|
||||||
class="p-2.5 mb-0.5 rounded-xl text-slate-400 hover:text-rose-500 hover:bg-rose-500/10 transition-all">
|
class="p-2 text-slate-300 hover:text-rose-500 transition-all transform hover:scale-110"
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
|
x-show="items.length > 1">
|
||||||
|
<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 18 18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -490,13 +542,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<x-confirm-modal
|
<x-confirm-modal
|
||||||
alpineVar="showConfirmModal"
|
alpine-var="showConfirmModal"
|
||||||
confirmAction="doConfirmStockIn()"
|
confirm-action="doConfirmStockIn()"
|
||||||
iconType="info"
|
icon-type="info"
|
||||||
confirmColor="emerald"
|
confirm-color="emerald"
|
||||||
:title="__('Confirm Stock-in Order')"
|
:title="__('Confirm Stock-in Order')"
|
||||||
:message="__('Are you sure you want to confirm this stock-in order? This action will update your inventory levels.')"
|
:message="__('Are you sure you want to confirm this stock-in order? This action will update your inventory levels.')"
|
||||||
:confirmText="__('Confirm')"
|
:confirm-text="__('Confirm Stock-In')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Order Details Slide-over (Rewritten to match index.blade.php structure) -->
|
<!-- Order Details Slide-over (Rewritten to match index.blade.php structure) -->
|
||||||
@ -633,5 +685,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<x-confirm-modal
|
||||||
|
alpine-var="showDeleteModal"
|
||||||
|
confirm-action="doDeleteStockIn()"
|
||||||
|
icon-type="danger"
|
||||||
|
confirm-color="rose"
|
||||||
|
:title="__('Confirm Deletion')"
|
||||||
|
:message="__('Are you sure you want to delete this draft order? This action cannot be undone.')"
|
||||||
|
:confirm-text="__('Delete Permanently')"
|
||||||
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
@ -1,139 +1,421 @@
|
|||||||
@extends('layouts.admin')
|
@extends('layouts.admin')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="space-y-3 pb-20" x-data="{
|
<div class="space-y-4 pb-20" x-data="{
|
||||||
showSlotDrawer: false,
|
viewMode: '{{ request('machine_id') ? 'detail' : 'list' }}',
|
||||||
|
displayMode: 'table', {{-- grid or table --}}
|
||||||
selectedMachine: null,
|
selectedMachine: null,
|
||||||
slots: [],
|
slots: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const machineId = urlParams.get('machine_id');
|
||||||
|
if (machineId) {
|
||||||
|
await this.viewSlots({id: machineId});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async viewSlots(machine) {
|
async viewSlots(machine) {
|
||||||
this.selectedMachine = machine;
|
this.selectedMachine = machine;
|
||||||
this.showSlotDrawer = true;
|
this.viewMode = 'detail';
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
this.slots = [];
|
||||||
|
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.searchParams.set('machine_id', machine.id);
|
||||||
|
window.history.pushState({}, '', url);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/admin/warehouses/machine-inventory/${machine.id}/slots`);
|
const res = await fetch(`/admin/warehouses/machine-inventory/${machine.id}/slots`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
this.slots = data.slots || [];
|
if (data.success) {
|
||||||
} catch(e) { this.slots = []; }
|
this.slots = data.slots || [];
|
||||||
this.loading = false;
|
this.selectedMachine = data.machine;
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error('Fetch error:', e);
|
||||||
|
this.slots = [];
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
backToList() {
|
||||||
|
this.viewMode = 'list';
|
||||||
|
this.selectedMachine = null;
|
||||||
|
this.slots = [];
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.searchParams.delete('machine_id');
|
||||||
|
window.history.pushState({}, '', url);
|
||||||
|
},
|
||||||
|
|
||||||
|
get activeSlotsCount() { return this.slots.filter(s => s.product_id != null).length; },
|
||||||
|
get emptySlotsCount() { return this.slots.filter(s => s.product_id == null).length; },
|
||||||
|
get highStockSlotsCount() {
|
||||||
|
return this.slots.filter(s => s.product_id != null && (s.stock / (s.max_stock || 1)) >= 0.5).length;
|
||||||
|
},
|
||||||
|
get totalStock() { return this.slots.reduce((acc, s) => acc + (parseInt(s.stock) || 0), 0); },
|
||||||
|
get totalCapacity() { return this.slots.reduce((acc, s) => acc + (parseInt(s.max_stock) || 10), 0); },
|
||||||
|
get overallFillRate() {
|
||||||
|
if (this.totalCapacity === 0) return 0;
|
||||||
|
return Math.round((this.totalStock / this.totalCapacity) * 100);
|
||||||
|
},
|
||||||
|
|
||||||
|
getSlotColorClass(slot) {
|
||||||
|
if (!slot.expiry_date) return 'bg-slate-50/50 dark:bg-slate-800/50 text-slate-400 border-slate-200/60 dark:border-slate-700/50';
|
||||||
|
const todayStr = new Date().toISOString().split('T')[0];
|
||||||
|
const expiryStr = slot.expiry_date;
|
||||||
|
if (expiryStr < todayStr) return 'bg-rose-50/60 dark:bg-rose-500/10 text-rose-600 dark:text-rose-400 border-rose-200 dark:border-rose-500/30';
|
||||||
|
const diffDays = Math.round((new Date(expiryStr) - new Date(todayStr)) / 86400000);
|
||||||
|
if (diffDays <= 7) return 'bg-amber-50/60 dark:bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-200 dark:border-amber-500/30';
|
||||||
|
return 'bg-emerald-50/60 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-200 dark:border-emerald-500/30';
|
||||||
}
|
}
|
||||||
}">
|
}">
|
||||||
{{-- Header --}}
|
{{-- Header --}}
|
||||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
<div class="flex items-center gap-4">
|
||||||
|
<template x-if="viewMode === 'detail'">
|
||||||
|
<button @click="backToList()"
|
||||||
|
class="p-2.5 rounded-xl bg-white dark:bg-slate-900 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-all border border-slate-200/50 dark:border-slate-700/50 shadow-sm hover:shadow-md active:scale-95">
|
||||||
|
<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="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Machine Inventory Overview') }}</h1>
|
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Machine Inventory Overview') }}</h1>
|
||||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Real-time slot-level inventory across all machines') }}</p>
|
<p class="text-base font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Real-time slot-level inventory across all machines') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Table --}}
|
{{-- List Mode --}}
|
||||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
<div x-show="viewMode === 'list'" class="space-y-6 animate-luxury-in" x-cloak>
|
||||||
<form action="{{ route('admin.warehouses.machine-inventory') }}" method="GET" class="mb-8">
|
<div class="luxury-card rounded-3xl p-8">
|
||||||
<div class="relative group">
|
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
|
||||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10"><svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg></span>
|
<form action="{{ route('admin.warehouses.machine-inventory') }}" method="GET" class="flex items-center gap-4">
|
||||||
<input type="text" name="search" value="{{ request('search') }}" class="py-2.5 pl-12 pr-6 block w-64 luxury-input" placeholder="{{ __('Search machines...') }}">
|
<div class="relative group">
|
||||||
</div>
|
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||||
</form>
|
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
<div class="overflow-x-auto">
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
<table class="w-full text-left border-separate border-spacing-y-0">
|
</svg>
|
||||||
<thead>
|
</span>
|
||||||
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
|
<input type="text" name="search" value="{{ request('search') }}" class="py-2.5 pl-12 pr-6 block w-72 luxury-input" placeholder="{{ __('Search machines...') }}">
|
||||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 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 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Serial No.') }}</th>
|
|
||||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Slots') }}</th>
|
|
||||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Total Stock') }}</th>
|
|
||||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Fill Rate') }}</th>
|
|
||||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 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-50 dark:divide-slate-800/80">
|
|
||||||
@forelse($machines as $machine)
|
|
||||||
@php
|
|
||||||
$maxStock = $machine->slots->sum('max_stock') ?: 1;
|
|
||||||
$currentStock = (int)($machine->total_stock ?? 0);
|
|
||||||
$fillRate = round(($currentStock / $maxStock) * 100);
|
|
||||||
$fillColor = $fillRate >= 50 ? 'emerald' : ($fillRate >= 20 ? 'amber' : 'rose');
|
|
||||||
@endphp
|
|
||||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
|
||||||
<td class="px-6 py-5">
|
|
||||||
<span class="font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ $machine->name }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-5">
|
|
||||||
<span class="font-mono font-bold text-slate-500">{{ $machine->serial_no }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-5 text-center">
|
|
||||||
<span class="font-black text-slate-800 dark:text-white">{{ $machine->total_slots ?? 0 }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-5 text-center">
|
|
||||||
<span class="font-black text-slate-800 dark:text-white">{{ $currentStock }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-5 text-center">
|
|
||||||
<div class="flex items-center justify-center gap-3">
|
|
||||||
<div class="w-24 h-2 bg-slate-100 dark:bg-slate-800 rounded-full overflow-hidden">
|
|
||||||
<div class="h-full bg-{{ $fillColor }}-500 rounded-full transition-all" style="width: {{ $fillRate }}%"></div>
|
|
||||||
</div>
|
|
||||||
<span class="text-xs font-black text-{{ $fillColor }}-500">{{ $fillRate }}%</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-5 text-right">
|
|
||||||
<button @click="viewSlots({{ json_encode($machine->only(['id', 'name', 'serial_no'])) }})"
|
|
||||||
class="px-4 py-2 rounded-xl text-xs font-black bg-cyan-500/10 text-cyan-500 border border-cyan-500/20 hover:bg-cyan-500 hover:text-white transition-all">
|
|
||||||
{{ __('View Slots') }}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
@empty
|
|
||||||
<tr><td colspan="6" class="px-6 py-20 text-center text-slate-400 font-bold">{{ __('No machines found') }}</td></tr>
|
|
||||||
@endforelse
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">{{ $machines->links('vendor.pagination.luxury') }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Slot Drawer --}}
|
|
||||||
<div x-show="showSlotDrawer" class="fixed inset-0 z-[100] overflow-hidden" x-cloak>
|
|
||||||
<div x-show="showSlotDrawer" x-transition class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm" @click="showSlotDrawer = false"></div>
|
|
||||||
<div x-show="showSlotDrawer" x-transition:enter="transform transition ease-in-out duration-300" x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0" x-transition:leave="transform transition ease-in-out duration-300" x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full"
|
|
||||||
class="fixed inset-y-0 right-0 w-full sm:max-w-lg luxury-card rounded-l-3xl shadow-2xl overflow-y-auto z-10">
|
|
||||||
<div class="p-8">
|
|
||||||
<div class="flex justify-between items-center mb-8">
|
|
||||||
<div>
|
|
||||||
<h3 class="text-xl font-black text-slate-800 dark:text-white font-display" x-text="selectedMachine?.name"></h3>
|
|
||||||
<p class="text-xs font-mono font-bold text-slate-500 mt-1" x-text="selectedMachine?.serial_no"></p>
|
|
||||||
</div>
|
</div>
|
||||||
<button @click="showSlotDrawer = false" class="text-slate-400 hover:text-slate-600 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 type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group" title="{{ __('Search') }}">
|
||||||
|
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button type="button"
|
||||||
|
@click="
|
||||||
|
$el.closest('form').querySelectorAll('input[type=text]').forEach(i => i.value = '');
|
||||||
|
$el.closest('form').submit();
|
||||||
|
"
|
||||||
|
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group" title="{{ __('Reset') }}">
|
||||||
|
<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="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>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left border-separate border-spacing-y-0">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
|
||||||
|
<th class="px-6 py-4 text-sm font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Machine') }}</th>
|
||||||
|
<th class="px-6 py-4 text-sm font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Serial No.') }}</th>
|
||||||
|
<th class="px-6 py-4 text-sm font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Slots') }}</th>
|
||||||
|
<th class="px-6 py-4 text-sm font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Total Stock') }}</th>
|
||||||
|
<th class="px-6 py-4 text-sm font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Stock Rate') }}</th>
|
||||||
|
<th class="px-6 py-4 text-sm font-bold text-slate-500 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-50 dark:divide-slate-800/80">
|
||||||
|
@forelse($machines as $machine)
|
||||||
|
@php
|
||||||
|
$maxStock = $machine->slots->sum('max_stock') ?: 1;
|
||||||
|
$currentStock = (int)($machine->total_stock ?? 0);
|
||||||
|
$fillRate = round(($currentStock / $maxStock) * 100);
|
||||||
|
$fillColor = $fillRate >= 50 ? 'emerald' : ($fillRate >= 20 ? 'amber' : 'rose');
|
||||||
|
@endphp
|
||||||
|
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300 cursor-pointer"
|
||||||
|
@click="viewSlots({{ json_encode($machine->only(['id', 'name', 'serial_no'])) }})">
|
||||||
|
<td class="px-6 py-5">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 group-hover:bg-cyan-500 group-hover:text-white transition-all overflow-hidden shadow-sm border border-slate-200 dark:border-slate-700">
|
||||||
|
@if($machine->image_urls && isset($machine->image_urls[0]))
|
||||||
|
<img src="{{ $machine->image_urls[0] }}" class="w-full h-full object-cover">
|
||||||
|
@else
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<span class="font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ $machine->name }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-5">
|
||||||
|
<span class="font-mono font-bold text-slate-500 uppercase tracking-widest text-sm">{{ $machine->serial_no }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-5 text-center">
|
||||||
|
<span class="font-black text-slate-800 dark:text-white">{{ $machine->total_slots ?? 0 }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-5 text-center">
|
||||||
|
<span class="font-black text-slate-800 dark:text-white">{{ $currentStock }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-5 text-center">
|
||||||
|
<div class="flex items-center justify-center gap-3">
|
||||||
|
<div class="w-24 h-2 bg-slate-100 dark:bg-slate-700/50 rounded-full overflow-hidden shadow-inner">
|
||||||
|
<div class="h-full bg-{{ $fillColor }}-500 rounded-full transition-all duration-500" style="width: {{ $fillRate }}%;"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-black text-{{ $fillColor }}-500 w-8">{{ $fillRate }}%</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-5 text-right">
|
||||||
|
<div class="flex items-center justify-end gap-2">
|
||||||
|
<a href="{{ route('admin.warehouses.replenishments') }}?auto_machine_id={{ $machine->id }}"
|
||||||
|
@click.stop
|
||||||
|
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20"
|
||||||
|
title="{{ __('Quick Replenish') }}">
|
||||||
|
<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.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<button class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20"
|
||||||
|
title="{{ __('View Slots') }}">
|
||||||
|
<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="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr><td colspan="6" class="px-6 py-24 text-center text-slate-400 font-bold italic">{{ __('No machines found') }}</td></tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 pt-6 border-t border-slate-50 dark:border-slate-800/50">{{ $machines->links('vendor.pagination.luxury') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Detail Mode --}}
|
||||||
|
<div x-show="viewMode === 'detail'" class="space-y-8 animate-luxury-in" x-cloak>
|
||||||
|
<!-- Statistics Dashboard -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-stretch">
|
||||||
|
<!-- Left: Overall Progress -->
|
||||||
|
<div class="lg:col-span-5 luxury-card rounded-[2.5rem] p-10 flex items-center justify-center relative overflow-hidden group">
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-br from-cyan-500/5 to-emerald-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-700"></div>
|
||||||
|
<div class="relative flex flex-col items-center">
|
||||||
|
<div class="relative w-48 h-48">
|
||||||
|
<!-- SVG Circle Progress -->
|
||||||
|
<svg class="w-full h-full -rotate-90 transform" viewBox="0 0 100 100">
|
||||||
|
<circle class="text-slate-100 dark:text-slate-700/50 stroke-current" stroke-width="10" fill="transparent" r="40" cx="50" cy="50"></circle>
|
||||||
|
<circle class="text-emerald-500 stroke-current transition-all duration-1000 ease-out" stroke-width="10" stroke-dasharray="251.2" :stroke-dashoffset="251.2 - (251.2 * Math.min(overallFillRate, 100)) / 100" stroke-linecap="round" fill="transparent" r="40" cx="50" cy="50"></circle>
|
||||||
|
</svg>
|
||||||
|
<div class="absolute inset-0 flex flex-col items-center justify-center">
|
||||||
|
<span class="text-5xl font-black text-slate-800 dark:text-white tracking-tighter" x-text="overallFillRate + '%'"></span>
|
||||||
|
<span class="text-sm font-black text-slate-400 uppercase tracking-[0.2em] mt-1" x-text="totalStock + ' / ' + totalCapacity"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 text-center">
|
||||||
|
<span class="text-base font-black text-slate-500 uppercase tracking-[0.3em]">{{ __('總庫存量') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right: Stats Cards -->
|
||||||
|
<div class="lg:col-span-7 grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||||
|
<!-- Machine Info Card -->
|
||||||
|
<div class="sm:col-span-2 luxury-card rounded-[2rem] p-6 border-slate-200/50 dark:border-slate-800/50 flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-6">
|
||||||
|
<div class="w-16 h-16 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 overflow-hidden">
|
||||||
|
<template x-if="selectedMachine?.image_urls && selectedMachine?.image_urls[0]">
|
||||||
|
<img :src="selectedMachine.image_urls[0]" class="w-full h-full object-cover">
|
||||||
|
</template>
|
||||||
|
<template x-if="!selectedMachine?.image_urls || !selectedMachine?.image_urls[0]">
|
||||||
|
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 x-text="selectedMachine?.name" class="text-2xl font-black text-slate-800 dark:text-white tracking-tight leading-tight"></h2>
|
||||||
|
<span x-text="selectedMachine?.serial_no" class="text-sm font-mono font-bold text-slate-400 uppercase tracking-widest mt-1 block"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hidden sm:flex items-center gap-4">
|
||||||
|
<div class="text-right">
|
||||||
|
<span class="text-sm font-black text-slate-400 uppercase tracking-widest block mb-1">{{ __('Location') }}</span>
|
||||||
|
<span x-text="selectedMachine?.location || '{{ __('No Location') }}'" class="text-base font-bold text-slate-600 dark:text-slate-300"></span>
|
||||||
|
</div>
|
||||||
|
<a :href="'{{ route('admin.warehouses.replenishments') }}?auto_machine_id=' + selectedMachine?.id"
|
||||||
|
class="px-4 py-2.5 rounded-xl text-xs font-black bg-cyan-500/10 text-cyan-500 border border-cyan-500/20 hover:bg-cyan-500 hover:text-white transition-all flex items-center gap-2 uppercase tracking-widest">
|
||||||
|
<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.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" /></svg>
|
||||||
|
{{ __('Quick Replenish') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div x-show="loading" class="py-20 text-center text-slate-400 font-bold">{{ __('Loading...') }}</div>
|
<!-- Active Slots -->
|
||||||
|
<div class="luxury-card rounded-[2rem] p-6 border-emerald-500/10 bg-emerald-500/[0.02]">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<div class="w-10 h-10 rounded-xl bg-emerald-500/10 flex items-center justify-center text-emerald-500">
|
||||||
|
<svg class="w-5 h-5" 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>
|
||||||
|
</div>
|
||||||
|
<span class="text-2xl font-black text-emerald-600 dark:text-emerald-400" x-text="activeSlotsCount"></span>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-black text-slate-500 uppercase tracking-widest">{{ __('Active Slots') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div x-show="!loading" class="space-y-3">
|
<!-- Empty Slots -->
|
||||||
|
<div class="luxury-card rounded-[2rem] p-6 border-slate-500/10 bg-slate-500/[0.02]">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<div class="w-10 h-10 rounded-xl bg-slate-500/10 flex items-center justify-center text-slate-500">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg>
|
||||||
|
</div>
|
||||||
|
<span class="text-2xl font-black text-slate-600 dark:text-slate-400" x-text="emptySlotsCount"></span>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-black text-slate-500 uppercase tracking-widest">{{ __('Empty Slots') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- High Stock -->
|
||||||
|
<div class="luxury-card rounded-[2rem] p-6 border-cyan-500/10 bg-cyan-500/[0.02] sm:col-span-2">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-10 h-10 rounded-xl bg-cyan-500/10 flex items-center justify-center text-cyan-500">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" /></svg>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-black text-slate-500 uppercase tracking-widest">{{ __('Stock Level > 50%') }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-2xl font-black text-cyan-600 dark:text-cyan-400" x-text="highStockSlotsCount"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cabinet Control & Display Toggle -->
|
||||||
|
<div class="flex items-center justify-between px-4">
|
||||||
|
<div class="flex items-center gap-6">
|
||||||
|
<button @click="displayMode = 'table'"
|
||||||
|
:class="displayMode === 'table' ? 'text-cyan-500 bg-cyan-500/10' : 'text-slate-400 hover:text-slate-600'"
|
||||||
|
class="flex items-center gap-2 px-4 py-2 rounded-xl transition-all font-black text-sm uppercase tracking-widest">
|
||||||
|
<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="M4 6h16M4 10h16M4 14h16M4 18h16" /></svg>
|
||||||
|
{{ __('Table View') }}
|
||||||
|
</button>
|
||||||
|
<button @click="displayMode = 'grid'"
|
||||||
|
:class="displayMode === 'grid' ? 'text-cyan-500 bg-cyan-500/10' : 'text-slate-400 hover:text-slate-600'"
|
||||||
|
class="flex items-center gap-2 px-4 py-2 rounded-xl transition-all font-black text-sm uppercase tracking-widest">
|
||||||
|
<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="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" /></svg>
|
||||||
|
{{ __('Grid View') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="w-2.5 h-2.5 rounded-full bg-rose-500"></span>
|
||||||
|
<span class="text-sm font-black text-slate-400 uppercase tracking-widest">{{ __('Expired') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="w-2.5 h-2.5 rounded-full bg-amber-500"></span>
|
||||||
|
<span class="text-sm font-black text-slate-400 uppercase tracking-widest">{{ __('Low Stock') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grid View -->
|
||||||
|
<div x-show="displayMode === 'grid'" class="space-y-6">
|
||||||
|
<div class="luxury-card rounded-[2.5rem] p-8 border-slate-200/50 dark:border-slate-800/50 bg-white/30 dark:bg-slate-900/40 backdrop-blur-xl relative overflow-hidden min-h-[400px]">
|
||||||
|
<div x-show="loading" class="absolute inset-0 bg-white/60 dark:bg-slate-900/60 backdrop-blur-md z-20 flex items-center justify-center transition-all duration-500">
|
||||||
|
<div class="w-12 h-12 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-6 relative z-10" x-show="!loading">
|
||||||
<template x-for="slot in slots" :key="slot.id">
|
<template x-for="slot in slots" :key="slot.id">
|
||||||
<div class="p-4 bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-100 dark:border-slate-800">
|
<div :class="getSlotColorClass(slot)"
|
||||||
<div class="flex items-center justify-between">
|
class="min-h-[260px] rounded-[2.5rem] p-5 flex flex-col items-center justify-center border-2 transition-all duration-500 group relative hover:scale-105 hover:-translate-y-1 hover:shadow-xl">
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="w-8 h-8 rounded-lg bg-slate-200 dark:bg-slate-800 flex items-center justify-center text-xs font-black text-slate-500" x-text="slot.slot_no"></div>
|
<div class="absolute top-4 left-5 px-3 py-1 rounded-xl bg-slate-900/5 dark:bg-white/10 backdrop-blur-md">
|
||||||
<div>
|
<span class="text-sm font-black tracking-tighter text-slate-800 dark:text-white" x-text="slot.slot_no"></span>
|
||||||
<p class="font-extrabold text-slate-800 dark:text-slate-100 text-sm" x-text="slot.product?.name || '{{ __('Empty') }}'"></p>
|
</div>
|
||||||
<p class="text-xs font-bold text-slate-400 mt-0.5" x-show="slot.expiry_date" x-text="'{{ __('Expiry') }}: ' + (slot.expiry_date || '')"></p>
|
|
||||||
</div>
|
<div class="relative w-16 h-16 mb-4 mt-2">
|
||||||
|
<div class="absolute inset-0 rounded-2xl bg-white/20 dark:bg-slate-900/40 backdrop-blur-xl border border-white/30 overflow-hidden shadow-inner">
|
||||||
|
<template x-if="slot.product && slot.product.image_url">
|
||||||
|
<img :src="slot.product.image_url" class="w-full h-full object-cover">
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<span class="text-lg font-black" :class="slot.stock <= 0 ? 'text-rose-500' : (slot.stock <= (slot.max_stock * 0.2) ? 'text-amber-500' : 'text-emerald-500')" x-text="slot.stock"></span>
|
<div class="text-center w-full">
|
||||||
<span class="text-xs font-bold text-slate-400">/ <span x-text="slot.max_stock"></span></span>
|
<div class="text-base font-black truncate w-full opacity-90 tracking-tight" x-text="slot.product?.name || '{{ __('Empty') }}'"></div>
|
||||||
</div>
|
<div class="flex items-baseline justify-center gap-1 mt-2">
|
||||||
|
<span class="text-2xl font-black tracking-tighter" x-text="slot.stock"></span>
|
||||||
|
<span class="text-sm font-black opacity-30">/</span>
|
||||||
|
<span class="text-sm font-bold opacity-50" x-text="slot.max_stock"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-sm font-black tracking-widest opacity-40 uppercase mt-2" x-text="slot.expiry_date || '----/--/--'"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div x-show="slots.length === 0" class="py-10 text-center text-slate-400 font-bold">{{ __('No slot data') }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Table View -->
|
||||||
|
<div x-show="displayMode === 'table'" class="animate-luxury-in" x-cloak>
|
||||||
|
<div class="luxury-card rounded-[2.5rem] overflow-hidden border-slate-200/50 dark:border-slate-800/50 shadow-xl">
|
||||||
|
<table class="w-full text-left">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-slate-50 dark:bg-slate-900/50">
|
||||||
|
<th class="px-8 py-5 text-sm font-black text-slate-400 uppercase tracking-[0.2em]">{{ __('Slot') }}</th>
|
||||||
|
<th class="px-8 py-5 text-sm font-black text-slate-400 uppercase tracking-[0.2em]">{{ __('Product') }}</th>
|
||||||
|
<th class="px-8 py-5 text-sm font-black text-slate-400 uppercase tracking-[0.2em]">{{ __('Barcode') }}</th>
|
||||||
|
<th class="px-8 py-5 text-sm font-black text-slate-400 uppercase tracking-[0.2em] text-center">{{ __('Stock Level') }} / {{ __('Max Stock') }}</th>
|
||||||
|
<th class="px-8 py-5 text-sm font-black text-slate-400 uppercase tracking-[0.2em] text-right">{{ __('Sale Price') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/40">
|
||||||
|
<template x-for="slot in slots" :key="slot.id">
|
||||||
|
<tr class="hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition-colors">
|
||||||
|
<td class="px-8 py-6">
|
||||||
|
<span class="px-3 py-1.5 rounded-lg bg-slate-100 dark:bg-slate-800 text-sm font-black text-slate-600 dark:text-slate-300" x-text="slot.slot_no"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-8 py-6">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-12 h-12 rounded-xl bg-slate-50 dark:bg-slate-900 border border-slate-100 dark:border-slate-800 flex items-center justify-center overflow-hidden">
|
||||||
|
<template x-if="slot.product?.image_url">
|
||||||
|
<img :src="slot.product.image_url" class="w-full h-full object-cover">
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-base font-black text-slate-800 dark:text-slate-200" x-text="slot.product?.name || '{{ __('Empty Slot') }}'"></div>
|
||||||
|
<div class="text-sm font-bold text-slate-400 uppercase tracking-widest mt-1" x-text="slot.expiry_date ? '{{ __('Expiry') }}: ' + slot.expiry_date : ''"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-8 py-6">
|
||||||
|
<span class="text-sm font-mono font-bold text-slate-400" x-text="slot.product?.barcode || '--'"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-8 py-6 text-center">
|
||||||
|
<div class="flex items-center justify-center gap-2">
|
||||||
|
<span class="text-lg font-black" :class="slot.stock <= 2 ? 'text-rose-500' : 'text-slate-700 dark:text-slate-200'" x-text="slot.stock"></span>
|
||||||
|
<span class="text-xs font-bold text-slate-300">/</span>
|
||||||
|
<span class="text-sm font-bold text-slate-400" x-text="slot.max_stock"></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-8 py-6 text-right">
|
||||||
|
<span class="text-base font-black text-emerald-500" x-text="'$' + (slot.price || 0)"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
{{-- 指派人員 Modal --}}
|
||||||
|
<div x-show="showAssignModal" class="fixed inset-0 z-[200] overflow-y-auto" x-cloak>
|
||||||
|
<div class="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<div x-show="showAssignModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200" class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm"></div>
|
||||||
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
||||||
|
<div x-show="showAssignModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" x-transition:leave="ease-in duration-200" class="inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white dark:bg-slate-900 rounded-3xl shadow-2xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-8 border border-slate-100 dark:border-white/10">
|
||||||
|
<div class="mb-8">
|
||||||
|
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight uppercase">{{ __('Assign Personnel') }}</h3>
|
||||||
|
<p class="text-sm font-bold text-slate-500 mt-2">{{ __('Select a team member to handle this replenishment order') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 mb-8">
|
||||||
|
<label class="block text-xs font-black text-slate-500 uppercase tracking-[0.15em] pl-1">{{ __('Select Personnel') }}</label>
|
||||||
|
<select x-model="assignUserId" class="luxury-input w-full px-6 py-3">
|
||||||
|
<option value="">{{ __('Select Personnel') }}</option>
|
||||||
|
@foreach($users as $u)
|
||||||
|
<option value="{{ $u->id }}">{{ $u->name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<button type="button" @click="showAssignModal = false" class="btn-luxury-ghost px-6">{{ __('Cancel') }}</button>
|
||||||
|
<button type="button" @click="executeAssign()" :disabled="loading || !assignUserId" class="btn-luxury-primary px-8">{{ __('Confirm') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,205 @@
|
|||||||
|
{{-- 一鍵補貨 Modal --}}
|
||||||
|
<div x-show="showAutoModal" class="fixed inset-0 z-[110] overflow-y-auto" style="display: none;" x-cloak>
|
||||||
|
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<div x-show="showAutoModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200" class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm" @click="showAutoModal = false"></div>
|
||||||
|
<span class="hidden sm:inline-block sm:align-middle sm:min-h-screen">​</span>
|
||||||
|
<div x-show="showAutoModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" x-transition:leave="ease-in duration-200" class="relative inline-flex flex-col align-bottom bg-white dark:bg-slate-900 rounded-[2.5rem] text-left shadow-2xl transform sm:my-8 sm:align-middle sm:max-w-3xl w-full border border-slate-100 dark:border-slate-800 z-10 max-h-[90vh]" @click.stop>
|
||||||
|
|
||||||
|
{{-- Header --}}
|
||||||
|
<div class="px-10 py-8 pb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight leading-none mb-3">
|
||||||
|
<svg class="w-6 h-6 inline-block text-cyan-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" /></svg>
|
||||||
|
{{ __('Auto Replenishment') }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest">{{ __('Automatically calculate replenishment needs based on machine capacity') }}</p>
|
||||||
|
</div>
|
||||||
|
<button @click="showAutoModal = false" class="p-2.5 rounded-full bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-slate-600 transition-all border border-slate-100 dark:border-slate-700 shadow-sm">
|
||||||
|
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-y-auto px-10 py-2 custom-scrollbar">
|
||||||
|
{{-- Step 1: 選擇倉庫與機台 --}}
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<label class="block text-xs font-black text-slate-500 uppercase tracking-[0.15em] pl-1">{{ __('Source Warehouse') }} <span class="text-rose-500">*</span></label>
|
||||||
|
<x-searchable-select name="auto_warehouse_id" :options="$warehouses ?? []" :placeholder="__('Select Warehouse')" class="w-full" @change="autoWarehouseId = $event.target.value; if(autoPreviewLoaded) loadAutoPreview()" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<label class="block text-xs font-black text-slate-500 uppercase tracking-[0.15em] pl-1">{{ __('Target Machine') }} <span class="text-rose-500">*</span></label>
|
||||||
|
<x-searchable-select name="auto_machine_id" :options="$machines->map(fn($m) => (object)['id' => $m->id, 'name' => $m->name . ' (' . $m->serial_no . ')'])" :placeholder="__('Select Machine')" class="w-full" @change="autoMachineId = $event.target.value; if(autoPreviewLoaded) loadAutoPreview()" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="block text-xs font-black text-slate-500 uppercase tracking-[0.15em] pl-1 mb-3">{{ __('Note') }}</label>
|
||||||
|
<input type="text" x-model="autoNote" class="luxury-input w-full px-6 py-3 bg-slate-50/50 dark:bg-slate-900/50" placeholder="{{ __('Optional remarks...') }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 載入預覽按鈕 --}}
|
||||||
|
<div x-show="!autoPreviewLoaded" class="text-center py-6">
|
||||||
|
<button type="button" @click="loadAutoPreview()" :disabled="autoLoading || !autoWarehouseId || !autoMachineId"
|
||||||
|
class="btn-luxury-primary px-10 py-3 relative">
|
||||||
|
<span :class="autoLoading ? 'opacity-0' : ''">{{ __('Calculate Replenishment') }}</span>
|
||||||
|
<template x-if="autoLoading"><div class="absolute inset-0 flex items-center justify-center"><div class="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div></div></template>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 庫存不足警告 --}}
|
||||||
|
<template x-if="autoPreviewLoaded && autoHasWarning">
|
||||||
|
<div class="mb-6 p-4 bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20 rounded-2xl">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<svg class="w-5 h-5 text-amber-500 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" /></svg>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-black text-amber-700 dark:text-amber-400">{{ __('Warehouse stock insufficient warning') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
{{-- 補貨清單預覽 --}}
|
||||||
|
<template x-if="autoPreviewLoaded && autoSlots.length > 0">
|
||||||
|
<div class="space-y-4 pb-6">
|
||||||
|
<div class="flex items-center justify-between px-1">
|
||||||
|
<h4 class="text-sm font-black text-slate-800 dark:text-white uppercase tracking-widest">{{ __('Slots need replenishment') }}</h4>
|
||||||
|
<span class="text-[10px] font-black text-cyan-500 bg-cyan-500/10 px-2 py-0.5 rounded-md uppercase" x-text="autoSlots.length + ' {{ __("Items") }}'"></span>
|
||||||
|
</div>
|
||||||
|
{{-- 桌面版:Table 佈局 --}}
|
||||||
|
<div class="hidden md:block overflow-x-auto">
|
||||||
|
<table class="w-full text-left border-separate border-spacing-y-0">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
|
||||||
|
<th class="px-4 py-3 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">{{ __('Slot') }}</th>
|
||||||
|
<th class="px-4 py-3 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">{{ __('Product') }}</th>
|
||||||
|
<th class="px-4 py-3 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Stock / Capacity') }}</th>
|
||||||
|
<th class="px-4 py-3 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Fill Quantity') }}</th>
|
||||||
|
<th class="px-4 py-3 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Warehouse Stock') }}</th>
|
||||||
|
<th class="w-10 border-b border-slate-100 dark:border-slate-800"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template x-for="(slot, idx) in autoSlots" :key="slot.slot_no">
|
||||||
|
<tr class="hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all border-b border-slate-50 dark:border-slate-800/30 last:border-0">
|
||||||
|
<td class="px-4 py-2.5 font-mono font-bold text-slate-700 dark:text-slate-300" x-text="slot.slot_no"></td>
|
||||||
|
<td class="px-4 py-2.5">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<template x-if="slot.image_url"><img :src="slot.image_url" class="w-8 h-8 rounded-lg object-cover border border-slate-100 dark:border-slate-800"></template>
|
||||||
|
<span class="text-sm font-bold text-slate-700 dark:text-slate-200 truncate max-w-[150px]" x-text="slot.product_name"></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2.5 text-center font-mono font-bold text-slate-500 whitespace-nowrap">
|
||||||
|
<span class="text-slate-700 dark:text-slate-300" x-text="slot.current_stock"></span>
|
||||||
|
<span class="text-slate-300 dark:text-slate-600 mx-0.5">/</span>
|
||||||
|
<span class="text-slate-400" x-text="slot.max_stock"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2.5 text-center flex justify-center">
|
||||||
|
<div class="flex items-center gap-1 bg-slate-100/50 dark:bg-slate-900/50 p-1 rounded-xl border border-slate-200/50 dark:border-slate-800/50 w-fit">
|
||||||
|
<button type="button" @click="slot.quantity > 1 ? slot.quantity-- : null" class="p-1 text-slate-400 hover:text-cyan-500 transition-colors">
|
||||||
|
<svg class="w-2.5 h-2.5 stroke-[3]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14" /></svg>
|
||||||
|
</button>
|
||||||
|
<input type="number" x-model.number="slot.quantity" min="1" :max="slot.max_stock - slot.current_stock" class="w-8 bg-transparent border-none p-0 text-center font-mono font-bold text-slate-800 dark:text-slate-200 focus:ring-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none text-xs" placeholder="0">
|
||||||
|
<button type="button" @click="slot.quantity < (slot.max_stock - slot.current_stock) ? slot.quantity++ : null" class="p-1 text-slate-400 hover:text-cyan-500 transition-colors">
|
||||||
|
<svg class="w-2.5 h-2.5 stroke-[3]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2.5 text-center">
|
||||||
|
<div class="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] font-black tracking-tight"
|
||||||
|
:class="getActualFillable(idx) < slot.quantity ? 'bg-rose-500/10 text-rose-500' : 'bg-emerald-500/10 text-emerald-500'">
|
||||||
|
<span x-text="getActualFillable(idx)"></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-2 py-2.5 text-center">
|
||||||
|
<button type="button" @click="removeAutoSlot(idx)" class="p-1.5 rounded-lg text-slate-400 hover:text-rose-500 hover:bg-rose-500/10 transition-all" title="{{ __('Remove') }}">
|
||||||
|
<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 18L18 6M6 6l12 12" /></svg>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 手機版:Card 佈局 --}}
|
||||||
|
<div class="md:hidden space-y-3">
|
||||||
|
<template x-for="(slot, idx) in autoSlots" :key="'m' + slot.slot_no">
|
||||||
|
<div class="relative bg-slate-50/50 dark:bg-slate-800/30 rounded-2xl border border-slate-100 dark:border-slate-800/50 p-4 transition-all">
|
||||||
|
{{-- 刪除按鈕 --}}
|
||||||
|
<button type="button" @click="removeAutoSlot(idx)" class="absolute top-3 right-3 p-1.5 rounded-lg text-slate-400 hover:text-rose-500 hover:bg-rose-500/10 transition-all" title="{{ __('Remove') }}">
|
||||||
|
<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 18L18 6M6 6l12 12" /></svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{{-- 上半部:商品資訊 --}}
|
||||||
|
<div class="flex items-center gap-3 mb-3 pr-8">
|
||||||
|
<div class="flex-shrink-0 w-7 h-7 rounded-lg bg-cyan-500/10 flex items-center justify-center">
|
||||||
|
<span class="text-xs font-black text-cyan-500 font-mono" x-text="slot.slot_no"></span>
|
||||||
|
</div>
|
||||||
|
<template x-if="slot.image_url">
|
||||||
|
<img :src="slot.image_url" class="w-10 h-10 rounded-xl object-cover border border-slate-200 dark:border-slate-700 flex-shrink-0">
|
||||||
|
</template>
|
||||||
|
<span class="text-sm font-bold text-slate-700 dark:text-slate-200 truncate" x-text="slot.product_name"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 下半部:數據區 --}}
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
|
{{-- 庫存 / 上限 --}}
|
||||||
|
<div class="text-center bg-white/60 dark:bg-slate-900/40 rounded-xl py-2 px-1">
|
||||||
|
<div class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Stock / Capacity') }}</div>
|
||||||
|
<div class="font-mono font-bold text-sm">
|
||||||
|
<span class="text-slate-700 dark:text-slate-300" x-text="slot.current_stock"></span>
|
||||||
|
<span class="text-slate-300 dark:text-slate-600">/</span>
|
||||||
|
<span class="text-slate-400" x-text="slot.max_stock"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 補貨數量 --}}
|
||||||
|
<div class="text-center bg-white/60 dark:bg-slate-900/40 rounded-xl py-2 px-1">
|
||||||
|
<div class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Fill Quantity') }}</div>
|
||||||
|
<div class="flex items-center justify-center gap-1">
|
||||||
|
<button type="button" @click="slot.quantity > 1 ? slot.quantity-- : null" class="p-0.5 text-slate-400 hover:text-cyan-500 transition-colors">
|
||||||
|
<svg class="w-3 h-3 stroke-[3]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14" /></svg>
|
||||||
|
</button>
|
||||||
|
<input type="number" x-model.number="slot.quantity" min="1" :max="slot.max_stock - slot.current_stock" class="w-8 bg-transparent border-none p-0 text-center font-mono font-bold text-sm text-slate-800 dark:text-slate-200 focus:ring-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" placeholder="0">
|
||||||
|
<button type="button" @click="slot.quantity < (slot.max_stock - slot.current_stock) ? slot.quantity++ : null" class="p-0.5 text-slate-400 hover:text-cyan-500 transition-colors">
|
||||||
|
<svg class="w-3 h-3 stroke-[3]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 倉庫數量 --}}
|
||||||
|
<div class="text-center bg-white/60 dark:bg-slate-900/40 rounded-xl py-2 px-1">
|
||||||
|
<div class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Warehouse Stock') }}</div>
|
||||||
|
<div class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-black tracking-tight"
|
||||||
|
:class="getActualFillable(idx) < slot.quantity ? 'bg-rose-500/10 text-rose-500' : 'bg-emerald-500/10 text-emerald-500'">
|
||||||
|
<span x-text="getActualFillable(idx)"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Footer --}}
|
||||||
|
<div class="px-10 py-6 border-t border-slate-100 dark:border-slate-800/50 flex items-center justify-end gap-3">
|
||||||
|
<button type="button" @click="showAutoModal = false" class="btn-luxury-ghost px-6">{{ __('Cancel') }}</button>
|
||||||
|
<template x-if="autoPreviewLoaded">
|
||||||
|
<button type="button" @click="loadAutoPreview()" :disabled="autoLoading"
|
||||||
|
class="px-6 py-2.5 rounded-xl border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400 font-black text-xs uppercase tracking-widest hover:bg-slate-50 dark:hover:bg-slate-800 transition-all flex items-center gap-2">
|
||||||
|
<svg class="w-3.5 h-3.5" :class="autoLoading ? 'animate-spin' : ''" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" /></svg>
|
||||||
|
<span>{{ __('Recalculate') }}</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template x-if="autoPreviewLoaded && autoSlots.length > 0">
|
||||||
|
<button type="button" @click="submitAutoReplenishment()" :disabled="autoLoading" class="btn-luxury-primary px-12 relative">
|
||||||
|
<span :class="autoLoading ? 'opacity-0' : ''">{{ __('Create') }}</span>
|
||||||
|
<template x-if="autoLoading"><div class="absolute inset-0 flex items-center justify-center"><div class="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div></div></template>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
{{-- 取消確認 Modal --}}
|
||||||
|
<div x-show="showCancelModal" class="fixed inset-0 z-[200] overflow-y-auto" x-cloak>
|
||||||
|
<div class="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<div x-show="showCancelModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200" class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm"></div>
|
||||||
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
||||||
|
<div x-show="showCancelModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" x-transition:leave="ease-in duration-200" class="inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white dark:bg-slate-900 rounded-3xl shadow-2xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-8 border border-slate-100 dark:border-white/10">
|
||||||
|
<div class="sm:flex sm:items-start">
|
||||||
|
<div class="flex items-center justify-center flex-shrink-0 w-14 h-14 mx-auto bg-rose-50 dark:bg-rose-500/10 rounded-2xl sm:mx-0">
|
||||||
|
<svg class="w-8 h-8 text-rose-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" /></svg>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 text-center sm:mt-0 sm:ms-6 sm:text-left">
|
||||||
|
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight uppercase">{{ __('Cancel Order') }}</h3>
|
||||||
|
<div class="mt-3">
|
||||||
|
<p class="text-base font-bold text-slate-500 dark:text-slate-400 leading-relaxed">
|
||||||
|
{{ __('Are you sure you want to cancel this order? If the order has been prepared, stock will be returned to the warehouse.') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-8 sm:mt-10 sm:flex sm:flex-row-reverse gap-3">
|
||||||
|
<button type="button" @click="executeCancel()" :disabled="loading"
|
||||||
|
class="inline-flex justify-center w-full px-6 py-3 text-sm font-black text-white bg-rose-500 rounded-xl hover:bg-rose-600 shadow-lg shadow-rose-200 dark:shadow-none hover:scale-[1.02] active:scale-[0.98] sm:w-auto uppercase tracking-widest transition-all">
|
||||||
|
{{ __('Confirm Cancel') }}
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="showCancelModal = false"
|
||||||
|
class="inline-flex justify-center w-full px-6 py-3 mt-3 text-sm font-black text-slate-700 dark:text-slate-200 bg-slate-100 dark:bg-slate-800 rounded-xl hover:bg-slate-200 dark:hover:bg-slate-700 sm:mt-0 sm:w-auto uppercase tracking-widest transition-all">
|
||||||
|
{{ __('Go Back') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
{{-- 新建補貨單 Modal --}}
|
||||||
|
<div x-show="showReplenishmentModal" class="fixed inset-0 z-[110] overflow-y-auto" style="display: none;" x-cloak>
|
||||||
|
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<div x-show="showReplenishmentModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity" @click="showReplenishmentModal = false"></div>
|
||||||
|
<span class="hidden sm:inline-block sm:align-middle sm:min-h-screen" aria-hidden="true">​</span>
|
||||||
|
<div x-show="showReplenishmentModal" 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="relative inline-flex flex-col align-bottom bg-white dark:bg-slate-900 rounded-[2.5rem] text-left shadow-2xl transform sm:my-8 sm:align-middle sm:max-w-3xl w-full border border-slate-100 dark:border-slate-800 z-10 max-h-[90vh]" @click.stop>
|
||||||
|
<div class="px-10 py-8 pb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight leading-none mb-3">{{ __('New Replenishment') }}</h3>
|
||||||
|
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest">{{ __('Create stock replenishment for specific machines') }}</p>
|
||||||
|
</div>
|
||||||
|
<button @click="showReplenishmentModal = false" class="p-2.5 rounded-full bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-slate-600 transition-all border border-slate-100 dark:border-slate-700 shadow-sm">
|
||||||
|
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 overflow-y-auto px-10 py-2 custom-scrollbar overflow-x-visible">
|
||||||
|
<form id="replenishmentForm" action="{{ route('admin.warehouses.replenishments.store') }}" method="POST" class="space-y-6 pb-20">
|
||||||
|
@csrf
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<label class="block text-xs font-black text-slate-500 uppercase tracking-[0.15em] pl-1">{{ __('Source Warehouse') }} <span class="text-rose-500">*</span></label>
|
||||||
|
<x-searchable-select name="warehouse_id" :options="$warehouses ?? []" :placeholder="__('Select Warehouse')" class="w-full" required x-model="fromId" @change="fromId = $event.target.value; fetchStock()" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<label class="block text-xs font-black text-slate-500 uppercase tracking-[0.15em] pl-1">{{ __('Target Machine') }} <span class="text-rose-500">*</span></label>
|
||||||
|
<x-searchable-select name="machine_id" :options="$machines->map(fn($m) => (object)['id' => $m->id, 'name' => $m->name . ' (' . $m->serial_no . ')'])" :placeholder="__('Select Machine')" class="w-full" required />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<label class="block text-xs font-black text-slate-500 uppercase tracking-[0.15em] pl-1">{{ __('Note') }}</label>
|
||||||
|
<input type="text" name="note" class="luxury-input w-full px-6 py-4 bg-slate-50/50 dark:bg-slate-900/50" placeholder="{{ __('Optional remarks...') }}">
|
||||||
|
</div>
|
||||||
|
<div class="space-y-5">
|
||||||
|
<div class="flex items-center justify-between pl-1">
|
||||||
|
<label class="text-xs font-black text-slate-500 uppercase tracking-[0.15em]">{{ __('Replenishment Items') }} <span class="text-rose-500">*</span></label>
|
||||||
|
<button type="button" @click="addItem()" class="text-xs font-black text-cyan-500 hover:text-cyan-400 uppercase tracking-widest flex items-center gap-1.5 transition-colors">
|
||||||
|
<svg class="w-4 h-4 stroke-[3]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
|
||||||
|
{{ __('Add Product') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<template x-for="(item, index) in items" :key="index">
|
||||||
|
<div class="group flex items-center gap-3 p-3 bg-slate-50/50 dark:bg-slate-900/30 rounded-2xl border border-slate-100 dark:border-slate-800/50 hover:border-cyan-500/30 transition-all">
|
||||||
|
<div class="w-20">
|
||||||
|
<input type="number" :name="'items['+index+'][slot_no]'" x-model.number="item.slot_no" min="1" required class="luxury-input w-full px-3 py-2 text-center text-sm font-mono font-bold [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" placeholder="{{ __('Slot') }}">
|
||||||
|
</div>
|
||||||
|
<div class="w-px h-6 bg-slate-200 dark:bg-slate-800/50"></div>
|
||||||
|
<div class="flex-1 min-w-[200px] relative" :id="'product-select-wrapper-' + index" x-init="$nextTick(() => updateProductSelects())">
|
||||||
|
<div x-show="isLoadingProducts" class="absolute inset-0 bg-white/50 dark:bg-slate-900/50 flex items-center justify-center z-10 rounded-xl">
|
||||||
|
<div class="w-4 h-4 border-2 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-px h-6 bg-slate-200 dark:bg-slate-800/50"></div>
|
||||||
|
<div class="flex items-center gap-1 bg-slate-100/50 dark:bg-slate-900/50 p-1 rounded-xl border border-slate-200/50 dark:border-slate-800/50">
|
||||||
|
<button type="button" @click="item.quantity > 1 ? item.quantity-- : null" class="p-1.5 text-slate-400 hover:text-cyan-500 transition-colors">
|
||||||
|
<svg class="w-3 h-3 stroke-[3]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14" /></svg>
|
||||||
|
</button>
|
||||||
|
<input type="number" :name="'items['+index+'][quantity]'" x-model.number="item.quantity" min="1" required class="w-8 bg-transparent border-none p-0 text-center font-mono font-bold text-slate-800 dark:text-slate-200 focus:ring-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" placeholder="0">
|
||||||
|
<button type="button" @click="item.quantity++" class="p-1.5 text-slate-400 hover:text-cyan-500 transition-colors">
|
||||||
|
<svg class="w-3 h-3 stroke-[3]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button type="button" @click="removeItem(index)" class="p-2 text-slate-300 hover:text-rose-500 transition-all" x-show="items.length > 1">
|
||||||
|
<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 18L18 6M6 6l12 12" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="px-10 py-6 border-t border-slate-100 dark:border-slate-800/50 flex items-center justify-end gap-4">
|
||||||
|
<button type="button" @click="showReplenishmentModal = false" class="btn-luxury-ghost px-8">{{ __('Cancel') }}</button>
|
||||||
|
<button type="button" @click="submitReplenishment()" :disabled="loading" class="btn-luxury-primary px-12 relative flex items-center justify-center">
|
||||||
|
<span :class="loading ? 'opacity-0' : ''">{{ __('Create') }}</span>
|
||||||
|
<template x-if="loading"><div class="absolute inset-0 flex items-center justify-center"><svg class="animate-spin h-5 w-5 text-white" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg></div></template>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,131 @@
|
|||||||
|
{{-- 補貨單詳情 Slide-over --}}
|
||||||
|
<div x-show="showOrderDetails" class="fixed inset-0 z-[120] overflow-hidden" style="display: none;" x-cloak>
|
||||||
|
<div class="absolute inset-0 overflow-hidden">
|
||||||
|
<div x-show="showOrderDetails" x-transition:enter="ease-in-out duration-500" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="ease-in-out duration-500" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm" @click="showOrderDetails = false"></div>
|
||||||
|
<div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10">
|
||||||
|
<div x-show="showOrderDetails" x-transition:enter="transform transition ease-in-out duration-500 sm:duration-700" x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0" x-transition:leave="transform transition ease-in-out duration-500 sm:duration-700" x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full" class="pointer-events-auto w-screen max-w-md">
|
||||||
|
<div class="flex h-full flex-col overflow-y-scroll bg-white dark:bg-slate-900 shadow-2xl border-l border-slate-100 dark:border-slate-800">
|
||||||
|
{{-- Header --}}
|
||||||
|
<div class="bg-slate-50/50 dark:bg-slate-900/50 px-6 py-8 border-b border-slate-100 dark:border-slate-800">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<h2 class="text-xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Replenishment Details') }}</h2>
|
||||||
|
<button type="button" @click="showOrderDetails = false" class="rounded-full bg-white dark:bg-slate-800 p-2 text-slate-400 hover:text-slate-500 border border-slate-100 dark:border-slate-700 shadow-sm transition-all">
|
||||||
|
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<template x-if="activeOrder">
|
||||||
|
<div class="mt-4 flex flex-col gap-1">
|
||||||
|
<span class="text-2xl font-black text-cyan-600 dark:text-cyan-400 font-mono tracking-tighter" x-text="activeOrder.order_no"></span>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]" x-text="activeOrder.warehouse_name"></span>
|
||||||
|
<svg class="w-3 h-3 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M13 7l5 5m0 0l-5 5m5-5H6" /></svg>
|
||||||
|
<span class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.2em]" x-text="activeOrder.machine_name"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Content --}}
|
||||||
|
<div class="relative flex-1 px-6 py-8">
|
||||||
|
<div x-show="detailsLoading" class="absolute inset-0 bg-white/60 dark:bg-slate-900/60 backdrop-blur-[2px] z-10 flex items-center justify-center">
|
||||||
|
<div class="w-10 h-10 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin"></div>
|
||||||
|
</div>
|
||||||
|
<template x-if="activeOrder">
|
||||||
|
<div class="space-y-10">
|
||||||
|
{{-- Info Grid --}}
|
||||||
|
<div class="grid grid-cols-2 gap-6 p-6 bg-slate-50/50 dark:bg-slate-800/30 rounded-[2rem] border border-slate-100 dark:border-slate-800">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Status') }}</p>
|
||||||
|
<span class="inline-flex items-center px-2 py-0.5 rounded-lg text-[10px] font-black uppercase tracking-widest"
|
||||||
|
:class="{
|
||||||
|
'bg-amber-500/10 text-amber-500 border border-amber-500/20': activeOrder.status === 'pending',
|
||||||
|
'bg-cyan-500/10 text-cyan-500 border border-cyan-500/20': activeOrder.status === 'prepared',
|
||||||
|
'bg-indigo-500/10 text-indigo-500 border border-indigo-500/20': activeOrder.status === 'delivering',
|
||||||
|
'bg-emerald-500/10 text-emerald-500 border border-emerald-500/20': activeOrder.status === 'completed',
|
||||||
|
'bg-slate-500/10 text-slate-500 border border-slate-500/20': activeOrder.status === 'cancelled',
|
||||||
|
}"
|
||||||
|
x-text="{'pending':'{{ __("Pending") }}','prepared':'{{ __("Prepared") }}','delivering':'{{ __("Delivering") }}','completed':'{{ __("Completed") }}','cancelled':'{{ __("Cancelled") }}'}[activeOrder.status]"></span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Assigned To') }}</p>
|
||||||
|
<p class="text-xs font-bold" :class="activeOrder.assignee_name ? 'text-slate-700 dark:text-slate-300' : 'text-slate-400'" x-text="activeOrder.assignee_name || '{{ __("Unassigned") }}'"></p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Created By') }}</p>
|
||||||
|
<p class="text-xs font-bold text-slate-700 dark:text-slate-300" x-text="activeOrder.creator_name"></p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Created At') }}</p>
|
||||||
|
<p class="text-[11px] font-mono font-bold text-slate-600 dark:text-slate-400" x-text="activeOrder.created_at"></p>
|
||||||
|
</div>
|
||||||
|
<template x-if="activeOrder.completed_at">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Completed At') }}</p>
|
||||||
|
<p class="text-[11px] font-mono font-bold text-emerald-600 dark:text-emerald-400" x-text="activeOrder.completed_at"></p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template x-if="activeOrder.note">
|
||||||
|
<div class="col-span-2 space-y-1 pt-2 border-t border-slate-200/50 dark:border-slate-700/50">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Note') }}</p>
|
||||||
|
<p class="text-xs font-bold text-slate-600 dark:text-slate-400 italic" x-text="activeOrder.note"></p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Action Buttons --}}
|
||||||
|
<template x-if="activeOrder.status !== 'completed' && activeOrder.status !== 'cancelled'">
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button type="button" @click="openAssignModal(activeOrder.id)" class="px-4 py-2 rounded-xl text-xs font-black bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all border border-slate-200 dark:border-slate-700">
|
||||||
|
{{ __('Assign Personnel') }}
|
||||||
|
</button>
|
||||||
|
<template x-if="activeOrder.status === 'pending'">
|
||||||
|
<button type="button" @click="advanceStatus(activeOrder.id, 'prepared')" class="px-4 py-2 rounded-xl text-xs font-black bg-cyan-500/10 text-cyan-500 border border-cyan-500/20 hover:bg-cyan-500 hover:text-white transition-all">{{ __('Confirm Prepare') }}</button>
|
||||||
|
</template>
|
||||||
|
<template x-if="activeOrder.status === 'prepared'">
|
||||||
|
<button type="button" @click="advanceStatus(activeOrder.id, 'delivering')" class="px-4 py-2 rounded-xl text-xs font-black bg-indigo-500/10 text-indigo-500 border border-indigo-500/20 hover:bg-indigo-500 hover:text-white transition-all">{{ __('Start Delivery') }}</button>
|
||||||
|
</template>
|
||||||
|
<template x-if="activeOrder.status === 'delivering'">
|
||||||
|
<button type="button" @click="advanceStatus(activeOrder.id, 'completed')" class="px-4 py-2 rounded-xl text-xs font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 hover:bg-emerald-500 hover:text-white transition-all">{{ __('Confirm Complete') }}</button>
|
||||||
|
</template>
|
||||||
|
<button type="button" @click="confirmCancel(activeOrder.id)" class="px-4 py-2 rounded-xl text-xs font-black bg-rose-500/10 text-rose-500 border border-rose-500/20 hover:bg-rose-500 hover:text-white transition-all">{{ __('Cancel Order') }}</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
{{-- Items List --}}
|
||||||
|
<div class="space-y-5">
|
||||||
|
<div class="flex items-center justify-between px-1">
|
||||||
|
<h3 class="text-sm font-black text-slate-800 dark:text-white uppercase tracking-widest">{{ __('Replenishment Items') }}</h3>
|
||||||
|
<span class="text-[10px] font-black text-cyan-500 bg-cyan-500/10 px-2 py-0.5 rounded-md uppercase" x-text="activeItems.length + ' {{ __("Items") }}'"></span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<template x-for="(item, idx) in activeItems" :key="idx">
|
||||||
|
<div class="group flex items-center gap-4 p-4 bg-white dark:bg-slate-900 rounded-2xl border border-slate-100 dark:border-slate-800 hover:border-cyan-500/30 transition-all">
|
||||||
|
<div class="w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center font-mono font-bold text-slate-500 border border-slate-200 dark:border-slate-700" x-text="item.slot_no"></div>
|
||||||
|
<div class="w-12 h-12 rounded-xl bg-slate-50 dark:bg-slate-800 flex items-center justify-center overflow-hidden border border-slate-100 dark:border-slate-800 group-hover:scale-105 transition-transform">
|
||||||
|
<template x-if="item.image_url"><img :src="item.image_url" class="w-full h-full object-cover"></template>
|
||||||
|
<template x-if="!item.image_url"><svg class="w-6 h-6 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg></template>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-sm font-black text-slate-800 dark:text-slate-100 truncate tracking-tight" x-text="item.product_name"></p>
|
||||||
|
<template x-if="item.current_stock !== null && item.max_stock">
|
||||||
|
<p class="text-[10px] font-bold text-slate-400 mt-0.5">
|
||||||
|
{{ __('Stock') }}: <span class="text-slate-600 dark:text-slate-300 font-mono" x-text="item.current_stock"></span>
|
||||||
|
/ <span class="font-mono" x-text="item.max_stock"></span>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<p class="text-base font-black text-cyan-600 dark:text-cyan-400 font-mono tracking-tighter" x-text="'x' + item.quantity"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -10,7 +10,6 @@
|
|||||||
:options="$warehouses"
|
:options="$warehouses"
|
||||||
:selected="request('warehouse_id')"
|
:selected="request('warehouse_id')"
|
||||||
:placeholder="__('All Warehouses')"
|
:placeholder="__('All Warehouses')"
|
||||||
x-on:change="handleFilterSubmit('movements')"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -19,7 +18,6 @@
|
|||||||
name="type"
|
name="type"
|
||||||
:selected="request('type')"
|
:selected="request('type')"
|
||||||
:placeholder="__('All Types')"
|
:placeholder="__('All Types')"
|
||||||
x-on:change="handleFilterSubmit('movements')"
|
|
||||||
>
|
>
|
||||||
@foreach(\App\Models\Warehouse\StockMovement::TYPE_LABELS as $key => $label)
|
@foreach(\App\Models\Warehouse\StockMovement::TYPE_LABELS as $key => $label)
|
||||||
<option value="{{ $key }}" {{ request('type') === $key ? 'selected' : '' }} data-title="{{ __($label) }}">
|
<option value="{{ $key }}" {{ request('type') === $key ? 'selected' : '' }} data-title="{{ __($label) }}">
|
||||||
@ -45,11 +43,9 @@
|
|||||||
if (selectedDates.length === 2) {
|
if (selectedDates.length === 2) {
|
||||||
$refs.startDate.value = instance.formatDate(selectedDates[0], 'Y-m-d H:i');
|
$refs.startDate.value = instance.formatDate(selectedDates[0], 'Y-m-d H:i');
|
||||||
$refs.endDate.value = instance.formatDate(selectedDates[1], 'Y-m-d H:i');
|
$refs.endDate.value = instance.formatDate(selectedDates[1], 'Y-m-d H:i');
|
||||||
handleFilterSubmit('movements');
|
|
||||||
} else if (selectedDates.length === 0) {
|
} else if (selectedDates.length === 0) {
|
||||||
$refs.startDate.value = '';
|
$refs.startDate.value = '';
|
||||||
$refs.endDate.value = '';
|
$refs.endDate.value = '';
|
||||||
handleFilterSubmit('movements');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})">
|
})">
|
||||||
@ -62,6 +58,30 @@
|
|||||||
value="{{ request('start_date') && request('end_date') ? request('start_date') . ' 至 ' . request('end_date') : (request('start_date') ?: '') }}"
|
value="{{ request('start_date') && request('end_date') ? request('start_date') . ' 至 ' . request('end_date') : (request('start_date') ?: '') }}"
|
||||||
placeholder="{{ __('Select Date Range') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer">
|
placeholder="{{ __('Select Date Range') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group" title="{{ __('Search') }}">
|
||||||
|
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button"
|
||||||
|
@click="
|
||||||
|
$el.closest('form').querySelectorAll('input[type=text], input[type=hidden]:not([name=tab])').forEach(i => i.value = '');
|
||||||
|
$el.closest('form').querySelectorAll('select').forEach(s => {
|
||||||
|
s.value = ' ';
|
||||||
|
const instance = window.HSSelect.getInstance(s);
|
||||||
|
if (instance) instance.setValue(' ');
|
||||||
|
});
|
||||||
|
handleFilterSubmit('movements');
|
||||||
|
"
|
||||||
|
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group" title="{{ __('Reset') }}">
|
||||||
|
<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="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>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -89,7 +109,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">
|
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">
|
||||||
{{ $mv->product?->name }}
|
{{ $mv->product?->localized_name }}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex items-center gap-2 mt-0.5">
|
<div class="flex items-center gap-2 mt-0.5">
|
||||||
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-wide">
|
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-wide">
|
||||||
|
|||||||
@ -34,11 +34,9 @@
|
|||||||
if (selectedDates.length === 2) {
|
if (selectedDates.length === 2) {
|
||||||
$refs.startDate.value = instance.formatDate(selectedDates[0], 'Y-m-d H:i');
|
$refs.startDate.value = instance.formatDate(selectedDates[0], 'Y-m-d H:i');
|
||||||
$refs.endDate.value = instance.formatDate(selectedDates[1], 'Y-m-d H:i');
|
$refs.endDate.value = instance.formatDate(selectedDates[1], 'Y-m-d H:i');
|
||||||
handleFilterSubmit('stock-in');
|
|
||||||
} else if (selectedDates.length === 0) {
|
} else if (selectedDates.length === 0) {
|
||||||
$refs.startDate.value = '';
|
$refs.startDate.value = '';
|
||||||
$refs.endDate.value = '';
|
$refs.endDate.value = '';
|
||||||
handleFilterSubmit('stock-in');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})">
|
})">
|
||||||
@ -51,6 +49,28 @@
|
|||||||
value="{{ request('start_date') && request('end_date') ? request('start_date') . ' 至 ' . request('end_date') : (request('start_date') ?: '') }}"
|
value="{{ request('start_date') && request('end_date') ? request('start_date') . ' 至 ' . request('end_date') : (request('start_date') ?: '') }}"
|
||||||
placeholder="{{ __('Select Date Range') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer">
|
placeholder="{{ __('Select Date Range') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group" title="{{ __('Search') }}">
|
||||||
|
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button"
|
||||||
|
@click="
|
||||||
|
$el.closest('form').querySelectorAll('input[type=text], input[type=hidden]:not([name=tab])').forEach(i => i.value = '');
|
||||||
|
$el.closest('form').querySelectorAll('select').forEach(s => {
|
||||||
|
s.value = ' ';
|
||||||
|
const instance = window.HSSelect.getInstance(s);
|
||||||
|
if (instance) instance.setValue(' ');
|
||||||
|
});
|
||||||
|
handleFilterSubmit('stock-in');
|
||||||
|
"
|
||||||
|
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group" title="{{ __('Reset') }}">
|
||||||
|
<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="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>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if(isset($stock_in_status_options))
|
@if(isset($stock_in_status_options))
|
||||||
@ -83,6 +103,9 @@
|
|||||||
<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">
|
<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') }}
|
{{ __('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 text-center">
|
||||||
|
{{ __('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">
|
<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">
|
||||||
{{ __('Date') }}
|
{{ __('Date') }}
|
||||||
</th>
|
</th>
|
||||||
@ -125,12 +148,19 @@
|
|||||||
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-amber-500/10 text-amber-500 border border-amber-500/20 uppercase tracking-widest shadow-sm shadow-amber-500/5">
|
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-amber-500/10 text-amber-500 border border-amber-500/20 uppercase tracking-widest shadow-sm shadow-amber-500/5">
|
||||||
{{ __('Draft') }}
|
{{ __('Draft') }}
|
||||||
</span>
|
</span>
|
||||||
|
@elseif($order->status === 'cancelled')
|
||||||
|
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-slate-500/10 text-slate-500 border border-slate-500/20 uppercase tracking-widest shadow-sm">
|
||||||
|
{{ __('Cancelled') }}
|
||||||
|
</span>
|
||||||
@else
|
@else
|
||||||
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 uppercase tracking-widest shadow-sm shadow-emerald-500/5">
|
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 uppercase tracking-widest shadow-sm shadow-emerald-500/5">
|
||||||
{{ __('Completed') }}
|
{{ __('Completed') }}
|
||||||
</span>
|
</span>
|
||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-6 py-5 text-center whitespace-nowrap">
|
||||||
|
<span class="font-black text-slate-800 dark:text-white">{{ $order->items_count }}</span>
|
||||||
|
</td>
|
||||||
<td class="px-6 py-5 text-center whitespace-nowrap">
|
<td class="px-6 py-5 text-center whitespace-nowrap">
|
||||||
<span class="text-xs font-mono font-black text-slate-500 dark:text-slate-400 tracking-widest">
|
<span class="text-xs font-mono font-black text-slate-500 dark:text-slate-400 tracking-widest">
|
||||||
{{ $order->created_at->format('Y-m-d H:i:s') }}
|
{{ $order->created_at->format('Y-m-d H:i:s') }}
|
||||||
@ -148,8 +178,9 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
@if($order->status === 'draft')
|
@if($order->status === 'draft')
|
||||||
|
{{-- Confirm Button --}}
|
||||||
<form action="{{ route('admin.warehouses.inventory.stock-in.confirm', $order) }}" method="POST" class="inline" @submit.prevent="confirmStockIn($el)">
|
<form action="{{ route('admin.warehouses.inventory.stock-in.confirm', $order) }}" method="POST" class="inline" @submit.prevent="confirmStockIn($el)">
|
||||||
@csrf
|
@csrf @method('PATCH')
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 transition-all border border-transparent hover:border-emerald-500/20"
|
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 transition-all border border-transparent hover:border-emerald-500/20"
|
||||||
title="{{ __('Confirm Stock-In') }}">
|
title="{{ __('Confirm Stock-In') }}">
|
||||||
@ -158,13 +189,22 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{{-- Delete/Cancel Button --}}
|
||||||
|
<button type="button" @click="confirmDelete('{{ route('admin.warehouses.inventory.stock-in.destroy', $order) }}')"
|
||||||
|
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 hover:bg-rose-500/5 transition-all border border-transparent hover:border-rose-500/20"
|
||||||
|
title="{{ __('Delete Draft') }}">
|
||||||
|
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@empty
|
@empty
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" class="px-6 py-20 text-center">
|
<td colspan="6" class="px-6 py-20 text-center">
|
||||||
<div class="flex flex-col items-center justify-center gap-3">
|
<div class="flex flex-col items-center justify-center gap-3">
|
||||||
<div class="p-4 rounded-3xl bg-slate-50 dark:bg-slate-800/50">
|
<div class="p-4 rounded-3xl bg-slate-50 dark:bg-slate-800/50">
|
||||||
<svg class="w-8 h-8 text-slate-300 dark:text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-8 h-8 text-slate-300 dark:text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@ -181,6 +221,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-8 py-6 border-t border-slate-50 dark:border-slate-800/50">
|
<div class="mt-8 py-6 border-t border-slate-50 dark:border-slate-800/50">
|
||||||
{{ $orders->links('vendor.pagination.luxury') }}
|
{{ $orders->links('vendor.pagination.luxury', ['ajax_navigate_event' => 'ajax:navigate:stock-in']) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||||
<form action="{{ route('admin.warehouses.inventory') }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 mb-8"
|
<form action="{{ route('admin.warehouses.inventory') }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 mb-8"
|
||||||
@submit.prevent="handleFilterSubmit('stock')"
|
@submit.prevent="handleFilterSubmit('stock')">
|
||||||
@multi-select-changed.window="handleFilterSubmit('stock')">
|
|
||||||
<input type="hidden" name="tab" value="stock">
|
<input type="hidden" name="tab" value="stock">
|
||||||
<div class="relative group">
|
<div class="relative group">
|
||||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10"><svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg></span>
|
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10"><svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg></span>
|
||||||
@ -20,6 +19,26 @@
|
|||||||
:placeholder="__('Filter by Warehouse Presence')"
|
:placeholder="__('Filter by Warehouse Presence')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group" title="{{ __('Search') }}">
|
||||||
|
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button"
|
||||||
|
@click="
|
||||||
|
$el.closest('form').querySelectorAll('input[type=text]').forEach(i => i.value = '');
|
||||||
|
$el.closest('form').dispatchEvent(new CustomEvent('multi-select-reset', { bubbles: true }));
|
||||||
|
handleFilterSubmit('stock');
|
||||||
|
"
|
||||||
|
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group" title="{{ __('Reset') }}">
|
||||||
|
<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="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>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="overflow-x-auto -mx-8 px-8 custom-scrollbar">
|
<div class="overflow-x-auto -mx-8 px-8 custom-scrollbar">
|
||||||
|
|||||||
@ -0,0 +1,110 @@
|
|||||||
|
<div id="replenishments-table-container">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left border-separate border-spacing-y-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 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 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 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Warehouse') }}</th>
|
||||||
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Items') }}</th>
|
||||||
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 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 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Assigned To') }}</th>
|
||||||
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Created By') }}</th>
|
||||||
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 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-50 dark:divide-slate-800/80">
|
||||||
|
@forelse($orders as $order)
|
||||||
|
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||||
|
{{-- 訂單編號 --}}
|
||||||
|
<td class="px-6 py-3.5">
|
||||||
|
<div class="flex flex-col cursor-pointer" @click="openOrderDetails('{{ $order->id }}')">
|
||||||
|
<span class="font-mono font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 transition-colors">{{ $order->order_no }}</span>
|
||||||
|
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">{{ $order->created_at->format('Y-m-d H:i:s') }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{{-- 機台 --}}
|
||||||
|
<td class="px-6 py-3.5">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-bold text-slate-700 dark:text-slate-200">{{ $order->machine?->name }}</span>
|
||||||
|
<span class="text-xs font-mono text-slate-400">{{ $order->machine?->serial_no }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{{-- 倉庫 --}}
|
||||||
|
<td class="px-6 py-3.5"><span class="font-bold text-slate-600 dark:text-slate-300">{{ $order->warehouse?->name }}</span></td>
|
||||||
|
{{-- 項目數 --}}
|
||||||
|
<td class="px-6 py-3.5 text-center"><span class="font-black text-slate-800 dark:text-white">{{ $order->items_count }}</span></td>
|
||||||
|
{{-- 狀態標籤 --}}
|
||||||
|
<td class="px-6 py-3.5 text-center">
|
||||||
|
@php
|
||||||
|
$colors = ['pending' => 'amber', 'prepared' => 'cyan', 'delivering' => 'indigo', 'completed' => 'emerald', 'cancelled' => 'slate'];
|
||||||
|
$sColor = $colors[$order->status] ?? 'cyan';
|
||||||
|
@endphp
|
||||||
|
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-{{ $sColor }}-500/10 text-{{ $sColor }}-500 border border-{{ $sColor }}-500/20 tracking-widest uppercase">{{ __(ucfirst($order->status)) }}</span>
|
||||||
|
</td>
|
||||||
|
{{-- 指派人員 --}}
|
||||||
|
<td class="px-6 py-3.5">
|
||||||
|
<span class="text-sm font-bold {{ $order->assignee ? 'text-slate-700 dark:text-slate-200' : 'text-slate-400' }}">
|
||||||
|
{{ $order->assignee?->name ?? __('Unassigned') }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
{{-- 建立人 --}}
|
||||||
|
<td class="px-6 py-3.5"><span class="text-sm font-bold text-slate-500">{{ $order->creator?->name ?? '-' }}</span></td>
|
||||||
|
{{-- 操作按鈕 --}}
|
||||||
|
<td class="px-6 py-3.5 text-right whitespace-nowrap">
|
||||||
|
<div class="flex items-center justify-end gap-2">
|
||||||
|
{{-- 查看詳情 --}}
|
||||||
|
<button type="button" @click="openOrderDetails('{{ $order->id }}')"
|
||||||
|
class="p-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-50 dark:hover:bg-cyan-500/10 transition-all border border-slate-100 dark:border-slate-700 shadow-sm group/btn"
|
||||||
|
title="{{ __('View Details') }}">
|
||||||
|
<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="M2.036 12.322a1.012 1.012 0 010-.644C3.399 8.049 7.21 5 12 5c4.79 0 8.601 3.049 9.964 6.678.045.133.045.278 0 .412C20.601 15.951 16.79 19 12 19c-4.79 0-8.601-3.049-9.964-6.678z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{{-- 狀態推進按鈕(依當前狀態顯示) --}}
|
||||||
|
@if($order->status === 'pending')
|
||||||
|
<button type="button"
|
||||||
|
@click.stop="advanceStatus('{{ $order->id }}', 'prepared')"
|
||||||
|
class="px-4 py-2 rounded-xl text-xs font-black bg-cyan-500/10 text-cyan-500 border border-cyan-500/20 hover:bg-cyan-500 hover:text-white transition-all shadow-sm">
|
||||||
|
{{ __('Confirm Prepare') }}
|
||||||
|
</button>
|
||||||
|
@elseif($order->status === 'prepared')
|
||||||
|
<button type="button"
|
||||||
|
@click.stop="advanceStatus('{{ $order->id }}', 'delivering')"
|
||||||
|
class="px-4 py-2 rounded-xl text-xs font-black bg-indigo-500/10 text-indigo-500 border border-indigo-500/20 hover:bg-indigo-500 hover:text-white transition-all shadow-sm">
|
||||||
|
{{ __('Start Delivery') }}
|
||||||
|
</button>
|
||||||
|
@elseif($order->status === 'delivering')
|
||||||
|
<button type="button"
|
||||||
|
@click.stop="advanceStatus('{{ $order->id }}', 'completed')"
|
||||||
|
class="px-4 py-2 rounded-xl text-xs font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 hover:bg-emerald-500 hover:text-white transition-all shadow-sm">
|
||||||
|
{{ __('Confirm Complete') }}
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- 取消按鈕(非完成/非取消狀態顯示) --}}
|
||||||
|
@if(in_array($order->status, ['pending', 'prepared', 'delivering']))
|
||||||
|
<button type="button"
|
||||||
|
@click.stop="confirmCancel('{{ $order->id }}')"
|
||||||
|
class="p-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-500/10 transition-all border border-slate-100 dark:border-slate-700 shadow-sm"
|
||||||
|
title="{{ __('Cancel Order') }}">
|
||||||
|
<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 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr><td colspan="8" class="px-6 py-20 text-center text-slate-400 font-bold">{{ __('No replenishment orders found') }}</td></tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">
|
||||||
|
{{ $orders->links('vendor.pagination.luxury', ['ajax_navigate_event' => 'ajax:navigate:replenishments']) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
<div id="transfers-table-container">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left border-separate border-spacing-y-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 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 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Type') }}</th>
|
||||||
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('From') }}</th>
|
||||||
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('To') }}</th>
|
||||||
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Items') }}</th>
|
||||||
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 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 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-50 dark:divide-slate-800/80">
|
||||||
|
@forelse($orders as $order)
|
||||||
|
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||||
|
<td class="px-6 py-5 cursor-pointer" @click="openOrderDetails('{{ $order->id }}')">
|
||||||
|
<div class="flex items-center gap-x-4">
|
||||||
|
<div class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col group/no">
|
||||||
|
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover/no:text-cyan-600 dark:group-hover/no:text-cyan-400 transition-colors">
|
||||||
|
{{ $order->order_no }}
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-1.5 mt-1">
|
||||||
|
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{{ $order->created_at->format('Y-m-d H:i:s') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-3.5">
|
||||||
|
@if($order->type === 'warehouse_to_warehouse')
|
||||||
|
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black bg-indigo-500/10 text-indigo-500 border border-indigo-500/20 uppercase tracking-widest">{{ __('W2W') }}</span>
|
||||||
|
@else
|
||||||
|
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black bg-amber-500/10 text-amber-500 border border-amber-500/20 uppercase tracking-widest">{{ __('M2W') }}</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-3.5 font-bold text-slate-600 dark:text-slate-300">
|
||||||
|
{{ $order->type === 'warehouse_to_warehouse' ? ($order->fromWarehouse?->name ?? '-') : ($order->fromMachine?->name ?? '-') }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-3.5 font-bold text-slate-600 dark:text-slate-300">{{ $order->toWarehouse?->name ?? '-' }}</td>
|
||||||
|
<td class="px-6 py-3.5 text-center font-black text-slate-800 dark:text-white">{{ $order->items_count }}</td>
|
||||||
|
<td class="px-6 py-3.5 text-center">
|
||||||
|
@php
|
||||||
|
$colors = ['draft' => 'amber', 'completed' => 'emerald', 'cancelled' => 'slate'];
|
||||||
|
$sColor = $colors[$order->status] ?? 'cyan';
|
||||||
|
@endphp
|
||||||
|
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-{{ $sColor }}-500/10 text-{{ $sColor }}-500 border border-{{ $sColor }}-500/20 tracking-widest uppercase">{{ __(ucfirst($order->status)) }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-3.5 text-right whitespace-nowrap">
|
||||||
|
<div class="flex items-center justify-end gap-2">
|
||||||
|
<button type="button" @click.stop="openOrderDetails('{{ $order->id }}')"
|
||||||
|
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20"
|
||||||
|
title="{{ __('View Details') }}">
|
||||||
|
<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="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@if($order->status === 'draft')
|
||||||
|
<button type="button" @click.stop="confirmTransfer('{{ route('admin.warehouses.transfers.confirm', $order->id) }}')"
|
||||||
|
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 transition-all border border-transparent hover:border-emerald-500/20"
|
||||||
|
title="{{ __('Confirm') }}">
|
||||||
|
<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="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" @click.stop="confirmDelete('{{ route('admin.warehouses.transfers.destroy', $order) }}')"
|
||||||
|
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 hover:bg-rose-500/5 transition-all border border-transparent hover:border-rose-500/20"
|
||||||
|
title="{{ __('Delete') }}">
|
||||||
|
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr><td colspan="7" class="px-6 py-20 text-center text-slate-400 font-bold uppercase tracking-widest">{{ __('No transfer orders found') }}</td></tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">
|
||||||
|
{{ $orders->links('vendor.pagination.luxury', ['ajax_navigate_event' => 'ajax:navigate:transfers']) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -4,152 +4,357 @@
|
|||||||
<script>
|
<script>
|
||||||
function replenishmentManager() {
|
function replenishmentManager() {
|
||||||
return {
|
return {
|
||||||
showModal: false,
|
showReplenishmentModal: false,
|
||||||
|
showAutoModal: false,
|
||||||
|
showCancelModal: false,
|
||||||
|
showAssignModal: false,
|
||||||
fromId: '',
|
fromId: '',
|
||||||
availableProducts: [],
|
availableProducts: [],
|
||||||
allProducts: @json(\App\Models\Product\Product::orderBy('name')->get(['id', 'name'])),
|
allProducts: @json($products ?? []),
|
||||||
isLoadingProducts: false,
|
isLoadingProducts: false,
|
||||||
items: [{ product_id: '', slot_no: '', quantity: 1 }],
|
items: [{ product_id: '', slot_no: '', quantity: 1, current_stock: 0, max_stock: 0 }],
|
||||||
loading: false,
|
loading: false,
|
||||||
|
cancelOrderId: null,
|
||||||
// Details Panel State
|
assignOrderId: null,
|
||||||
showDetailsPanel: false,
|
assignUserId: '',
|
||||||
|
|
||||||
|
// Auto replenishment
|
||||||
|
autoWarehouseId: '',
|
||||||
|
autoMachineId: '',
|
||||||
|
autoNote: '',
|
||||||
|
autoSlots: [],
|
||||||
|
autoHasWarning: false,
|
||||||
|
autoInsufficient: {},
|
||||||
|
autoWarehouseStocks: {},
|
||||||
|
autoPreviewLoaded: false,
|
||||||
|
autoLoading: false,
|
||||||
|
|
||||||
|
// Details Panel
|
||||||
|
showOrderDetails: false,
|
||||||
detailsLoading: false,
|
detailsLoading: false,
|
||||||
selectedOrder: null,
|
activeOrder: null,
|
||||||
orderItems: [],
|
activeItems: [],
|
||||||
|
|
||||||
async openDetails(id) {
|
init() {
|
||||||
this.showDetailsPanel = true;
|
// 偵測 URL 參數自動開啟一鍵補貨 Modal
|
||||||
this.detailsLoading = true;
|
const params = new URLSearchParams(window.location.search);
|
||||||
this.selectedOrder = null;
|
const autoMachineId = params.get('auto_machine_id');
|
||||||
this.orderItems = [];
|
if (autoMachineId) {
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`{{ url('/admin/warehouses/replenishments') }}/${id}/details`, {
|
|
||||||
headers: {
|
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
|
||||||
'Accept': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
|
||||||
this.selectedOrder = data.order;
|
|
||||||
this.orderItems = data.items;
|
|
||||||
} else {
|
|
||||||
throw new Error(data.message || 'Failed to load details');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('openDetails error:', e);
|
|
||||||
window.showToast?.('{{ __("Failed to load details") }}', 'error');
|
|
||||||
} finally {
|
|
||||||
this.detailsLoading = false;
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit();
|
this.autoMachineId = autoMachineId;
|
||||||
|
this.showAutoModal = true;
|
||||||
|
this.autoPreviewLoaded = false;
|
||||||
|
this.autoSlots = [];
|
||||||
|
this.autoNote = '';
|
||||||
|
|
||||||
|
// 同步 HSSelect UI
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const select = document.querySelector('[name="auto_machine_id"]');
|
||||||
|
if (select && window.HSSelect) {
|
||||||
|
const inst = window.HSSelect.getInstance(select);
|
||||||
|
if (inst) inst.setValue(autoMachineId);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
// 清除 URL 參數避免重複觸發
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.searchParams.delete('auto_machine_id');
|
||||||
|
window.history.replaceState({}, '', url);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
openCreateModal() {
|
async openOrderDetails(id) {
|
||||||
this.items = [{ product_id: '', slot_no: '', quantity: 1 }];
|
this.showOrderDetails = true;
|
||||||
this.showModal = true;
|
this.detailsLoading = true;
|
||||||
this.$nextTick(() => {
|
this.activeOrder = null;
|
||||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
this.activeItems = [];
|
||||||
});
|
try {
|
||||||
|
const res = await fetch(`{{ url('/admin/warehouses/replenishments') }}/${id}/details`, {
|
||||||
|
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) { this.activeOrder = data.order; this.activeItems = data.items; }
|
||||||
|
} catch (e) { console.error(e); window.showToast?.('{{ __("Failed to load details") }}', 'error'); }
|
||||||
|
finally { this.detailsLoading = false; }
|
||||||
},
|
},
|
||||||
|
|
||||||
addItem() {
|
openCreateModal() {
|
||||||
this.items.push({ product_id: '', slot_no: '', quantity: 1 });
|
this.items = [{ product_id: '', slot_no: '', quantity: 1, current_stock: 0, max_stock: 0 }];
|
||||||
this.$nextTick(() => {
|
this.showReplenishmentModal = true;
|
||||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
this.$nextTick(() => { if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select'); });
|
||||||
});
|
},
|
||||||
|
|
||||||
|
addItem() {
|
||||||
|
this.items.push({ product_id: '', slot_no: '', quantity: 1, current_stock: 0, max_stock: 0 });
|
||||||
|
this.$nextTick(() => this.updateProductSelects());
|
||||||
},
|
},
|
||||||
removeItem(i) { if (this.items.length > 1) this.items.splice(i, 1); },
|
removeItem(i) { if (this.items.length > 1) this.items.splice(i, 1); },
|
||||||
|
|
||||||
async fetchStock() {
|
async fetchStock() {
|
||||||
this.availableProducts = [];
|
this.availableProducts = [];
|
||||||
if (!this.fromId || this.fromId.trim() === '') return;
|
if (!this.fromId) { this.$nextTick(() => this.updateProductSelects()); return; }
|
||||||
|
|
||||||
this.isLoadingProducts = true;
|
this.isLoadingProducts = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch('{{ route('admin.warehouses.ajax.stock') }}?warehouse_id=' + this.fromId);
|
const res = await fetch('{{ route("admin.warehouses.ajax.stock") }}?warehouse_id=' + this.fromId);
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
if (json.success) {
|
if (json.success) { this.availableProducts = json.data; this.$nextTick(() => this.updateProductSelects()); }
|
||||||
this.availableProducts = json.data;
|
} catch (e) { console.error(e); }
|
||||||
|
finally { this.isLoadingProducts = false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
updateProductSelects() {
|
||||||
|
this.items.forEach((item, index) => {
|
||||||
|
const wrapper = document.getElementById(`product-select-wrapper-${index}`);
|
||||||
|
if (!wrapper) return;
|
||||||
|
const oldSelect = wrapper.querySelector('select');
|
||||||
|
if (oldSelect && window.HSSelect?.getInstance(oldSelect)) {
|
||||||
|
try { window.HSSelect.getInstance(oldSelect).destroy(); } catch (e) {}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
wrapper.innerHTML = '';
|
||||||
console.error(e);
|
const selectEl = document.createElement('select');
|
||||||
} finally {
|
selectEl.id = `product-select-${index}-${Date.now()}`;
|
||||||
this.isLoadingProducts = false;
|
selectEl.name = `items[${index}][product_id]`;
|
||||||
this.$nextTick(() => {
|
selectEl.required = true;
|
||||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
selectEl.className = 'hidden';
|
||||||
|
const products = this.fromId ? this.availableProducts : this.allProducts;
|
||||||
|
const placeholderOpt = document.createElement('option');
|
||||||
|
placeholderOpt.value = '';
|
||||||
|
placeholderOpt.textContent = '{{ __("Select Product") }}';
|
||||||
|
selectEl.appendChild(placeholderOpt);
|
||||||
|
products.forEach(p => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = p.id;
|
||||||
|
const stockText = p.quantity !== undefined ? ` (${'{{ __("Stock") }}'}: ${p.quantity})` : '';
|
||||||
|
opt.textContent = p.name + stockText;
|
||||||
|
opt.setAttribute('data-title', p.name + stockText);
|
||||||
|
if (item.product_id == p.id) opt.selected = true;
|
||||||
|
selectEl.appendChild(opt);
|
||||||
});
|
});
|
||||||
}
|
selectEl.setAttribute('data-hs-select', JSON.stringify({
|
||||||
|
"placeholder": "{{ __('Select Product') }}",
|
||||||
|
"toggleClasses": "hs-select-toggle luxury-select-toggle w-full text-left",
|
||||||
|
"toggleTemplate": "<button type=\"button\"><span class=\"text-slate-800 dark:text-slate-200\" data-title></span><div class=\"ms-auto\"><svg class=\"size-4 text-slate-400\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m6 9 6 6 6-6\"/></svg></div></button>",
|
||||||
|
"dropdownClasses": "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10 rounded-xl shadow-2xl mt-2 z-[150] max-h-48 overflow-y-auto custom-scrollbar-thin",
|
||||||
|
"optionClasses": "hs-select-option py-2 px-3 text-sm text-slate-800 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-cyan-500/10 rounded-lg",
|
||||||
|
"hasSearch": true, "searchPlaceholder": "{{ __('Search Product') }}",
|
||||||
|
"searchClasses": "block w-[calc(100%-16px)] mx-2 py-2 px-3 text-sm border-slate-200 dark:border-white/10 rounded-lg focus:border-cyan-500 focus:ring-cyan-500 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200",
|
||||||
|
"searchWrapperClasses": "sticky top-0 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md p-2 z-10",
|
||||||
|
"strategy": "fixed"
|
||||||
|
}));
|
||||||
|
wrapper.appendChild(selectEl);
|
||||||
|
selectEl.addEventListener('change', (e) => { item.product_id = e.target.value; });
|
||||||
|
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async submitReplenishment() {
|
async submitReplenishment() {
|
||||||
const form = document.getElementById('replenishmentForm');
|
const form = document.getElementById('replenishmentForm');
|
||||||
const warehouseId = form.querySelector('[name="warehouse_id"]')?.value;
|
const warehouseId = this.fromId;
|
||||||
const machineId = form.querySelector('[name="machine_id"]')?.value;
|
const machineId = form.querySelector('[name="machine_id"]')?.value;
|
||||||
|
|
||||||
if (!warehouseId) {
|
if (!warehouseId || warehouseId === ' ') {
|
||||||
window.showToast?.('{{ __("Please select source warehouse") }}', 'error');
|
window.showToast?.('{{ __("Please select source warehouse") }}', 'error'); return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (!machineId) {
|
if (!machineId || machineId === ' ') {
|
||||||
window.showToast?.('{{ __("Please select target machine") }}', 'error');
|
window.showToast?.('{{ __("Please select target machine") }}', 'error'); return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
if (this.items.length === 0) {
|
||||||
if (this.items.length === 0) {
|
window.showToast?.('{{ __("Please add at least one item") }}', 'error'); return;
|
||||||
window.showToast?.('{{ __("Please add at least one item") }}', 'error');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasError = false;
|
for (let i = 0; i < this.items.length; i++) {
|
||||||
this.items.forEach(item => {
|
if (!this.items[i].product_id) {
|
||||||
if (!item.slot_no || !item.product_id || !item.quantity || item.quantity < 1) {
|
window.showToast?.('{{ __("Please select product for item :num") }}'.replace(':num', i+1), 'error'); return;
|
||||||
hasError = true;
|
}
|
||||||
|
if (!this.items[i].quantity || this.items[i].quantity < 1) {
|
||||||
|
window.showToast?.('{{ __("Please enter valid quantity for item :num") }}'.replace(':num', i+1), 'error'); return;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (hasError) {
|
|
||||||
window.showToast?.('{{ __("Please select slot, product and valid quantity for all items") }}', 'error');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
const response = await fetch(form.action, {
|
const response = await fetch(form.action, {
|
||||||
method: 'POST',
|
method: 'POST', body: formData,
|
||||||
body: formData,
|
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
|
||||||
headers: {
|
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (response.ok && result.success) {
|
||||||
|
this.showReplenishmentModal = false;
|
||||||
window.showToast?.(result.message, 'success');
|
window.showToast?.(result.message, 'success');
|
||||||
this.showModal = false;
|
this.fetchTabData();
|
||||||
window.location.reload();
|
} else if (response.status === 422) {
|
||||||
} else {
|
const firstError = Object.values(result.errors || {})[0]?.[0] || '{{ __("Validation failed") }}';
|
||||||
window.showToast?.(result.message || '{{ __("Validation failed") }}', 'error');
|
window.showToast?.(firstError, 'error');
|
||||||
}
|
} else { window.showToast?.(result.message || '{{ __("System error") }}', 'error'); }
|
||||||
} catch (error) {
|
} catch (e) { console.error(e); window.showToast?.('{{ __("System Error") }}', 'error'); }
|
||||||
console.error(error);
|
finally { this.loading = false; }
|
||||||
window.showToast?.('{{ __("System Error") }}', 'error');
|
},
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
// 一鍵補貨預覽
|
||||||
|
async loadAutoPreview() {
|
||||||
|
if (!this.autoWarehouseId || this.autoWarehouseId === ' ' || !this.autoMachineId || this.autoMachineId === ' ') {
|
||||||
|
window.showToast?.('{{ __("Please select warehouse and machine") }}', 'error'); return;
|
||||||
}
|
}
|
||||||
|
this.autoLoading = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch('{{ route("admin.warehouses.replenishments.auto") }}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
|
||||||
|
body: JSON.stringify({ warehouse_id: this.autoWarehouseId, machine_id: this.autoMachineId })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success && data.preview) {
|
||||||
|
this.autoSlots = data.slots;
|
||||||
|
this.autoHasWarning = data.has_warning;
|
||||||
|
this.autoInsufficient = data.insufficient || {};
|
||||||
|
this.autoWarehouseStocks = data.warehouse_stocks || {};
|
||||||
|
this.autoPreviewLoaded = true;
|
||||||
|
} else {
|
||||||
|
window.showToast?.(data.message || '{{ __("Failed to load preview") }}', 'error');
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e); window.showToast?.('{{ __("System Error") }}', 'error'); }
|
||||||
|
finally { this.autoLoading = false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
getActualFillable(index) {
|
||||||
|
const slot = this.autoSlots[index];
|
||||||
|
if (!slot) return 0;
|
||||||
|
|
||||||
|
let available = this.autoWarehouseStocks[slot.product_id] || 0;
|
||||||
|
for (let i = 0; i < index; i++) {
|
||||||
|
if (this.autoSlots[i].product_id === slot.product_id) {
|
||||||
|
available -= this.autoSlots[i].quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Math.max(0, Math.min(slot.quantity, available));
|
||||||
|
},
|
||||||
|
|
||||||
|
removeAutoSlot(idx) {
|
||||||
|
this.autoSlots.splice(idx, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
async submitAutoReplenishment() {
|
||||||
|
if (!this.autoSlots || this.autoSlots.length === 0) {
|
||||||
|
window.showToast?.('{{ __("No items to replenish") }}', 'error'); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.autoLoading = true;
|
||||||
|
const items = this.autoSlots.map(s => ({
|
||||||
|
slot_no: s.slot_no,
|
||||||
|
product_id: s.product_id,
|
||||||
|
quantity: s.quantity,
|
||||||
|
current_stock: s.current_stock,
|
||||||
|
max_stock: s.max_stock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('{{ route("admin.warehouses.replenishments.auto") }}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
warehouse_id: this.autoWarehouseId,
|
||||||
|
machine_id: this.autoMachineId,
|
||||||
|
note: this.autoNote,
|
||||||
|
items
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
this.showAutoModal = false;
|
||||||
|
this.autoPreviewLoaded = false;
|
||||||
|
this.autoSlots = [];
|
||||||
|
window.showToast?.(data.message, 'success');
|
||||||
|
this.fetchTabData();
|
||||||
|
} else {
|
||||||
|
window.showToast?.(data.message || '{{ __("Creation failed") }}', 'error');
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e); window.showToast?.('{{ __("System Error") }}', 'error'); }
|
||||||
|
finally { this.autoLoading = false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
// 狀態推進
|
||||||
|
async advanceStatus(orderId, newStatus) {
|
||||||
|
const labels = { prepared: '{{ __("Confirm Prepare") }}', delivering: '{{ __("Start Delivery") }}', completed: '{{ __("Confirm Complete") }}' };
|
||||||
|
if (!confirm(labels[newStatus] + '?')) return;
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`{{ url('/admin/warehouses/replenishments') }}/${orderId}/status`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
|
||||||
|
body: JSON.stringify({ status: newStatus })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) { window.showToast?.(data.message, 'success'); this.fetchTabData(); }
|
||||||
|
else { window.showToast?.(data.message || '{{ __("Operation failed") }}', 'error'); }
|
||||||
|
} catch (e) { console.error(e); window.showToast?.('{{ __("System Error") }}', 'error'); }
|
||||||
|
finally { this.loading = false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
// 取消
|
||||||
|
confirmCancel(orderId) { this.cancelOrderId = orderId; this.showCancelModal = true; },
|
||||||
|
async executeCancel() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`{{ url('/admin/warehouses/replenishments') }}/${this.cancelOrderId}/cancel`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
this.showCancelModal = false;
|
||||||
|
let msg = data.message;
|
||||||
|
if (data.stock_returned) msg += ' — {{ __("Stock returned to warehouse") }}';
|
||||||
|
window.showToast?.(msg, 'success');
|
||||||
|
this.fetchTabData();
|
||||||
|
} else { window.showToast?.(data.message, 'error'); }
|
||||||
|
} catch (e) { console.error(e); window.showToast?.('{{ __("System Error") }}', 'error'); }
|
||||||
|
finally { this.loading = false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
// 指派
|
||||||
|
openAssignModal(orderId) { this.assignOrderId = orderId; this.assignUserId = ''; this.showAssignModal = true; },
|
||||||
|
async executeAssign() {
|
||||||
|
if (!this.assignUserId) { window.showToast?.('{{ __("Select Personnel") }}', 'error'); return; }
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`{{ url('/admin/warehouses/replenishments') }}/${this.assignOrderId}/assign`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
|
||||||
|
body: JSON.stringify({ assigned_to: this.assignUserId })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) { this.showAssignModal = false; window.showToast?.(data.message, 'success'); this.fetchTabData(); }
|
||||||
|
else { window.showToast?.(data.message, 'error'); }
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
finally { this.loading = false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchTabData(url = null) {
|
||||||
|
this.loading = true;
|
||||||
|
const container = document.getElementById('replenishments-table-container');
|
||||||
|
if (!url) {
|
||||||
|
const form = document.querySelector('form[action="{{ route('admin.warehouses.replenishments') }}"]');
|
||||||
|
url = `${form.action}?${new URLSearchParams(new FormData(form)).toString()}`;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } });
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success && container) {
|
||||||
|
container.outerHTML = data.html;
|
||||||
|
this.$nextTick(() => { if (window.HSStaticMethods) window.HSStaticMethods.autoInit(); });
|
||||||
|
window.history.pushState({}, '', new URL(url, window.location.origin).toString());
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e); window.showToast?.('{{ __("Loading failed") }}', 'error'); }
|
||||||
|
finally { this.loading = false; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-3 pb-10" x-data="replenishmentManager()" @open-details.window="openDetails($event.detail.id)">
|
<div class="space-y-3 pb-10" x-data="replenishmentManager()"
|
||||||
|
@open-details.window="openOrderDetails($event.detail.id)"
|
||||||
|
@ajax:navigate:replenishments.window.prevent="fetchTabData($event.detail.url)">
|
||||||
|
|
||||||
{{-- Header --}}
|
{{-- Header --}}
|
||||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
@ -157,390 +362,66 @@
|
|||||||
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Machine Replenishment') }}</h1>
|
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Machine Replenishment') }}</h1>
|
||||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Create and manage replenishment orders from warehouse to machines') }}</p>
|
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Create and manage replenishment orders from warehouse to machines') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" @click="openCreateModal()" class="btn-luxury-primary">
|
<div class="flex items-center gap-3">
|
||||||
<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 type="button" @click="
|
||||||
<span>{{ __('New Replenishment') }}</span>
|
autoWarehouseId = '';
|
||||||
</button>
|
autoMachineId = '';
|
||||||
|
autoNote = '';
|
||||||
|
autoPreviewLoaded = false;
|
||||||
|
autoSlots = [];
|
||||||
|
showAutoModal = true;
|
||||||
|
$nextTick(() => {
|
||||||
|
document.querySelectorAll('[name^=\'auto_\']').forEach(s => {
|
||||||
|
if (window.HSSelect) {
|
||||||
|
const inst = window.HSSelect.getInstance(s);
|
||||||
|
if (inst) inst.setValue(' ');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
" class="btn-luxury-ghost border-cyan-500/30 text-cyan-600 hover:bg-cyan-50 dark:hover:bg-cyan-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.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" /></svg>
|
||||||
|
<span>{{ __('Auto Replenishment') }}</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="openCreateModal()" class="btn-luxury-primary">
|
||||||
|
<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>
|
||||||
|
<span>{{ __('New Replenishment') }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Filters --}}
|
{{-- Filters --}}
|
||||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||||
<form action="{{ route('admin.warehouses.replenishments') }}" method="GET" class="mb-8">
|
<form id="replenishment-filter-form" action="{{ route('admin.warehouses.replenishments') }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 mb-8">
|
||||||
<div class="flex items-center p-1 bg-slate-100/50 dark:bg-slate-900/50 backdrop-blur-md rounded-2xl border border-slate-200/50 dark:border-slate-700/50 w-fit">
|
<div class="relative group flex-1 md:max-w-[240px]">
|
||||||
<a href="{{ route('admin.warehouses.replenishments') }}"
|
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||||
class="px-5 py-2 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ !request()->filled('status') ? 'bg-white dark:bg-slate-800 text-cyan-600 shadow-lg shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600' }}">
|
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||||
{{ __('All') }}
|
</span>
|
||||||
</a>
|
<input type="text" name="search_order" value="{{ request('search_order') }}" @keydown.enter.prevent="fetchTabData()" class="py-2.5 pl-12 pr-6 block w-full luxury-input" placeholder="{{ __('Search order number...') }}">
|
||||||
@foreach(['pending' => 'Pending', 'prepared' => 'Prepared', 'delivering' => 'Delivering', 'completed' => 'Completed'] as $key => $label)
|
|
||||||
<a href="{{ route('admin.warehouses.replenishments', ['status' => $key]) }}"
|
|
||||||
class="px-5 py-2 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ request('status') === $key ? 'bg-white dark:bg-slate-800 text-cyan-600 shadow-lg shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600' }}">
|
|
||||||
{{ __($label) }}
|
|
||||||
</a>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-1 md:max-w-[180px]">
|
||||||
|
<x-searchable-select name="status" :selected="request('status')" :placeholder="__('All Statuses')">
|
||||||
|
<option value="" data-title="{{ __('All Statuses') }}">{{ __('All Statuses') }}</option>
|
||||||
|
<option value="pending" {{ request('status') === 'pending' ? 'selected' : '' }} data-title="{{ __('Pending') }}">{{ __('Pending') }}</option>
|
||||||
|
<option value="prepared" {{ request('status') === 'prepared' ? 'selected' : '' }} data-title="{{ __('Prepared') }}">{{ __('Prepared') }}</option>
|
||||||
|
<option value="delivering" {{ request('status') === 'delivering' ? 'selected' : '' }} data-title="{{ __('Delivering') }}">{{ __('Delivering') }}</option>
|
||||||
|
<option value="completed" {{ request('status') === 'completed' ? 'selected' : '' }} data-title="{{ __('Completed') }}">{{ __('Completed') }}</option>
|
||||||
|
<option value="cancelled" {{ request('status') === 'cancelled' ? 'selected' : '' }} data-title="{{ __('Cancelled') }}">{{ __('Cancelled') }}</option>
|
||||||
|
</x-searchable-select>
|
||||||
|
</div>
|
||||||
|
<button type="button" @click="fetchTabData()" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25" title="{{ __('Search') }}">
|
||||||
|
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="$el.closest('form').querySelectorAll('input[type=text]').forEach(i => i.value = ''); $el.closest('form').querySelectorAll('select').forEach(s => { s.value = ' '; const inst = window.HSSelect?.getInstance(s); if(inst) inst.setValue(' '); }); fetchTabData();" class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700" title="{{ __('Reset') }}">
|
||||||
|
<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="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>
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
@include('admin.warehouses.partials.table-replenishments')
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="w-full text-left border-separate border-spacing-y-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 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 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 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Warehouse') }}</th>
|
|
||||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Items') }}</th>
|
|
||||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 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 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Created By') }}</th>
|
|
||||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 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-50 dark:divide-slate-800/80">
|
|
||||||
@forelse($orders as $order)
|
|
||||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
|
||||||
<td class="px-6 py-3.5">
|
|
||||||
<div class="flex flex-col cursor-pointer" @click="openDetails('{{ $order->id }}')">
|
|
||||||
<span class="font-mono font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 transition-colors">{{ $order->order_no }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-3.5">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<span class="font-bold text-slate-700 dark:text-slate-200">{{ $order->machine?->name }}</span>
|
|
||||||
<span class="text-xs font-mono text-slate-400">{{ $order->machine?->serial_no }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-3.5"><span class="font-bold text-slate-600 dark:text-slate-300">{{ $order->warehouse?->name }}</span></td>
|
|
||||||
<td class="px-6 py-3.5 text-center"><span class="font-black text-slate-800 dark:text-white">{{ $order->items_count }}</span></td>
|
|
||||||
<td class="px-6 py-3.5 text-center">
|
|
||||||
@php
|
|
||||||
$colors = ['pending' => 'amber', 'prepared' => 'cyan', 'delivering' => 'indigo', 'completed' => 'emerald', 'cancelled' => 'slate'];
|
|
||||||
$sColor = $colors[$order->status] ?? 'cyan';
|
|
||||||
@endphp
|
|
||||||
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-{{ $sColor }}-500/10 text-{{ $sColor }}-500 border border-{{ $sColor }}-500/20 tracking-widest uppercase">{{ __(ucfirst($order->status)) }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-3.5"><span class="text-sm font-bold text-slate-500">{{ $order->creator?->name ?? '-' }}</span></td>
|
|
||||||
<td class="px-6 py-3.5 text-right whitespace-nowrap">
|
|
||||||
<div class="flex items-center justify-end gap-2">
|
|
||||||
<button type="button" @click="openDetails('{{ $order->id }}')"
|
|
||||||
class="p-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20"
|
|
||||||
title="{{ __('View Details') }}">
|
|
||||||
<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="M2.036 12.322a1.012 1.012 0 010-.644C3.399 8.049 7.21 5 12 5c4.79 0 8.601 3.049 9.964 6.678.045.133.045.278 0 .412C20.601 15.951 16.79 19 12 19c-4.79 0-8.601-3.049-9.964-6.678z" />
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
@if(in_array($order->status, ['pending', 'prepared', 'delivering']))
|
|
||||||
<form action="{{ route('admin.warehouses.replenishments.confirm', $order->id) }}" method="POST" class="inline" onsubmit="return confirm('{{ __('Confirm replenishment completed?') }}')">
|
|
||||||
@csrf @method('PATCH')
|
|
||||||
<button type="submit" class="px-4 py-2 rounded-xl text-xs font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 hover:bg-emerald-500 hover:text-white transition-all">{{ __('Complete') }}</button>
|
|
||||||
</form>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
@empty
|
|
||||||
<tr><td colspan="7" class="px-6 py-20 text-center text-slate-400 font-bold">{{ __('No replenishment orders found') }}</td></tr>
|
|
||||||
@endforelse
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">{{ $orders->links('vendor.pagination.luxury') }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- New Replenishment Modal --}}
|
@include('admin.warehouses.partials.modal-replenishment-create')
|
||||||
<div x-show="showModal" class="fixed inset-0 z-[110] overflow-y-auto" style="display: none;" role="dialog" aria-modal="true" x-cloak>
|
@include('admin.warehouses.partials.modal-replenishment-auto')
|
||||||
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
@include('admin.warehouses.partials.slideover-replenishment-details')
|
||||||
<!-- Background Backdrop -->
|
@include('admin.warehouses.partials.modal-replenishment-cancel')
|
||||||
<div x-show="showModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0"
|
@include('admin.warehouses.partials.modal-replenishment-assign')
|
||||||
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200"
|
|
||||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
|
||||||
class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity"
|
|
||||||
@click="showModal = false"></div>
|
|
||||||
|
|
||||||
<span class="hidden sm:inline-block sm:align-middle sm:min-h-screen" aria-hidden="true">​</span>
|
|
||||||
|
|
||||||
<!-- Modal Panel -->
|
|
||||||
<div x-show="showModal"
|
|
||||||
x-transition:enter="ease-out duration-300"
|
|
||||||
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 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 translate-y-0 sm:scale-100"
|
|
||||||
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
class="relative inline-flex flex-col align-bottom bg-white dark:bg-slate-900 rounded-[2.5rem] text-left shadow-2xl transform sm:my-8 sm:align-middle sm:max-w-3xl w-full border border-slate-100 dark:border-slate-800 z-10 max-h-[90vh]"
|
|
||||||
@click.stop>
|
|
||||||
|
|
||||||
<!-- Modal Header -->
|
|
||||||
<div class="px-10 py-8 pb-4 flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight leading-none mb-3">
|
|
||||||
{{ __('New Replenishment') }}
|
|
||||||
</h3>
|
|
||||||
<p class="text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">
|
|
||||||
{{ __('Create and manage replenishment orders from warehouse to machines') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button @click="showModal = false"
|
|
||||||
class="p-2.5 rounded-full bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-all border border-slate-100 dark:border-slate-700 shadow-sm">
|
|
||||||
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto px-10 py-2 custom-scrollbar overflow-x-visible">
|
|
||||||
<form id="replenishmentForm" action="{{ route('admin.warehouses.replenishments.store') }}" method="POST" class="space-y-6 pb-20">
|
|
||||||
@csrf
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div class="space-y-3">
|
|
||||||
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] pl-1">
|
|
||||||
{{ __('Source Warehouse') }} <span class="text-rose-500">*</span>
|
|
||||||
</label>
|
|
||||||
<x-searchable-select
|
|
||||||
name="warehouse_id"
|
|
||||||
:options="$warehouses ?? []"
|
|
||||||
:placeholder="__('Select Warehouse')"
|
|
||||||
class="w-full"
|
|
||||||
required
|
|
||||||
x-model="fromId"
|
|
||||||
@change="fetchStock()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-3">
|
|
||||||
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] pl-1">
|
|
||||||
{{ __('Target Machine') }} <span class="text-rose-500">*</span>
|
|
||||||
</label>
|
|
||||||
<x-searchable-select
|
|
||||||
name="machine_id"
|
|
||||||
:options="$machines->map(fn($m) => (object)['id' => $m->id, 'name' => $m->name . ' (' . $m->serial_no . ')'])"
|
|
||||||
:placeholder="__('Select Machine')"
|
|
||||||
class="w-full"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-3">
|
|
||||||
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] pl-1">
|
|
||||||
{{ __('Note') }}
|
|
||||||
</label>
|
|
||||||
<input type="text" name="note" class="luxury-input w-full px-6 py-4 bg-slate-50/50 dark:bg-slate-900/50"
|
|
||||||
placeholder="{{ __('Optional remarks...') }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Items --}}
|
|
||||||
<div class="space-y-5">
|
|
||||||
<div class="flex items-center justify-between pl-1">
|
|
||||||
<label class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em]">
|
|
||||||
{{ __('Replenishment Items') }} <span class="text-rose-500">*</span>
|
|
||||||
</label>
|
|
||||||
<button type="button" @click="addItem()"
|
|
||||||
class="text-xs font-black text-cyan-500 hover:text-cyan-400 uppercase tracking-widest flex items-center gap-1.5 transition-colors">
|
|
||||||
<svg class="w-4 h-4 stroke-[3]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
||||||
</svg>
|
|
||||||
{{ __('Add Product') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-3">
|
|
||||||
<template x-for="(item, index) in items" :key="index">
|
|
||||||
<div class="group flex items-center gap-3 p-3 bg-slate-50/50 dark:bg-slate-900/30 rounded-2xl border border-slate-100 dark:border-slate-800/50 hover:border-cyan-500/30 transition-all duration-300">
|
|
||||||
<div class="w-20">
|
|
||||||
<input type="number" :name="'items['+index+'][slot_no]'" x-model="item.slot_no" min="1" required
|
|
||||||
class="luxury-input w-full px-3 py-2 text-center text-sm font-mono font-bold" placeholder="{{ __('Slot') }}">
|
|
||||||
</div>
|
|
||||||
<div class="w-px h-6 bg-slate-200 dark:bg-slate-800/50"></div>
|
|
||||||
<div class="flex-1 min-w-[200px]" :key="'select-wrapper-' + index + '-' + (fromId ? 'filtered-' + fromId + '-' + availableProducts.length : 'all')">
|
|
||||||
<select :id="'product-select-' + index + '-' + Date.now()" :name="'items['+index+'][product_id]'" x-model="item.product_id" required
|
|
||||||
data-hs-select='{"placeholder": "{{ __('Select Product') }}","toggleClasses": "hs-select-toggle luxury-select-toggle w-full text-left","toggleTemplate": "<button type=\"button\"><span class=\"text-slate-800 dark:text-slate-200\" data-title></span><div class=\"ms-auto\"><svg class=\"size-4 text-slate-400\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m6 9 6 6 6-6\"/></svg></div></button>","dropdownClasses": "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10 rounded-xl shadow-[0_20px_50px_rgba(0,0,0,0.3)] mt-2 z-[150] max-h-72 overflow-y-auto custom-scrollbar-thin","optionClasses": "hs-select-option py-2 px-3 text-sm text-slate-800 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-cyan-500/10 dark:hover:text-cyan-400 rounded-lg","hasSearch": true,"searchPlaceholder": "{{ __('Search Product') }}","searchClasses": "block w-[calc(100%-16px)] mx-2 py-2 px-3 text-sm border-slate-200 dark:border-white/10 rounded-lg focus:border-cyan-500 focus:ring-cyan-500 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200","searchWrapperClasses": "sticky top-0 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md p-2 z-10","strategy": "fixed"}' class="hidden">
|
|
||||||
<option value="">{{ __('Select Product') }}</option>
|
|
||||||
<template x-for="p in (fromId ? availableProducts : allProducts)" :key="p.id">
|
|
||||||
<option :value="p.id"
|
|
||||||
x-text="p.name + (p.quantity !== undefined ? ' (' + '{{ __('Stock') }}: ' + p.quantity + ')' : '')"
|
|
||||||
:data-title="p.name + (p.quantity !== undefined ? ' (' + '{{ __('Stock') }}: ' + p.quantity + ')' : '')">
|
|
||||||
</option>
|
|
||||||
</template>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="w-px h-6 bg-slate-200 dark:bg-slate-800/50"></div>
|
|
||||||
<div class="w-20">
|
|
||||||
<input type="number" :name="'items['+index+'][quantity]'" x-model="item.quantity" min="1" required
|
|
||||||
class="luxury-input w-full px-3 py-2 text-center text-sm font-mono font-bold" placeholder="{{ __('Qty') }}">
|
|
||||||
</div>
|
|
||||||
<button type="button" @click="removeItem(index)"
|
|
||||||
class="p-2 text-slate-300 hover:text-rose-500 transition-all transform hover:scale-110"
|
|
||||||
x-show="items.length > 1">
|
|
||||||
<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 18 18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal Footer -->
|
|
||||||
<div class="px-10 py-6 border-t border-slate-100 dark:border-slate-800/50 flex items-center justify-end gap-4">
|
|
||||||
<button type="button" @click="showModal = false" class="btn-luxury-ghost px-8">
|
|
||||||
{{ __('Cancel') }}
|
|
||||||
</button>
|
|
||||||
<button type="button" @click="submitReplenishment()"
|
|
||||||
:disabled="loading"
|
|
||||||
class="btn-luxury-primary px-12 relative flex items-center justify-center">
|
|
||||||
<span :class="loading ? 'opacity-0' : ''">{{ __('Create') }}</span>
|
|
||||||
<template x-if="loading">
|
|
||||||
<div class="absolute inset-0 flex items-center justify-center">
|
|
||||||
<svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Details Slide-over -->
|
|
||||||
<div x-show="showDetailsPanel"
|
|
||||||
class="fixed inset-0 z-[120] overflow-hidden"
|
|
||||||
style="display: none;"
|
|
||||||
x-cloak>
|
|
||||||
<div class="absolute inset-0 overflow-hidden">
|
|
||||||
<!-- Backdrop -->
|
|
||||||
<div x-show="showDetailsPanel"
|
|
||||||
x-transition:enter="ease-in-out duration-500" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
|
||||||
x-transition:leave="ease-in-out duration-500" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
|
||||||
class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity"
|
|
||||||
@click="showDetailsPanel = false"></div>
|
|
||||||
|
|
||||||
<div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10">
|
|
||||||
<div x-show="showDetailsPanel"
|
|
||||||
x-transition:enter="transform transition ease-in-out duration-500 sm:duration-700" x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0"
|
|
||||||
x-transition:leave="transform transition ease-in-out duration-500 sm:duration-700" x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full"
|
|
||||||
class="pointer-events-auto w-screen max-w-md">
|
|
||||||
<div class="flex h-full flex-col overflow-y-scroll bg-white dark:bg-slate-900 shadow-2xl border-l border-slate-100 dark:border-slate-800">
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="bg-slate-50/50 dark:bg-slate-900/50 px-6 py-8 border-b border-slate-100 dark:border-slate-800">
|
|
||||||
<div class="flex items-start justify-between">
|
|
||||||
<h2 class="text-xl font-black text-slate-800 dark:text-white font-display tracking-tight leading-none">
|
|
||||||
{{ __('Replenishment Details') }}
|
|
||||||
</h2>
|
|
||||||
<div class="ml-3 flex h-7 items-center">
|
|
||||||
<button type="button" @click="showDetailsPanel = false"
|
|
||||||
class="rounded-full bg-white dark:bg-slate-800 p-2 text-slate-400 hover:text-slate-500 dark:hover:text-slate-300 focus:outline-none border border-slate-100 dark:border-slate-700 shadow-sm transition-all">
|
|
||||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-4">
|
|
||||||
<template x-if="selectedOrder">
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<span class="text-2xl font-black text-cyan-600 dark:text-cyan-400 font-mono tracking-tighter" x-text="selectedOrder.order_no"></span>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em]" x-text="selectedOrder.warehouse_name"></span>
|
|
||||||
<svg class="w-3 h-3 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M13 7l5 5m0 0l-5 5m5-5H6" /></svg>
|
|
||||||
<span class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.2em]" x-text="selectedOrder.machine_name"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Content -->
|
|
||||||
<div class="relative flex-1 px-6 py-8">
|
|
||||||
<!-- Loading Spinner -->
|
|
||||||
<div x-show="detailsLoading" class="absolute inset-0 bg-white/60 dark:bg-slate-900/60 backdrop-blur-[2px] z-10 flex items-center justify-center">
|
|
||||||
<div class="w-10 h-10 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template x-if="selectedOrder">
|
|
||||||
<div class="space-y-10">
|
|
||||||
<!-- Info Grid -->
|
|
||||||
<div class="grid grid-cols-2 gap-6 p-6 bg-slate-50/50 dark:bg-slate-800/30 rounded-[2rem] border border-slate-100 dark:border-slate-800">
|
|
||||||
<div class="space-y-1">
|
|
||||||
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Status') }}</p>
|
|
||||||
<div>
|
|
||||||
@php
|
|
||||||
$statusMap = [
|
|
||||||
'pending' => ['color' => 'amber', 'label' => __('Pending')],
|
|
||||||
'prepared' => ['color' => 'cyan', 'label' => __('Prepared')],
|
|
||||||
'delivering' => ['color' => 'indigo', 'label' => __('Delivering')],
|
|
||||||
'completed' => ['color' => 'emerald', 'label' => __('Completed')],
|
|
||||||
'cancelled' => ['color' => 'slate', 'label' => __('Cancelled')],
|
|
||||||
];
|
|
||||||
@endphp
|
|
||||||
<template x-for="(config, status) in {{ json_encode($statusMap) }}" :key="status">
|
|
||||||
<template x-if="selectedOrder.status === status">
|
|
||||||
<span :class="'inline-flex items-center px-2 py-0.5 rounded-lg text-[10px] font-black bg-' + config.color + '-500/10 text-' + config.color + '-500 border border-' + config.color + '-500/20 uppercase tracking-widest'" x-text="config.label"></span>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Created By') }}</p>
|
|
||||||
<p class="text-xs font-bold text-slate-700 dark:text-slate-300" x-text="selectedOrder.creator_name"></p>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Created At') }}</p>
|
|
||||||
<p class="text-[11px] font-mono font-bold text-slate-600 dark:text-slate-400" x-text="selectedOrder.created_at"></p>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Completed At') }}</p>
|
|
||||||
<p class="text-[11px] font-mono font-bold text-slate-600 dark:text-slate-400" x-text="selectedOrder.completed_at || '-'"></p>
|
|
||||||
</div>
|
|
||||||
<template x-if="selectedOrder.note">
|
|
||||||
<div class="col-span-2 space-y-1 pt-2 border-t border-slate-200/50 dark:border-slate-700/50">
|
|
||||||
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Note') }}</p>
|
|
||||||
<p class="text-xs font-bold text-slate-600 dark:text-slate-400 italic" x-text="selectedOrder.note"></p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Items List -->
|
|
||||||
<div class="space-y-5">
|
|
||||||
<div class="flex items-center justify-between px-1">
|
|
||||||
<h3 class="text-sm font-black text-slate-800 dark:text-white uppercase tracking-widest">{{ __('Replenishment Items') }}</h3>
|
|
||||||
<span class="text-[10px] font-black text-cyan-500 bg-cyan-500/10 px-2 py-0.5 rounded-md uppercase" x-text="orderItems.length + ' {{ __('Items') }}'"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-3">
|
|
||||||
<template x-for="(item, idx) in orderItems" :key="idx">
|
|
||||||
<div class="group flex items-center gap-4 p-4 bg-white dark:bg-slate-900 rounded-2xl border border-slate-100 dark:border-slate-800 hover:border-cyan-500/30 transition-all duration-300">
|
|
||||||
<div class="w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center font-mono font-bold text-slate-500 dark:text-slate-400 border border-slate-200 dark:border-slate-700" x-text="item.slot_no"></div>
|
|
||||||
<div class="w-12 h-12 rounded-xl bg-slate-50 dark:bg-slate-800 flex items-center justify-center overflow-hidden border border-slate-100 dark:border-slate-800 group-hover:scale-105 transition-transform">
|
|
||||||
<template x-if="item.image_url">
|
|
||||||
<img :src="item.image_url" class="w-full h-full object-cover">
|
|
||||||
</template>
|
|
||||||
<template x-if="!item.image_url">
|
|
||||||
<svg class="w-6 h-6 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<p class="text-sm font-black text-slate-800 dark:text-slate-100 truncate tracking-tight" x-text="item.product_name"></p>
|
|
||||||
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ __('Product ID') }}: <span class="text-slate-500 dark:text-slate-400" x-text="item.product_id"></span></p>
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<p class="text-base font-black text-cyan-600 dark:text-cyan-400 font-mono tracking-tighter" x-text="'x' + item.quantity"></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<script>
|
<script>
|
||||||
function transferManager() {
|
function transferManager() {
|
||||||
return {
|
return {
|
||||||
showModal: false,
|
showTransferModal: false,
|
||||||
transferType: 'warehouse_to_warehouse',
|
transferType: 'warehouse_to_warehouse',
|
||||||
fromId: '',
|
fromId: '',
|
||||||
availableProducts: [],
|
availableProducts: [],
|
||||||
@ -14,53 +14,129 @@
|
|||||||
loading: false,
|
loading: false,
|
||||||
|
|
||||||
// Details Panel State
|
// Details Panel State
|
||||||
showDetailsPanel: false,
|
showOrderDetails: false,
|
||||||
selectedOrder: null,
|
activeOrder: null,
|
||||||
orderItems: [],
|
activeItems: [],
|
||||||
loadingDetails: false,
|
detailsLoading: false,
|
||||||
|
|
||||||
|
// Delete State
|
||||||
|
showDeleteModal: false,
|
||||||
|
deleteUrl: '',
|
||||||
|
|
||||||
|
// Confirm State
|
||||||
|
showConfirmModal: false,
|
||||||
|
pendingTransferUrl: '',
|
||||||
|
|
||||||
openCreateModal() {
|
openCreateModal() {
|
||||||
this.items = [{ product_id: '', quantity: 1 }];
|
this.items = [{ product_id: '', quantity: 1 }];
|
||||||
this.showModal = true;
|
this.showTransferModal = true;
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async openDetails(id) {
|
async openOrderDetails(id) {
|
||||||
this.loadingDetails = true;
|
console.log('openOrderDetails triggered for ID:', id);
|
||||||
this.showDetailsPanel = true;
|
this.showOrderDetails = true;
|
||||||
this.selectedOrder = null;
|
this.detailsLoading = true;
|
||||||
this.orderItems = [];
|
this.activeOrder = null;
|
||||||
|
this.activeItems = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/admin/warehouses/transfers/${id}/details`);
|
const response = await fetch(`/admin/warehouses/transfers/${id}/details`, {
|
||||||
const result = await response.json();
|
headers: {
|
||||||
if (result.success) {
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
this.selectedOrder = result.order;
|
'Accept': 'application/json'
|
||||||
this.orderItems = result.items;
|
}
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fetch error:', error);
|
|
||||||
} finally {
|
|
||||||
this.loadingDetails = false;
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit();
|
|
||||||
});
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
this.activeOrder = data.order;
|
||||||
|
this.activeItems = data.items;
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || 'Failed to load details');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('openOrderDetails error:', e);
|
||||||
|
window.showToast?.('{{ __("Failed to load details") }}', 'error');
|
||||||
|
} finally {
|
||||||
|
this.detailsLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmTransfer(url) {
|
||||||
|
this.confirmUrl = url;
|
||||||
|
this.showConfirmModal = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmDelete(url) {
|
||||||
|
this.deleteUrl = url;
|
||||||
|
this.showDeleteModal = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchTabData(url = null) {
|
||||||
|
this.loading = true;
|
||||||
|
const container = document.getElementById('transfers-table-container');
|
||||||
|
let targetUrl = url;
|
||||||
|
|
||||||
|
if (!targetUrl) {
|
||||||
|
const form = document.getElementById('transfer-filter-form');
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const params = new URLSearchParams(formData);
|
||||||
|
targetUrl = `${form.action}?${params.toString()}`;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(targetUrl, window.location.origin);
|
||||||
|
const perPage = urlObj.searchParams.get('per_page');
|
||||||
|
if (perPage) {
|
||||||
|
const hiddenInput = document.querySelector('#transfer-filter-form input[name="per_page"]');
|
||||||
|
if (hiddenInput) hiddenInput.value = perPage;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('URL sync error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(targetUrl, {
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success && container) {
|
||||||
|
container.outerHTML = data.html;
|
||||||
|
// Re-init HSSelect or other Preline components if needed
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (window.HSStaticMethods) window.HSStaticMethods.autoInit();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update URL without reload
|
||||||
|
window.history.pushState({}, '', targetUrl);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
window.showToast?.('{{ __("Loading failed") }}', 'error');
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addItem() {
|
addItem() {
|
||||||
this.items.push({ product_id: '', quantity: 1 });
|
this.items.push({ product_id: '', quantity: 1 });
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
this.updateProductSelects();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
removeItem(i) { if (this.items.length > 1) this.items.splice(i, 1); },
|
removeItem(i) { if (this.items.length > 1) this.items.splice(i, 1); },
|
||||||
|
|
||||||
async fetchStock() {
|
async fetchStock() {
|
||||||
this.availableProducts = [];
|
this.availableProducts = [];
|
||||||
if (!this.fromId || this.fromId.trim() === '') return;
|
if (!this.fromId || this.fromId.trim() === '') {
|
||||||
|
this.$nextTick(() => this.updateProductSelects());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.isLoadingProducts = true;
|
this.isLoadingProducts = true;
|
||||||
try {
|
try {
|
||||||
@ -71,33 +147,96 @@
|
|||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
if (json.success) {
|
if (json.success) {
|
||||||
this.availableProducts = json.data;
|
this.availableProducts = json.data;
|
||||||
|
this.$nextTick(() => this.updateProductSelects());
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoadingProducts = false;
|
this.isLoadingProducts = false;
|
||||||
this.$nextTick(() => {
|
|
||||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateProductSelects() {
|
||||||
|
this.items.forEach((item, index) => {
|
||||||
|
const wrapper = document.getElementById(`product-select-wrapper-${index}`);
|
||||||
|
if (!wrapper) return;
|
||||||
|
|
||||||
|
// 銷毀舊實例
|
||||||
|
const oldSelect = wrapper.querySelector('select');
|
||||||
|
if (oldSelect && window.HSSelect && window.HSSelect.getInstance(oldSelect)) {
|
||||||
|
try { window.HSSelect.getInstance(oldSelect).destroy(); } catch (e) {}
|
||||||
|
}
|
||||||
|
wrapper.innerHTML = '';
|
||||||
|
|
||||||
|
// 構建新 Select
|
||||||
|
const selectEl = document.createElement('select');
|
||||||
|
selectEl.id = `product-select-${index}-${Date.now()}`;
|
||||||
|
selectEl.name = `items[${index}][product_id]`;
|
||||||
|
selectEl.required = true;
|
||||||
|
selectEl.className = 'hidden';
|
||||||
|
|
||||||
|
const products = this.fromId ? this.availableProducts : this.allProducts;
|
||||||
|
|
||||||
|
// Placeholder
|
||||||
|
const placeholderOpt = document.createElement('option');
|
||||||
|
placeholderOpt.value = '';
|
||||||
|
placeholderOpt.textContent = '{{ __("Select Product") }}';
|
||||||
|
selectEl.appendChild(placeholderOpt);
|
||||||
|
|
||||||
|
products.forEach(p => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = p.id;
|
||||||
|
const stockText = p.quantity !== undefined ? ` (${'{{ __("Stock") }}'}: ${p.quantity})` : '';
|
||||||
|
opt.textContent = p.name + stockText;
|
||||||
|
opt.setAttribute('data-title', p.name + stockText);
|
||||||
|
if (item.product_id == p.id) opt.selected = true;
|
||||||
|
selectEl.appendChild(opt);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Preline Config
|
||||||
|
const config = {
|
||||||
|
"placeholder": "{{ __('Select Product') }}",
|
||||||
|
"toggleClasses": "hs-select-toggle luxury-select-toggle w-full text-left",
|
||||||
|
"toggleTemplate": "<button type=\"button\"><span class=\"text-slate-800 dark:text-slate-200\" data-title></span><div class=\"ms-auto\"><svg class=\"size-4 text-slate-400\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m6 9 6 6 6-6\"/></svg></div></button>",
|
||||||
|
"dropdownClasses": "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10 rounded-xl shadow-2xl mt-2 z-[150] max-h-48 overflow-y-auto custom-scrollbar-thin",
|
||||||
|
"optionClasses": "hs-select-option py-2 px-3 text-sm text-slate-800 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-cyan-500/10 dark:hover:text-cyan-400 rounded-lg",
|
||||||
|
"hasSearch": true,
|
||||||
|
"searchPlaceholder": "{{ __('Search Product') }}",
|
||||||
|
"searchClasses": "block w-[calc(100%-16px)] mx-2 py-2 px-3 text-sm border-slate-200 dark:border-white/10 rounded-lg focus:border-cyan-500 focus:ring-cyan-500 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200",
|
||||||
|
"searchWrapperClasses": "sticky top-0 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md p-2 z-10",
|
||||||
|
"strategy": "fixed"
|
||||||
|
};
|
||||||
|
selectEl.setAttribute('data-hs-select', JSON.stringify(config));
|
||||||
|
|
||||||
|
wrapper.appendChild(selectEl);
|
||||||
|
selectEl.addEventListener('change', (e) => {
|
||||||
|
item.product_id = e.target.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Init Preline
|
||||||
|
if (window.HSStaticMethods && window.HSStaticMethods.autoInit) {
|
||||||
|
window.HSStaticMethods.autoInit('select');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
resetFrom() {
|
resetFrom() {
|
||||||
this.fromId = '';
|
this.fromId = '';
|
||||||
this.availableProducts = [];
|
this.availableProducts = [];
|
||||||
this.$nextTick(() => {
|
this.items.forEach(item => item.product_id = ''); // 清除已選商品
|
||||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
this.$nextTick(() => this.updateProductSelects());
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async submitTransfer() {
|
async submitTransfer() {
|
||||||
const form = document.getElementById('transferForm');
|
const form = document.getElementById('transferForm');
|
||||||
const type = this.transferType;
|
const type = this.transferType;
|
||||||
const fromId = form.querySelector(type === 'warehouse_to_warehouse' ? '[name="from_warehouse_id"]' : '[name="from_machine_id"]')?.value;
|
const fromId = this.fromId; // 使用 Alpine 變數更直接
|
||||||
const toId = form.querySelector('[name="to_warehouse_id"]')?.value;
|
const toId = form.querySelector('[name="to_warehouse_id"]')?.value;
|
||||||
|
|
||||||
|
// 前端必填檢查
|
||||||
if (!fromId) {
|
if (!fromId) {
|
||||||
window.showToast?.('{{ __("Please select source") }}', 'error');
|
const label = type === 'warehouse_to_warehouse' ? '{{ __("Source Warehouse") }}' : '{{ __("Source Machine") }}';
|
||||||
|
window.showToast?.('{{ __("Please select") }} ' + label, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!toId) {
|
if (!toId) {
|
||||||
@ -114,15 +253,22 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasError = false;
|
// 檢查每一列商品與數量
|
||||||
this.items.forEach(item => {
|
let itemError = null;
|
||||||
if (!item.product_id || !item.quantity || item.quantity < 1) {
|
for (let i = 0; i < this.items.length; i++) {
|
||||||
hasError = true;
|
const item = this.items[i];
|
||||||
|
if (!item.product_id) {
|
||||||
|
itemError = '{{ __("Please select product for item :num") }}'.replace(':num', i + 1);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
if (!item.quantity || item.quantity < 1) {
|
||||||
|
itemError = '{{ __("Please enter valid quantity for item :num") }}'.replace(':num', i + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (hasError) {
|
if (itemError) {
|
||||||
window.showToast?.('{{ __("Please select product and valid quantity for all items") }}', 'error');
|
window.showToast?.(itemError, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,16 +286,24 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (response.ok) {
|
||||||
window.showToast?.(result.message, 'success');
|
if (result.success) {
|
||||||
this.showModal = false;
|
this.showTransferModal = false;
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
window.showToast?.(result.message || '{{ __("Creation failed") }}', 'error');
|
||||||
|
}
|
||||||
|
} else if (response.status === 422) {
|
||||||
|
// Laravel 驗證錯誤
|
||||||
|
const errors = result.errors || {};
|
||||||
|
const firstError = Object.values(errors)[0]?.[0] || '{{ __("Validation failed") }}';
|
||||||
|
window.showToast?.(firstError, 'error');
|
||||||
} else {
|
} else {
|
||||||
window.showToast?.(result.message || '{{ __("Validation failed") }}', 'error');
|
window.showToast?.(result.message || '{{ __("System error, please try again later") }}', 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
window.showToast?.('{{ __("System Error") }}', 'error');
|
window.showToast?.('{{ __("Connection error or system failure") }}', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
@ -158,7 +312,10 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-3 pb-10" x-data="transferManager()">
|
<div class="space-y-3 pb-10" x-data="transferManager()"
|
||||||
|
@open-details.window="openOrderDetails($event.detail.id)"
|
||||||
|
@ajax:filter.window="fetchTabData()"
|
||||||
|
@ajax:navigate:transfers.window.prevent="fetchTabData($event.detail.url)">
|
||||||
|
|
||||||
{{-- Header --}}
|
{{-- Header --}}
|
||||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
@ -173,15 +330,50 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Table --}}
|
{{-- Table --}}
|
||||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
<div class="luxury-card rounded-3xl p-8 animate-luxury-in relative min-h-[400px]">
|
||||||
<form action="{{ route('admin.warehouses.transfers') }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 mb-8">
|
<!-- Loading Overlay -->
|
||||||
|
<div x-show="loading" 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"
|
||||||
|
class="absolute inset-0 z-20 bg-white/40 dark:bg-slate-900/40 backdrop-blur-[1px] flex flex-col items-center justify-center rounded-3xl"
|
||||||
|
x-cloak>
|
||||||
|
<div class="relative w-16 h-16 mb-4 flex items-center justify-center">
|
||||||
|
<div class="absolute inset-0 rounded-full border-2 border-transparent border-t-cyan-500 border-r-cyan-500/30 animate-spin"></div>
|
||||||
|
<div class="absolute inset-2 rounded-full border border-cyan-500/10 animate-spin" style="animation-duration: 3s; direction: reverse;"></div>
|
||||||
|
<div class="relative w-8 h-8 flex items-center justify-center">
|
||||||
|
<svg class="w-6 h-6 text-cyan-500 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="transfer-filter-form" action="{{ route('admin.warehouses.transfers') }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 mb-8">
|
||||||
|
<input type="hidden" name="per_page" value="{{ request('per_page', 15) }}">
|
||||||
|
|
||||||
|
<div class="relative group flex-1 md:max-w-[240px]">
|
||||||
|
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||||
|
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
|
||||||
|
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<input type="text" name="search_order" value="{{ request('search_order') }}"
|
||||||
|
@keydown.enter.prevent="$dispatch('ajax:filter')"
|
||||||
|
class="py-2.5 pl-12 pr-6 block w-full luxury-input" placeholder="{{ __('Search order number...') }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 md:max-w-[220px]">
|
<div class="flex-1 md:max-w-[220px]">
|
||||||
<x-searchable-select
|
<x-searchable-select
|
||||||
name="type"
|
name="type"
|
||||||
:selected="request('type')"
|
:selected="request('type')"
|
||||||
:placeholder="__('All Types')"
|
:placeholder="__('All Types')"
|
||||||
onchange="this.form.submit()"
|
onchange="this.dispatchEvent(new CustomEvent('ajax:filter', { bubbles: true }))"
|
||||||
>
|
>
|
||||||
|
<option value="" data-title="{{ __('All Types') }}">{{ __('All Types') }}</option>
|
||||||
<option value="warehouse_to_warehouse" {{ request('type') === 'warehouse_to_warehouse' ? 'selected' : '' }} data-title="{{ __('Warehouse to Warehouse') }}">{{ __('Warehouse to Warehouse') }}</option>
|
<option value="warehouse_to_warehouse" {{ request('type') === 'warehouse_to_warehouse' ? 'selected' : '' }} data-title="{{ __('Warehouse to Warehouse') }}">{{ __('Warehouse to Warehouse') }}</option>
|
||||||
<option value="machine_to_warehouse" {{ request('type') === 'machine_to_warehouse' ? 'selected' : '' }} data-title="{{ __('Machine to Warehouse') }}">{{ __('Machine to Warehouse') }}</option>
|
<option value="machine_to_warehouse" {{ request('type') === 'machine_to_warehouse' ? 'selected' : '' }} data-title="{{ __('Machine to Warehouse') }}">{{ __('Machine to Warehouse') }}</option>
|
||||||
</x-searchable-select>
|
</x-searchable-select>
|
||||||
@ -191,89 +383,89 @@
|
|||||||
name="status"
|
name="status"
|
||||||
:selected="request('status')"
|
:selected="request('status')"
|
||||||
:placeholder="__('All Statuses')"
|
:placeholder="__('All Statuses')"
|
||||||
onchange="this.form.submit()"
|
onchange="this.dispatchEvent(new CustomEvent('ajax:filter', { bubbles: true }))"
|
||||||
>
|
>
|
||||||
|
<option value="" data-title="{{ __('All Statuses') }}">{{ __('All Statuses') }}</option>
|
||||||
<option value="draft" {{ request('status') === 'draft' ? 'selected' : '' }} data-title="{{ __('Draft') }}">{{ __('Draft') }}</option>
|
<option value="draft" {{ request('status') === 'draft' ? 'selected' : '' }} data-title="{{ __('Draft') }}">{{ __('Draft') }}</option>
|
||||||
<option value="completed" {{ request('status') === 'completed' ? 'selected' : '' }} data-title="{{ __('Completed') }}">{{ __('Completed') }}</option>
|
<option value="completed" {{ request('status') === 'completed' ? 'selected' : '' }} data-title="{{ __('Completed') }}">{{ __('Completed') }}</option>
|
||||||
<option value="cancelled" {{ request('status') === 'cancelled' ? 'selected' : '' }} data-title="{{ __('Cancelled') }}">{{ __('Cancelled') }}</option>
|
<option value="cancelled" {{ request('status') === 'cancelled' ? 'selected' : '' }} data-title="{{ __('Cancelled') }}">{{ __('Cancelled') }}</option>
|
||||||
</x-searchable-select>
|
</x-searchable-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- Date Range --}}
|
||||||
|
<div class="flex-1 md:max-w-[320px]">
|
||||||
|
<div class="relative group"
|
||||||
|
x-data="{ fp: null }"
|
||||||
|
x-init="fp = flatpickr($refs.dateRange, {
|
||||||
|
mode: 'range',
|
||||||
|
dateFormat: 'Y-m-d H:i',
|
||||||
|
enableTime: true,
|
||||||
|
time_24hr: true,
|
||||||
|
defaultHour: 0,
|
||||||
|
defaultMinute: 0,
|
||||||
|
locale: 'zh_tw',
|
||||||
|
defaultDate: '{{ request('start_date') }}' && '{{ request('end_date') }}' ? ['{{ request('start_date') }}', '{{ request('end_date') }}'] : [],
|
||||||
|
onChange: function(selectedDates, dateStr, instance) {
|
||||||
|
if (selectedDates.length === 2) {
|
||||||
|
$refs.startDate.value = instance.formatDate(selectedDates[0], 'Y-m-d H:i');
|
||||||
|
$refs.endDate.value = instance.formatDate(selectedDates[1], 'Y-m-d H:i');
|
||||||
|
} else if (selectedDates.length === 0) {
|
||||||
|
$refs.startDate.value = '';
|
||||||
|
$refs.endDate.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})">
|
||||||
|
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10 text-slate-400 group-focus-within:text-cyan-500 transition-colors">
|
||||||
|
<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" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
||||||
|
</span>
|
||||||
|
<input type="hidden" name="start_date" x-ref="startDate" value="{{ request('start_date') }}">
|
||||||
|
<input type="hidden" name="end_date" x-ref="endDate" value="{{ request('end_date') }}">
|
||||||
|
<input type="text" x-ref="dateRange"
|
||||||
|
value="{{ request('start_date') && request('end_date') ? request('start_date') . ' 至 ' . request('end_date') : (request('start_date') ?: '') }}"
|
||||||
|
placeholder="{{ __('Select Date Range') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" @click="fetchTabData()" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group" title="{{ __('Search') }}">
|
||||||
|
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button"
|
||||||
|
@click="
|
||||||
|
$el.closest('form').querySelectorAll('input[type=text], input[type=hidden]:not([name=per_page])').forEach(i => i.value = '');
|
||||||
|
$el.closest('form').querySelectorAll('select').forEach(s => {
|
||||||
|
s.value = ' ';
|
||||||
|
const instance = window.HSSelect.getInstance(s);
|
||||||
|
if (instance) instance.setValue(' ');
|
||||||
|
});
|
||||||
|
fetchTabData();
|
||||||
|
"
|
||||||
|
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group" title="{{ __('Reset') }}">
|
||||||
|
<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="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>
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
@include('admin.warehouses.partials.table-transfers')
|
||||||
<table class="w-full text-left border-separate border-spacing-y-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 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 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Type') }}</th>
|
|
||||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('From') }}</th>
|
|
||||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('To') }}</th>
|
|
||||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Items') }}</th>
|
|
||||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 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 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-50 dark:divide-slate-800/80">
|
|
||||||
@forelse($orders as $order)
|
|
||||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
|
||||||
<td class="px-6 py-3.5"><span class="font-mono font-extrabold text-slate-800 dark:text-slate-100">{{ $order->order_no }}</span></td>
|
|
||||||
<td class="px-6 py-5 text-center">
|
|
||||||
@if($order->type === 'warehouse_to_warehouse')
|
|
||||||
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black bg-indigo-500/10 text-indigo-500 border border-indigo-500/20 tracking-widest uppercase">W2W</span>
|
|
||||||
@else
|
|
||||||
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black bg-amber-500/10 text-amber-500 border border-amber-500/20 tracking-widest uppercase">M2W</span>
|
|
||||||
@endif
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-3.5 font-bold text-slate-600 dark:text-slate-300">
|
|
||||||
{{ $order->type === 'warehouse_to_warehouse' ? ($order->fromWarehouse?->name ?? '-') : ($order->fromMachine?->name ?? '-') }}
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-3.5 font-bold text-slate-600 dark:text-slate-300">{{ $order->toWarehouse?->name ?? '-' }}</td>
|
|
||||||
<td class="px-6 py-3.5 text-center font-black text-slate-800 dark:text-white">{{ $order->items->count() }}</td>
|
|
||||||
<td class="px-6 py-3.5 text-center">
|
|
||||||
@php
|
|
||||||
$colors = ['draft' => 'amber', 'completed' => 'emerald', 'cancelled' => 'slate'];
|
|
||||||
$sColor = $colors[$order->status] ?? 'cyan';
|
|
||||||
@endphp
|
|
||||||
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-{{ $sColor }}-500/10 text-{{ $sColor }}-500 border border-{{ $sColor }}-500/20 tracking-widest uppercase">{{ __(ucfirst($order->status)) }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-3.5 text-right flex items-center justify-end gap-2">
|
|
||||||
<button type="button" @click="openDetails({{ $order->id }})" class="p-2 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 transition-all border border-slate-100 dark:border-slate-700" title="{{ __('View Details') }}">
|
|
||||||
<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="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12.a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
@if($order->status === 'draft')
|
|
||||||
<form action="{{ route('admin.warehouses.transfers.confirm', $order->id) }}" method="POST" class="inline" onsubmit="return confirm('{{ __('Confirm this transfer?') }}')">
|
|
||||||
@csrf @method('PATCH')
|
|
||||||
<button type="submit" class="px-4 py-2 rounded-xl text-xs font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 hover:bg-emerald-500 hover:text-white transition-all">{{ __('Confirm') }}</button>
|
|
||||||
</form>
|
|
||||||
@else
|
|
||||||
<span class="text-xs font-bold text-slate-400">{{ $order->completed_at?->format('Y-m-d H:i') }}</span>
|
|
||||||
@endif
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
@empty
|
|
||||||
<tr><td colspan="7" class="px-6 py-20 text-center text-slate-400 font-bold">{{ __('No transfer orders found') }}</td></tr>
|
|
||||||
@endforelse
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">{{ $orders->links('vendor.pagination.luxury') }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- New Transfer Modal --}}
|
{{-- New Transfer Modal --}}
|
||||||
<div x-show="showModal" class="fixed inset-0 z-[110] overflow-y-auto" style="display: none;" role="dialog" aria-modal="true" x-cloak>
|
<div x-show="showTransferModal" class="fixed inset-0 z-[110] overflow-y-auto" style="display: none;" role="dialog" aria-modal="true" x-cloak>
|
||||||
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||||
<!-- Background Backdrop -->
|
<!-- Background Backdrop -->
|
||||||
<div x-show="showModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0"
|
<div x-show="showTransferModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0"
|
||||||
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200"
|
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200"
|
||||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
||||||
class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity"
|
class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity"
|
||||||
@click="showModal = false"></div>
|
@click="showTransferModal = false"></div>
|
||||||
|
|
||||||
<span class="hidden sm:inline-block sm:align-middle sm:min-h-screen" aria-hidden="true">​</span>
|
<span class="hidden sm:inline-block sm:align-middle sm:min-h-screen" aria-hidden="true">​</span>
|
||||||
|
|
||||||
<!-- Modal Panel -->
|
<!-- Modal Panel -->
|
||||||
<div x-show="showModal"
|
<div x-show="showTransferModal"
|
||||||
x-transition:enter="ease-out duration-300"
|
x-transition:enter="ease-out duration-300"
|
||||||
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||||
@ -293,7 +485,7 @@
|
|||||||
{{ __('Create stock transfers between warehouses and machine returns') }}
|
{{ __('Create stock transfers between warehouses and machine returns') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button @click="showModal = false"
|
<button @click="showTransferModal = false"
|
||||||
class="p-2.5 rounded-full bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-all border border-slate-100 dark:border-slate-700 shadow-sm">
|
class="p-2.5 rounded-full bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-all border border-slate-100 dark:border-slate-700 shadow-sm">
|
||||||
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||||
@ -337,7 +529,7 @@
|
|||||||
:placeholder="__('Select Warehouse')"
|
:placeholder="__('Select Warehouse')"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
x-model="fromId"
|
x-model="fromId"
|
||||||
@change="fetchStock()"
|
@change="fromId = $event.target.value; fetchStock()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div x-show="transferType === 'machine_to_warehouse'" class="space-y-3">
|
<div x-show="transferType === 'machine_to_warehouse'" class="space-y-3">
|
||||||
@ -350,7 +542,7 @@
|
|||||||
:placeholder="__('Select Machine')"
|
:placeholder="__('Select Machine')"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
x-model="fromId"
|
x-model="fromId"
|
||||||
@change="fetchStock()"
|
@change="fromId = $event.target.value; fetchStock()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -395,17 +587,13 @@
|
|||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<template x-for="(item, index) in items" :key="index">
|
<template x-for="(item, index) in items" :key="index">
|
||||||
<div class="group flex items-center gap-3 p-3 bg-slate-50/50 dark:bg-slate-900/30 rounded-2xl border border-slate-100 dark:border-slate-800/50 hover:border-cyan-500/30 transition-all duration-300">
|
<div class="group flex items-center gap-3 p-3 bg-slate-50/50 dark:bg-slate-900/30 rounded-2xl border border-slate-100 dark:border-slate-800/50 hover:border-cyan-500/30 transition-all duration-300">
|
||||||
<div class="flex-1 min-w-[200px]" :key="'select-wrapper-' + index + '-' + (fromId ? 'filtered-' + fromId + '-' + availableProducts.length : 'all')">
|
<div class="flex-1 min-w-[200px] relative"
|
||||||
<select :id="'product-select-' + index + '-' + Date.now()" :name="'items['+index+'][product_id]'" x-model="item.product_id" required
|
:id="'product-select-wrapper-' + index"
|
||||||
data-hs-select='{"placeholder": "{{ __('Select Product') }}","toggleClasses": "hs-select-toggle luxury-select-toggle w-full text-left","toggleTemplate": "<button type=\"button\"><span class=\"text-slate-800 dark:text-slate-200\" data-title></span><div class=\"ms-auto\"><svg class=\"size-4 text-slate-400\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m6 9 6 6 6-6\"/></svg></div></button>","dropdownClasses": "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10 rounded-xl shadow-2xl mt-2 z-[150] max-h-48 overflow-y-auto custom-scrollbar-thin","optionClasses": "hs-select-option py-2 px-3 text-sm text-slate-800 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-cyan-500/10 dark:hover:text-cyan-400 rounded-lg","hasSearch": true,"searchPlaceholder": "{{ __('Search Product') }}","searchClasses": "block w-[calc(100%-16px)] mx-2 py-2 px-3 text-sm border-slate-200 dark:border-white/10 rounded-lg focus:border-cyan-500 focus:ring-cyan-500 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200","searchWrapperClasses": "sticky top-0 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md p-2 z-10","strategy": "fixed"}' class="hidden">
|
x-init="$nextTick(() => updateProductSelects())">
|
||||||
<option value="">{{ __('Select Product') }}</option>
|
<!-- 動態渲染商品 Select -->
|
||||||
<template x-for="p in (fromId ? availableProducts : allProducts)" :key="p.id">
|
<div x-show="isLoadingProducts" class="absolute inset-0 bg-white/50 dark:bg-slate-900/50 flex items-center justify-center z-10 rounded-xl">
|
||||||
<option :value="p.id"
|
<div class="w-4 h-4 border-2 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin"></div>
|
||||||
x-text="p.name + (p.quantity !== undefined ? ' (' + '{{ __('Stock') }}: ' + p.quantity + ')' : '')"
|
</div>
|
||||||
:data-title="p.name + (p.quantity !== undefined ? ' (' + '{{ __('Stock') }}: ' + p.quantity + ')' : '')">
|
|
||||||
</option>
|
|
||||||
</template>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="w-px h-6 bg-slate-200 dark:bg-slate-800/50"></div>
|
<div class="w-px h-6 bg-slate-200 dark:bg-slate-800/50"></div>
|
||||||
<div class="flex items-center gap-1 bg-slate-100/50 dark:bg-slate-900/50 p-1 rounded-xl border border-slate-200/50 dark:border-slate-800/50">
|
<div class="flex items-center gap-1 bg-slate-100/50 dark:bg-slate-900/50 p-1 rounded-xl border border-slate-200/50 dark:border-slate-800/50">
|
||||||
@ -437,7 +625,7 @@
|
|||||||
|
|
||||||
<!-- Modal Footer -->
|
<!-- Modal Footer -->
|
||||||
<div class="px-10 py-6 border-t border-slate-100 dark:border-slate-800/50 flex items-center justify-end gap-4">
|
<div class="px-10 py-6 border-t border-slate-100 dark:border-slate-800/50 flex items-center justify-end gap-4">
|
||||||
<button type="button" @click="showModal = false" class="btn-luxury-ghost px-8">
|
<button type="button" @click="showTransferModal = false" class="btn-luxury-ghost px-8">
|
||||||
{{ __('Cancel') }}
|
{{ __('Cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" @click="submitTransfer()"
|
<button type="button" @click="submitTransfer()"
|
||||||
@ -456,142 +644,290 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{-- Details Offcanvas --}}
|
</div>
|
||||||
<div x-show="showDetailsPanel"
|
{{-- Details Slide-over --}}
|
||||||
class="fixed inset-0 z-[120] overflow-hidden"
|
<div x-show="showOrderDetails" class="fixed inset-0 z-[120] overflow-hidden" style="display: none;" x-cloak>
|
||||||
style="display: none;"
|
|
||||||
x-cloak>
|
|
||||||
<div class="absolute inset-0 overflow-hidden">
|
<div class="absolute inset-0 overflow-hidden">
|
||||||
<div class="absolute inset-0 bg-slate-900/40 backdrop-blur-sm transition-opacity"
|
<!-- Backdrop -->
|
||||||
@click="showDetailsPanel = false"
|
<div x-show="showOrderDetails"
|
||||||
x-show="showDetailsPanel"
|
x-transition:enter="ease-in-out duration-500" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||||
x-transition:enter="ease-in-out duration-500"
|
x-transition:leave="ease-in-out duration-500" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
||||||
x-transition:enter-start="opacity-0"
|
class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity"
|
||||||
x-transition:enter-end="opacity-100"
|
@click="showOrderDetails = false"></div>
|
||||||
x-transition:leave="ease-in-out duration-500"
|
|
||||||
x-transition:leave-start="opacity-100"
|
|
||||||
x-transition:leave-end="opacity-0"></div>
|
|
||||||
|
|
||||||
<div class="fixed inset-y-0 right-0 flex max-w-full pl-10">
|
<div class="fixed inset-y-0 right-0 max-w-full flex">
|
||||||
<div class="w-screen max-w-md transform transition duration-500 ease-in-out sm:max-w-lg"
|
<div x-show="showOrderDetails"
|
||||||
x-show="showDetailsPanel"
|
x-transition:enter="transform transition ease-in-out duration-500 sm:duration-700" x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0"
|
||||||
x-transition:enter="transform transition ease-in-out duration-500 sm:duration-700"
|
x-transition:leave="transform transition ease-in-out duration-500 sm:duration-700" x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full"
|
||||||
x-transition:enter-start="translate-x-full"
|
class="w-screen max-w-2xl">
|
||||||
x-transition:enter-end="translate-x-0"
|
|
||||||
x-transition:leave="transform transition ease-in-out duration-500 sm:duration-700"
|
|
||||||
x-transition:leave-start="translate-x-0"
|
|
||||||
x-transition:leave-end="translate-x-full">
|
|
||||||
|
|
||||||
<div class="flex h-full flex-col bg-white dark:bg-slate-900 shadow-2xl border-l border-slate-100 dark:border-slate-800">
|
<div class="h-full flex flex-col bg-white dark:bg-slate-900 shadow-2xl border-l border-slate-100 dark:border-slate-800">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="px-8 py-8 bg-slate-50/50 dark:bg-slate-900/50 border-b border-slate-100 dark:border-slate-800">
|
<div class="px-5 py-6 sm:px-8 border-b border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50">
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between gap-4">
|
||||||
<div>
|
<div class="min-w-0 flex-1">
|
||||||
<h2 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight leading-none mb-2" x-text="selectedOrder ? selectedOrder.order_no : '{{ __('Loading...') }}'"></h2>
|
<h2 class="text-xl sm:text-2xl font-black text-slate-800 dark:text-white font-display flex items-center gap-2 sm:gap-3">
|
||||||
<p class="text-xs font-bold text-slate-400 uppercase tracking-[0.2em]">{{ __('Transfer Details') }}</p>
|
<svg class="w-5 h-5 sm:w-6 sm:h-6 text-cyan-500 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M16 16v1a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2m5.66 0H14a2 2 0 0 1 2 2v3.34" />
|
||||||
|
<path d="M11 9.66V7a2 2 0 0 0-2-2H5.66" />
|
||||||
|
<path d="m20 21-5-5" />
|
||||||
|
<path d="m15 21 5-5" />
|
||||||
|
<path d="M12 13H7" />
|
||||||
|
<path d="M12 17H7" />
|
||||||
|
<path d="M17 12V7a2 2 0 0 0-2-2h-5" />
|
||||||
|
</svg>
|
||||||
|
<span class="truncate">{{ __('Transfer Details') }}</span>
|
||||||
|
</h2>
|
||||||
|
<template x-if="activeOrder">
|
||||||
|
<div class="mt-2 flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 font-bold uppercase tracking-widest">
|
||||||
|
<span x-text="activeOrder.order_no" class="text-cyan-600 dark:text-cyan-400 font-mono tracking-tighter text-lg"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<button @click="showDetailsPanel = false" class="p-2 rounded-full hover:bg-white dark:hover:bg-slate-800 text-slate-400 transition-all">
|
<button type="button" @click="showOrderDetails = false"
|
||||||
<svg class="w-6 h-6 stroke-[2]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
class="bg-white dark:bg-slate-800 rounded-full p-2 text-slate-400 hover:text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700 focus:outline-none transition duration-300 shadow-sm border border-slate-200 dark:border-slate-700">
|
||||||
|
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="flex-1 overflow-y-auto custom-scrollbar p-8">
|
<div class="flex-1 overflow-y-auto p-6 sm:p-8">
|
||||||
<template x-if="loadingDetails">
|
<div class="relative min-h-[200px]">
|
||||||
<div class="flex flex-col items-center justify-center h-64 space-y-4">
|
<!-- Loading State -->
|
||||||
<div class="w-12 h-12 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin"></div>
|
<div x-show="detailsLoading"
|
||||||
<p class="text-sm font-bold text-slate-400 animate-pulse">{{ __('Fetching data...') }}</p>
|
class="absolute inset-0 z-50 flex flex-col items-center justify-center bg-white/40 dark:bg-slate-900/40 backdrop-blur-[1px]">
|
||||||
|
<div class="w-10 h-10 border-2 border-cyan-500 border-t-transparent rounded-full animate-spin"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="!loadingDetails && selectedOrder">
|
<template x-if="activeOrder">
|
||||||
<div class="space-y-8 animate-luxury-in">
|
<div class="space-y-10">
|
||||||
<!-- Info Cards -->
|
<!-- Transfer Flow -->
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="flex items-center gap-4 justify-center">
|
||||||
<div class="p-4 rounded-2xl bg-slate-50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-800">
|
<div class="flex-1 text-center p-6 bg-slate-50 dark:bg-slate-800/50 rounded-3xl border border-slate-100 dark:border-slate-800 shadow-sm transition-all duration-300 hover:shadow-md">
|
||||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Status') }}</p>
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2">{{ __('Source') }}</p>
|
||||||
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black tracking-widest uppercase"
|
<p class="text-sm font-black text-slate-800 dark:text-white" x-text="activeOrder.from_name"></p>
|
||||||
:class="{
|
|
||||||
'bg-amber-500/10 text-amber-500 border border-amber-500/20': selectedOrder.status === 'draft',
|
|
||||||
'bg-emerald-500/10 text-emerald-500 border border-emerald-500/20': selectedOrder.status === 'completed',
|
|
||||||
'bg-slate-500/10 text-slate-500 border border-slate-500/20': selectedOrder.status === 'cancelled'
|
|
||||||
}"
|
|
||||||
x-text="selectedOrder.status"></span>
|
|
||||||
</div>
|
|
||||||
<div class="p-4 rounded-2xl bg-slate-50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-800">
|
|
||||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Type') }}</p>
|
|
||||||
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black tracking-widest uppercase"
|
|
||||||
:class="selectedOrder.type === 'warehouse_to_warehouse' ? 'bg-indigo-500/10 text-indigo-500' : 'bg-amber-500/10 text-amber-500'"
|
|
||||||
x-text="selectedOrder.type === 'warehouse_to_warehouse' ? 'W2W' : 'M2W'"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="flex-1 p-4 rounded-2xl bg-slate-50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-800">
|
|
||||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('From') }}</p>
|
|
||||||
<p class="font-bold text-slate-800 dark:text-white" x-text="selectedOrder.from_name || '-'"></p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-shrink-0 text-slate-300">
|
<div class="flex-shrink-0 w-12 h-12 flex items-center justify-center bg-cyan-500 text-white rounded-2xl shadow-lg shadow-cyan-500/20 animate-pulse">
|
||||||
<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="M14 5l7 7m0 0l-7 7m7-7H3" /></svg>
|
<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="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 p-4 rounded-2xl bg-slate-50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-800">
|
<div class="flex-1 text-center p-6 bg-slate-50 dark:bg-slate-800/50 rounded-3xl border border-slate-100 dark:border-slate-800 shadow-sm transition-all duration-300 hover:shadow-md">
|
||||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('To') }}</p>
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2">{{ __('Target') }}</p>
|
||||||
<p class="font-bold text-slate-800 dark:text-white" x-text="selectedOrder.to_warehouse_name || '-'"></p>
|
<p class="text-sm font-black text-slate-800 dark:text-white" x-text="activeOrder.to_name"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-3">
|
<!-- Info Grid -->
|
||||||
<h4 class="text-xs font-black text-slate-400 uppercase tracking-[0.2em] pl-1">{{ __('Order Items') }}</h4>
|
<div class="grid grid-cols-2 gap-8 p-8 bg-slate-50/50 dark:bg-slate-800/30 rounded-[2rem] border border-slate-100 dark:border-slate-800">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<template x-for="item in orderItems" :key="item.product_id">
|
<p class="text-xs font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Status') }}</p>
|
||||||
<div class="flex items-center gap-4 p-4 bg-slate-50 dark:bg-slate-800/30 rounded-2xl border border-slate-100 dark:border-slate-800/50">
|
<div>
|
||||||
<div class="w-12 h-12 rounded-xl bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 p-1 overflow-hidden flex-shrink-0">
|
<span class="inline-flex items-center px-2 py-0.5 rounded-lg text-xs font-black tracking-widest uppercase border"
|
||||||
<img :src="item.image_url || '/images/placeholder-product.png'" class="w-full h-full object-cover rounded-lg" alt="">
|
:class="{
|
||||||
</div>
|
'bg-amber-500/10 text-amber-500 border-amber-500/20': activeOrder.status === 'draft',
|
||||||
<div class="flex-1 min-w-0">
|
'bg-emerald-500/10 text-emerald-500 border-emerald-500/20': activeOrder.status === 'completed',
|
||||||
<p class="text-sm font-bold text-slate-800 dark:text-slate-200 truncate" x-text="item.product_name"></p>
|
'bg-slate-500/10 text-slate-500 border-slate-500/20': activeOrder.status === 'cancelled'
|
||||||
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest" x-text="'ID: ' + item.product_id"></p>
|
}"
|
||||||
</div>
|
x-text="{
|
||||||
<div class="text-right">
|
'draft': '{{ __('Draft') }}',
|
||||||
<p class="text-sm font-black text-slate-800 dark:text-white" x-text="'x' + item.quantity"></p>
|
'completed': '{{ __('Completed') }}',
|
||||||
</div>
|
'cancelled': '{{ __('Cancelled') }}'
|
||||||
|
}[activeOrder.status] || activeOrder.status"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p class="text-xs font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Transfer Type') }}</p>
|
||||||
|
<div>
|
||||||
|
<span class="inline-flex items-center px-2 py-0.5 rounded-lg text-xs font-black tracking-widest uppercase border border-slate-200 dark:border-slate-700 bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300"
|
||||||
|
x-text="{
|
||||||
|
'warehouse_to_warehouse': '{{ __('Warehouse to Warehouse') }}',
|
||||||
|
'machine_to_warehouse': '{{ __('Machine to Warehouse') }}'
|
||||||
|
}[activeOrder.type] || activeOrder.type"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p class="text-xs font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Created By') }}</p>
|
||||||
|
<p class="text-sm font-bold text-slate-700 dark:text-slate-300" x-text="activeOrder.creator_name"></p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p class="text-xs font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Created At') }}</p>
|
||||||
|
<p class="text-sm font-mono font-bold text-slate-600 dark:text-slate-400" x-text="activeOrder.created_at"></p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p class="text-xs font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Completed At') }}</p>
|
||||||
|
<p class="text-sm font-mono font-bold text-slate-600 dark:text-slate-400" x-text="activeOrder.completed_at || '-'"></p>
|
||||||
|
</div>
|
||||||
|
<template x-if="activeOrder.note">
|
||||||
|
<div class="col-span-2 space-y-1 pt-2 border-t border-slate-200/50 dark:border-slate-700/50">
|
||||||
|
<p class="text-xs font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Note') }}</p>
|
||||||
|
<p class="text-xs font-bold text-slate-600 dark:text-slate-400 italic" x-text="activeOrder.note"></p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-6 rounded-3xl bg-slate-50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-800 space-y-4">
|
<!-- Items List -->
|
||||||
<div class="flex justify-between items-center">
|
<div class="space-y-5">
|
||||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Created By') }}</p>
|
<div class="flex items-center justify-between px-1">
|
||||||
<p class="text-xs font-bold text-slate-600 dark:text-slate-300" x-text="selectedOrder.creator_name"></p>
|
<h3 class="text-sm font-black text-slate-800 dark:text-white uppercase tracking-widest">{{ __('Product Details') }}</h3>
|
||||||
</div>
|
<span class="text-[10px] font-black text-cyan-500 bg-cyan-500/10 px-2 py-0.5 rounded-md uppercase" x-text="activeItems.length + ' {{ __('Items') }}'"></span>
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Created At') }}</p>
|
|
||||||
<p class="text-xs font-bold text-slate-600 dark:text-slate-300" x-text="selectedOrder.created_at"></p>
|
|
||||||
</div>
|
|
||||||
<template x-if="selectedOrder.completed_at">
|
|
||||||
<div class="flex justify-between items-center pt-2 border-t border-slate-200 dark:border-slate-700">
|
|
||||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Completed At') }}</p>
|
|
||||||
<p class="text-xs font-bold text-emerald-500" x-text="selectedOrder.completed_at"></p>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
<template x-if="selectedOrder.note">
|
<div class="space-y-3">
|
||||||
<div class="pt-2 border-t border-slate-200 dark:border-slate-700">
|
<template x-for="(item, idx) in activeItems" :key="idx">
|
||||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Note') }}</p>
|
<div class="luxury-card p-4 rounded-2xl flex items-center gap-4 group/item transition-all hover:border-cyan-500/30">
|
||||||
<p class="text-xs font-bold text-slate-600 dark:text-slate-300 italic" x-text="selectedOrder.note"></p>
|
<div class="w-16 h-16 rounded-xl bg-slate-100 dark:bg-slate-800 flex-shrink-0 overflow-hidden border border-slate-200 dark:border-slate-700 group-hover:scale-105 transition-transform">
|
||||||
|
<template x-if="item.image_url">
|
||||||
|
<img :src="item.image_url" class="w-full h-full object-cover">
|
||||||
|
</template>
|
||||||
|
<template x-if="!item.image_url">
|
||||||
|
<div class="w-full h-full flex items-center justify-center text-slate-400">
|
||||||
|
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-base font-extrabold text-slate-800 dark:text-white truncate" x-text="item.product_name"></p>
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<span class="text-xs font-black text-slate-400 uppercase tracking-widest">{{ __('Product ID') }}:</span>
|
||||||
|
<span class="text-xs font-mono font-bold text-cyan-600 dark:text-cyan-400" x-text="item.product_id"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<p class="text-2xl font-black text-cyan-600 dark:text-cyan-400 font-mono tracking-tighter" x-text="'x' + item.quantity"></p>
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Quantity') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- Confirmation Modal --}}
|
||||||
|
<template x-teleport="body">
|
||||||
|
<div x-show="showConfirmModal" class="fixed inset-0 z-[200] overflow-y-auto" x-cloak>
|
||||||
|
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<div x-show="showConfirmModal" x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||||
|
x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100"
|
||||||
|
x-transition:leave-end="opacity-0"
|
||||||
|
class="fixed inset-0 transition-opacity bg-slate-900/60 backdrop-blur-sm"
|
||||||
|
@click="showConfirmModal = false"></div>
|
||||||
|
|
||||||
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
||||||
|
|
||||||
|
<div x-show="showConfirmModal" x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 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 translate-y-0 sm:scale-100"
|
||||||
|
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
class="inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white dark:bg-slate-900 rounded-3xl shadow-2xl sm:my-8 sm:align-middle sm:max-w-md sm:w-full sm:p-8 border border-slate-100 dark:border-slate-800">
|
||||||
|
|
||||||
|
<div class="sm:flex sm:items-start text-center sm:text-left">
|
||||||
|
<div class="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto bg-emerald-100 dark:bg-emerald-500/10 rounded-2xl sm:mx-0 sm:h-12 sm:w-12 text-emerald-600 dark:text-emerald-400">
|
||||||
|
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 sm:mt-0 sm:ml-6">
|
||||||
|
<h3 class="text-xl font-black text-slate-800 dark:text-white leading-6 tracking-tight font-display uppercase">
|
||||||
|
{{ __('Confirm Transfer Order') }}
|
||||||
|
</h3>
|
||||||
|
<div class="mt-4">
|
||||||
|
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 leading-relaxed">
|
||||||
|
{{ __('Are you sure you want to confirm this transfer? This will deduct stock from source and add to target.') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-8 sm:mt-10 sm:flex sm:flex-row-reverse gap-3">
|
||||||
|
<form :action="confirmUrl" method="POST" class="inline">
|
||||||
|
@csrf
|
||||||
|
@method('PATCH')
|
||||||
|
<button type="submit"
|
||||||
|
class="inline-flex justify-center w-full px-6 py-3 text-sm font-black text-white transition-all bg-emerald-500 rounded-xl hover:bg-emerald-600 shadow-lg shadow-emerald-200 dark:shadow-none hover:scale-[1.02] active:scale-[0.98] sm:w-auto uppercase tracking-widest font-display">
|
||||||
|
{{ __('Confirm Transfer') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<button type="button" @click="showConfirmModal = false"
|
||||||
|
class="inline-flex justify-center w-full px-6 py-3 mt-3 text-sm font-black text-slate-700 dark:text-slate-200 transition-all bg-slate-100 dark:bg-slate-800 rounded-xl hover:bg-slate-200 dark:hover:bg-slate-700 sm:mt-0 sm:w-auto uppercase tracking-widest font-display">
|
||||||
|
{{ __('Cancel') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
{{-- Delete Confirmation Modal --}}
|
||||||
|
<template x-teleport="body">
|
||||||
|
<div x-show="showDeleteModal" class="fixed inset-0 z-[200] overflow-y-auto" x-cloak>
|
||||||
|
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<div x-show="showDeleteModal" x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||||
|
x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100"
|
||||||
|
x-transition:leave-end="opacity-0"
|
||||||
|
class="fixed inset-0 transition-opacity bg-slate-900/60 backdrop-blur-sm"
|
||||||
|
@click="showDeleteModal = false"></div>
|
||||||
|
|
||||||
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
||||||
|
|
||||||
|
<div x-show="showDeleteModal" x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 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 translate-y-0 sm:scale-100"
|
||||||
|
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
class="inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white dark:bg-slate-900 rounded-3xl shadow-2xl sm:my-8 sm:align-middle sm:max-w-md sm:w-full sm:p-8 border border-slate-100 dark:border-slate-800">
|
||||||
|
|
||||||
|
<div class="sm:flex sm:items-start text-center sm:text-left">
|
||||||
|
<div class="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto bg-amber-100 dark:bg-amber-500/10 rounded-2xl sm:mx-0 sm:h-12 sm:w-12 text-amber-600 dark:text-amber-400">
|
||||||
|
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 sm:mt-0 sm:ml-6">
|
||||||
|
<h3 class="text-xl font-black text-slate-800 dark:text-white leading-6 tracking-tight font-display uppercase">
|
||||||
|
{{ __('Confirm Deletion') }}
|
||||||
|
</h3>
|
||||||
|
<div class="mt-4">
|
||||||
|
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 leading-relaxed">
|
||||||
|
{{ __('Are you sure you want to delete this draft order? This action cannot be undone.') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-8 sm:mt-10 sm:flex sm:flex-row-reverse gap-3">
|
||||||
|
<form :action="deleteUrl" method="POST" class="inline">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit"
|
||||||
|
class="inline-flex justify-center w-full px-6 py-3 text-sm font-black text-white transition-all bg-rose-500 rounded-xl hover:bg-rose-600 shadow-lg shadow-rose-200 dark:shadow-none hover:scale-[1.02] active:scale-[0.98] sm:w-auto uppercase tracking-widest font-display">
|
||||||
|
{{ __('Delete Permanently') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<button type="button" @click="showDeleteModal = false"
|
||||||
|
class="inline-flex justify-center w-full px-6 py-3 mt-3 text-sm font-black text-slate-700 dark:text-slate-200 transition-all bg-slate-100 dark:bg-slate-800 rounded-xl hover:bg-slate-200 dark:hover:bg-slate-700 sm:mt-0 sm:w-auto uppercase tracking-widest font-display">
|
||||||
|
{{ __('Cancel') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@ -95,6 +95,7 @@
|
|||||||
}"
|
}"
|
||||||
@click.outside="open = false"
|
@click.outside="open = false"
|
||||||
@keydown.escape.window="open = false"
|
@keydown.escape.window="open = false"
|
||||||
|
@multi-select-reset.window="clear()"
|
||||||
{{ $attributes->except(['class', 'options', 'selected', 'placeholder', 'id', 'name', 'hasSearch', 'no-auto-submit']) }}>
|
{{ $attributes->except(['class', 'options', 'selected', 'placeholder', 'id', 'name', 'hasSearch', 'no-auto-submit']) }}>
|
||||||
|
|
||||||
{{-- 隱藏 input 送出表單用 --}}
|
{{-- 隱藏 input 送出表單用 --}}
|
||||||
|
|||||||
@ -24,6 +24,9 @@ x-init="
|
|||||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message, type } }));
|
window.dispatchEvent(new CustomEvent('toast', { detail: { message, type } }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
window.showToast = (message, type = 'success') => {
|
||||||
|
window.Alpine.store('toast').show(message, type);
|
||||||
|
};
|
||||||
@if(session('success')) add('{{ addslashes(session('success')) }}', 'success'); @endif
|
@if(session('success')) add('{{ addslashes(session('success')) }}', 'success'); @endif
|
||||||
@if(session('error')) add('{{ addslashes(session('error')) }}', 'error'); @endif
|
@if(session('error')) add('{{ addslashes(session('error')) }}', 'error'); @endif
|
||||||
@foreach($allErrors as $error) add('{{ addslashes($error) }}', 'error'); @endforeach
|
@foreach($allErrors as $error) add('{{ addslashes($error) }}', 'error'); @endforeach
|
||||||
|
|||||||
@ -9,8 +9,9 @@
|
|||||||
@php
|
@php
|
||||||
$currentLimit = $paginator->perPage();
|
$currentLimit = $paginator->perPage();
|
||||||
$limits = [10, 25, 50, 100];
|
$limits = [10, 25, 50, 100];
|
||||||
|
$event = $ajax_navigate_event ?? 'ajax:navigate';
|
||||||
@endphp
|
@endphp
|
||||||
<select onchange="{ const params = new URLSearchParams(window.location.search); params.set('per_page', this.value); params.delete('page'); const url = window.location.pathname + '?' + params.toString(); const evt = new CustomEvent('ajax:navigate', { detail: { url: url }, bubbles: true, cancelable: true }); if (this.dispatchEvent(evt)) { window.location.href = url; } }"
|
<select onchange="{ const params = new URLSearchParams(window.location.search); params.set('per_page', this.value); params.delete('page'); const url = window.location.pathname + '?' + params.toString(); const evt = new CustomEvent('{{ $event }}', { detail: { url: url }, bubbles: true, cancelable: true }); if (this.dispatchEvent(evt)) { window.location.href = url; } }"
|
||||||
class="h-7 pl-2 pr-7 rounded-lg bg-white dark:bg-slate-800 border-none text-[11px] font-black text-slate-600 dark:text-slate-300 appearance-none focus:ring-4 focus:ring-cyan-500/10 outline-none transition-all cursor-pointer shadow-sm leading-none py-0 !bg-none">
|
class="h-7 pl-2 pr-7 rounded-lg bg-white dark:bg-slate-800 border-none text-[11px] font-black text-slate-600 dark:text-slate-300 appearance-none focus:ring-4 focus:ring-cyan-500/10 outline-none transition-all cursor-pointer shadow-sm leading-none py-0 !bg-none">
|
||||||
@foreach($limits as $l)
|
@foreach($limits as $l)
|
||||||
<option value="{{ $l }}" {{ $currentLimit == $l ? 'selected' : '' }}>{{ $l }}</option>
|
<option value="{{ $l }}" {{ $currentLimit == $l ? 'selected' : '' }}>{{ $l }}</option>
|
||||||
@ -44,7 +45,7 @@
|
|||||||
</span>
|
</span>
|
||||||
@else
|
@else
|
||||||
<a href="{{ $paginator->previousPageUrl() }}"
|
<a href="{{ $paginator->previousPageUrl() }}"
|
||||||
onclick="{ event.preventDefault(); const url = this.href; const evt = new CustomEvent('ajax:navigate', { detail: { url: url }, bubbles: true, cancelable: true }); if (this.dispatchEvent(evt)) { window.location.href = url; } }"
|
onclick="{ event.preventDefault(); const url = this.href; const evt = new CustomEvent('{{ $event }}', { detail: { url: url }, bubbles: true, cancelable: true }); if (this.dispatchEvent(evt)) { window.location.href = url; } }"
|
||||||
class="h-9 inline-flex items-center gap-x-1.5 sm:gap-x-2 px-3 sm:px-4 rounded-xl text-[10px] sm:text-xs font-black bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 transition-all shadow-sm group">
|
class="h-9 inline-flex items-center gap-x-1.5 sm:gap-x-2 px-3 sm:px-4 rounded-xl text-[10px] sm:text-xs font-black bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 transition-all shadow-sm group">
|
||||||
<svg class="size-3.5 sm:size-4 text-cyan-500 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M15 19l-7-7 7-7"/></svg>
|
<svg class="size-3.5 sm:size-4 text-cyan-500 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M15 19l-7-7 7-7"/></svg>
|
||||||
<span class="hidden xxs:inline">{{ __('Previous') }}</span>
|
<span class="hidden xxs:inline">{{ __('Previous') }}</span>
|
||||||
@ -53,7 +54,7 @@
|
|||||||
|
|
||||||
{{-- Unified Quick Jump Selection (Desktop & Mobile) --}}
|
{{-- Unified Quick Jump Selection (Desktop & Mobile) --}}
|
||||||
<div class="relative group">
|
<div class="relative group">
|
||||||
<select onchange="{ const url = this.value; const evt = new CustomEvent('ajax:navigate', { detail: { url: url }, bubbles: true, cancelable: true }); if (this.dispatchEvent(evt)) { window.location.href = url; } }"
|
<select onchange="{ const url = this.value; const evt = new CustomEvent('{{ $event }}', { detail: { url: url }, bubbles: true, cancelable: true }); if (this.dispatchEvent(evt)) { window.location.href = url; } }"
|
||||||
class="h-9 pl-4 pr-10 rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 text-[11px] sm:text-xs font-black text-slate-600 dark:text-slate-300 appearance-none focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 outline-none transition-all cursor-pointer shadow-sm hover:border-slate-300 dark:hover:border-slate-600 !bg-none {{ $paginator->lastPage() <= 1 ? 'opacity-50 cursor-not-allowed pointer-events-none' : '' }}"
|
class="h-9 pl-4 pr-10 rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 text-[11px] sm:text-xs font-black text-slate-600 dark:text-slate-300 appearance-none focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 outline-none transition-all cursor-pointer shadow-sm hover:border-slate-300 dark:hover:border-slate-600 !bg-none {{ $paginator->lastPage() <= 1 ? 'opacity-50 cursor-not-allowed pointer-events-none' : '' }}"
|
||||||
{{ $paginator->lastPage() <= 1 ? 'disabled' : '' }}>
|
{{ $paginator->lastPage() <= 1 ? 'disabled' : '' }}>
|
||||||
@for ($i = 1; $i <= $paginator->lastPage(); $i++)
|
@for ($i = 1; $i <= $paginator->lastPage(); $i++)
|
||||||
@ -70,7 +71,7 @@
|
|||||||
{{-- Next Page Link --}}
|
{{-- Next Page Link --}}
|
||||||
@if ($paginator->hasMorePages())
|
@if ($paginator->hasMorePages())
|
||||||
<a href="{{ $paginator->nextPageUrl() }}"
|
<a href="{{ $paginator->nextPageUrl() }}"
|
||||||
onclick="{ event.preventDefault(); const url = this.href; const evt = new CustomEvent('ajax:navigate', { detail: { url: url }, bubbles: true, cancelable: true }); if (this.dispatchEvent(evt)) { window.location.href = url; } }"
|
onclick="{ event.preventDefault(); const url = this.href; const evt = new CustomEvent('{{ $event }}', { detail: { url: url }, bubbles: true, cancelable: true }); if (this.dispatchEvent(evt)) { window.location.href = url; } }"
|
||||||
class="h-9 inline-flex items-center gap-x-1.5 sm:gap-x-2 px-3 sm:px-4 rounded-xl text-[10px] sm:text-xs font-black bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 transition-all shadow-sm group">
|
class="h-9 inline-flex items-center gap-x-1.5 sm:gap-x-2 px-3 sm:px-4 rounded-xl text-[10px] sm:text-xs font-black bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 transition-all shadow-sm group">
|
||||||
<span class="hidden xxs:inline">{{ __('Next') }}</span>
|
<span class="hidden xxs:inline">{{ __('Next') }}</span>
|
||||||
<svg class="size-3.5 sm:size-4 text-cyan-500 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M9 5l7 7-7 7"/></svg>
|
<svg class="size-3.5 sm:size-4 text-cyan-500 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M9 5l7 7-7 7"/></svg>
|
||||||
|
|||||||
@ -84,13 +84,15 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
|
|||||||
Route::get('/inventory', [App\Http\Controllers\Admin\WarehouseController::class, 'inventory'])->name('inventory');
|
Route::get('/inventory', [App\Http\Controllers\Admin\WarehouseController::class, 'inventory'])->name('inventory');
|
||||||
Route::post('/inventory/stock-in', [App\Http\Controllers\Admin\WarehouseController::class, 'storeStockIn'])->name('inventory.stock-in.store');
|
Route::post('/inventory/stock-in', [App\Http\Controllers\Admin\WarehouseController::class, 'storeStockIn'])->name('inventory.stock-in.store');
|
||||||
Route::patch('/inventory/stock-in/{stockInOrder}/confirm', [App\Http\Controllers\Admin\WarehouseController::class, 'confirmStockIn'])->name('inventory.stock-in.confirm');
|
Route::patch('/inventory/stock-in/{stockInOrder}/confirm', [App\Http\Controllers\Admin\WarehouseController::class, 'confirmStockIn'])->name('inventory.stock-in.confirm');
|
||||||
|
Route::delete('/inventory/stock-in/{stockInOrder}', [App\Http\Controllers\Admin\WarehouseController::class, 'destroyStockIn'])->name('inventory.stock-in.destroy');
|
||||||
Route::get('/inventory/stock-in/{order}/details', [App\Http\Controllers\Admin\WarehouseController::class, 'stockInOrderDetails'])->name('inventory.stock-in.details');
|
Route::get('/inventory/stock-in/{order}/details', [App\Http\Controllers\Admin\WarehouseController::class, 'stockInOrderDetails'])->name('inventory.stock-in.details');
|
||||||
|
|
||||||
// 模組 3:調撥單
|
// 模組 3:調撥單
|
||||||
Route::get('/transfers', [App\Http\Controllers\Admin\WarehouseController::class, 'transfers'])->name('transfers');
|
Route::get('/transfers', [App\Http\Controllers\Admin\WarehouseController::class, 'transfers'])->name('transfers');
|
||||||
Route::post('/transfers', [App\Http\Controllers\Admin\WarehouseController::class, 'storeTransfer'])->name('transfers.store');
|
Route::post('/transfers', [App\Http\Controllers\Admin\WarehouseController::class, 'storeTransfer'])->name('transfers.store');
|
||||||
Route::patch('/transfers/{transferOrder}/confirm', [App\Http\Controllers\Admin\WarehouseController::class, 'confirmTransfer'])->name('transfers.confirm');
|
Route::patch('/transfers/{transferOrder}/confirm', [App\Http\Controllers\Admin\WarehouseController::class, 'confirmTransfer'])->name('transfers.confirm');
|
||||||
Route::get('/transfers/{order}/details', [App\Http\Controllers\Admin\WarehouseController::class, 'transferOrderDetails'])->name('transfers.details');
|
Route::delete('/transfers/{transferOrder}', [App\Http\Controllers\Admin\WarehouseController::class, 'destroyTransfer'])->name('transfers.destroy');
|
||||||
|
Route::get('/transfers/{id}/details', [App\Http\Controllers\Admin\WarehouseController::class, 'transferOrderDetails'])->name('transfers.details');
|
||||||
|
|
||||||
// 模組 4:機台庫存總覽
|
// 模組 4:機台庫存總覽
|
||||||
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');
|
||||||
@ -99,7 +101,11 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
|
|||||||
// 模組 5:機台補貨
|
// 模組 5:機台補貨
|
||||||
Route::get('/replenishments', [App\Http\Controllers\Admin\WarehouseController::class, 'replenishments'])->name('replenishments');
|
Route::get('/replenishments', [App\Http\Controllers\Admin\WarehouseController::class, 'replenishments'])->name('replenishments');
|
||||||
Route::post('/replenishments', [App\Http\Controllers\Admin\WarehouseController::class, 'storeReplenishment'])->name('replenishments.store');
|
Route::post('/replenishments', [App\Http\Controllers\Admin\WarehouseController::class, 'storeReplenishment'])->name('replenishments.store');
|
||||||
Route::patch('/replenishments/{replenishmentOrder}/confirm', [App\Http\Controllers\Admin\WarehouseController::class, 'confirmReplenishment'])->name('replenishments.confirm');
|
Route::post('/replenishments/auto', [App\Http\Controllers\Admin\WarehouseController::class, 'autoReplenishment'])->name('replenishments.auto');
|
||||||
|
Route::patch('/replenishments/{replenishmentOrder}/status', [App\Http\Controllers\Admin\WarehouseController::class, 'updateReplenishmentStatus'])->name('replenishments.status');
|
||||||
|
Route::patch('/replenishments/{replenishmentOrder}/cancel', [App\Http\Controllers\Admin\WarehouseController::class, 'cancelReplenishment'])->name('replenishments.cancel');
|
||||||
|
Route::patch('/replenishments/{replenishmentOrder}/assign', [App\Http\Controllers\Admin\WarehouseController::class, 'assignReplenishment'])->name('replenishments.assign');
|
||||||
|
Route::get('/replenishments/machine-slots/{machine}', [App\Http\Controllers\Admin\WarehouseController::class, 'getMachineSlotsForReplenishment'])->name('replenishments.machine-slots');
|
||||||
Route::get('/replenishments/{order}/details', [App\Http\Controllers\Admin\WarehouseController::class, 'replenishmentOrderDetails'])->name('replenishments.details');
|
Route::get('/replenishments/{order}/details', [App\Http\Controllers\Admin\WarehouseController::class, 'replenishmentOrderDetails'])->name('replenishments.details');
|
||||||
|
|
||||||
// AJAX 庫存查詢
|
// AJAX 庫存查詢
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user