[FEAT] 倉儲管理模組標準化與庫存矩陣功能實作
1. 實作「目前庫存」矩陣化視圖:將商品作為行、倉庫作為列,支援跨倉庫水位分析與自動加總。 2. 優化矩陣 UI:實作固定欄位 (Sticky Columns) 以應對多倉庫場景,並修正深色模式下的背景色對齊問題。 3. 強化倉儲操作驗證:實作調撥與補貨單的 Alpine.js 前端欄位驗證與 AJAX 提交邏輯,提升操作流暢度。 4. 優化機台地址定位:實作結構化地址解析與 Nominatim API 對接,提升經緯度自動獲取的準確性。 5. 標準化 UI 組件:更新 Breadcrumbs、刪除確認彈窗與搜尋下拉選單,確保全站視覺一致性。 6. 完善多語系支援:補全倉儲管理、機台設定與 UI 提示的繁體中文翻譯。 7. 增加 Product Model 的 stocks() 關聯,支援高效率的庫存矩陣查詢。
This commit is contained in:
parent
2f86bb90a2
commit
39a246d3c5
@ -20,7 +20,22 @@ class AdvertisementController extends AdminController
|
||||
$tab = $request->input('tab', 'list');
|
||||
|
||||
// Tab 1: 廣告列表
|
||||
$advertisements = Advertisement::with('company')->latest()->paginate(10);
|
||||
$query = Advertisement::with('company');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where('name', 'like', "%{$search}%");
|
||||
}
|
||||
|
||||
if ($request->filled('type')) {
|
||||
$query->where('type', $request->type);
|
||||
}
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$query->where('company_id', $request->company_id);
|
||||
}
|
||||
|
||||
$advertisements = $query->latest()->paginate(10);
|
||||
|
||||
// Tab 2: 機台廣告設置 (所需資料) - 隱藏已過期的廣告
|
||||
$allAds = Advertisement::playing()->get();
|
||||
|
||||
@ -126,6 +126,9 @@ class MachineSettingController extends AdminController
|
||||
'machine_model_id' => 'required|exists:machine_models,id',
|
||||
'payment_config_id' => 'nullable|exists:payment_configs,id',
|
||||
'location' => 'nullable|string|max:255',
|
||||
'address' => 'nullable|string|max:255',
|
||||
'latitude' => 'nullable|numeric|between:-90,90',
|
||||
'longitude' => 'nullable|numeric|between:-180,180',
|
||||
'images.*' => 'image|mimes:jpeg,png,jpg,gif,webp|max:10240', // Increase to 10MB
|
||||
]);
|
||||
|
||||
@ -194,6 +197,9 @@ class MachineSettingController extends AdminController
|
||||
'machine_model_id' => 'required|exists:machine_models,id',
|
||||
'payment_config_id' => 'nullable|exists:payment_configs,id',
|
||||
'location' => 'nullable|string|max:255',
|
||||
'address' => 'nullable|string|max:255',
|
||||
'latitude' => 'nullable|numeric|between:-90,90',
|
||||
'longitude' => 'nullable|numeric|between:-180,180',
|
||||
'image_0' => 'nullable|image|mimes:jpeg,png,jpg,gif,webp|max:10240',
|
||||
'image_1' => 'nullable|image|mimes:jpeg,png,jpg,gif,webp|max:10240',
|
||||
'image_2' => 'nullable|image|mimes:jpeg,png,jpg,gif,webp|max:10240',
|
||||
@ -281,5 +287,15 @@ class MachineSettingController extends AdminController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 公開機台分布地圖
|
||||
*/
|
||||
public function distribution(): View
|
||||
{
|
||||
$machines = Machine::whereNotNull('latitude')
|
||||
->whereNotNull('longitude')
|
||||
->get(['id', 'name', 'serial_no', 'address', 'location', 'latitude', 'longitude', 'status']);
|
||||
|
||||
return view('admin.basic-settings.machines.distribution', compact('machines'));
|
||||
}
|
||||
}
|
||||
|
||||
137
app/Http/Controllers/Admin/GeocodingController.php
Normal file
137
app/Http/Controllers/Admin/GeocodingController.php
Normal file
@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* 地址地理編碼代理控制器
|
||||
*
|
||||
* 透過後端轉發 Nominatim API 請求,解決瀏覽器無法自訂 User-Agent
|
||||
* 導致 Nominatim 回傳 403 Forbidden 的問題。
|
||||
*/
|
||||
class GeocodingController extends Controller
|
||||
{
|
||||
/**
|
||||
* 將台灣地址轉換為經緯度座標
|
||||
*/
|
||||
public function resolve(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'address' => 'required|string|max:255',
|
||||
]);
|
||||
|
||||
$address = $request->input('address');
|
||||
|
||||
// 策略 1:結構化搜尋(台灣地址精確度最高)
|
||||
$parsed = $this->parseTaiwanAddress($address);
|
||||
|
||||
if ($parsed['street']) {
|
||||
$response = Http::withHeaders([
|
||||
'User-Agent' => 'StarCloud/1.0 (admin@starcloud.com)',
|
||||
])->get('https://nominatim.openstreetmap.org/search', [
|
||||
'format' => 'json',
|
||||
'street' => $parsed['street'],
|
||||
'city' => $parsed['city'],
|
||||
'country' => 'Taiwan',
|
||||
'limit' => 1,
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$data = $response->json();
|
||||
if (!empty($data)) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'lat' => (float) $data[0]['lat'],
|
||||
'lon' => (float) $data[0]['lon'],
|
||||
'display_name' => $data[0]['display_name'] ?? '',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 策略 2:自由搜尋 + 台灣限定
|
||||
$searchQuery = $address;
|
||||
if (!str_contains($searchQuery, '台灣') && !str_contains($searchQuery, 'Taiwan')) {
|
||||
$searchQuery .= ', 台灣';
|
||||
}
|
||||
|
||||
$response = Http::withHeaders([
|
||||
'User-Agent' => 'StarCloud/1.0 (admin@starcloud.com)',
|
||||
])->get('https://nominatim.openstreetmap.org/search', [
|
||||
'format' => 'json',
|
||||
'q' => $searchQuery,
|
||||
'countrycodes' => 'tw',
|
||||
'limit' => 1,
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$data = $response->json();
|
||||
if (!empty($data)) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'lat' => (float) $data[0]['lat'],
|
||||
'lon' => (float) $data[0]['lon'],
|
||||
'display_name' => $data[0]['display_name'] ?? '',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => __('Location not found'),
|
||||
], 404);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析台灣地址格式為結構化參數
|
||||
* 例如:「台中市北區興進路90之3號」→ city: "台中市 北區", street: "90之3 興進路"
|
||||
*/
|
||||
private function parseTaiwanAddress(string $address): array
|
||||
{
|
||||
$address = trim($address);
|
||||
|
||||
// 解析城市 (市/縣)
|
||||
preg_match('/^(.+?[市縣])/u', $address, $cityMatch);
|
||||
$city = $cityMatch[1] ?? '';
|
||||
|
||||
// 解析區域 (區/鄉/鎮) — 排除「市」以避免與城市混淆
|
||||
preg_match('/[市縣](.+?[區鄉鎮])/u', $address, $districtMatch);
|
||||
$district = $districtMatch[1] ?? '';
|
||||
|
||||
// 取得路街部分:從最後一個「區/鄉/鎮」之後開始截取
|
||||
if ($district) {
|
||||
// 精確定位到 district 結束後的部分
|
||||
$pos = mb_strpos($address, $district);
|
||||
if ($pos !== false) {
|
||||
$streetPart = mb_substr($address, $pos + mb_strlen($district));
|
||||
} else {
|
||||
$streetPart = $address;
|
||||
}
|
||||
} else {
|
||||
$streetPart = $address;
|
||||
}
|
||||
|
||||
// 拆分路名與門牌號碼
|
||||
preg_match('/^(.+?(?:路|街|大道|巷|弄)(?:[一二三四五六七八九十]+段)?)\s*(.*)/u', $streetPart, $roadMatch);
|
||||
|
||||
$street = '';
|
||||
$houseNumber = '';
|
||||
if ($roadMatch) {
|
||||
$street = $roadMatch[1];
|
||||
$houseNumber = str_replace('號', '', $roadMatch[2] ?? '');
|
||||
} else {
|
||||
$street = $streetPart;
|
||||
}
|
||||
|
||||
$fullStreet = ($houseNumber ? $houseNumber . ' ' : '') . $street;
|
||||
$fullCity = $city . ($district ? ' ' . $district : '');
|
||||
|
||||
return [
|
||||
'city' => $fullCity,
|
||||
'street' => $fullStreet,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -26,6 +26,7 @@ class WarehouseController extends Controller
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = Warehouse::query()
|
||||
->with(['company'])
|
||||
->withCount('stocks as products_count')
|
||||
->withSum('stocks as total_stock', 'quantity');
|
||||
|
||||
@ -52,33 +53,71 @@ class WarehouseController extends Controller
|
||||
->whereHas('warehouse', fn($q) => $q)->count(),
|
||||
];
|
||||
|
||||
return view('admin.warehouses.index', compact('warehouses', 'stats'));
|
||||
$companies = collect();
|
||||
if (Auth::user()->isSystemAdmin()) {
|
||||
$companies = \App\Models\System\Company::active()->orderBy('name')->get();
|
||||
}
|
||||
|
||||
return view('admin.warehouses.index', compact('warehouses', 'stats', 'companies'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'company_id' => Auth::user()->isSystemAdmin() ? 'nullable|exists:companies,id' : 'nullable',
|
||||
'name' => 'required|string|max:255',
|
||||
'type' => 'required|in:main,branch',
|
||||
'address' => 'nullable|string|max:500',
|
||||
], [], [
|
||||
'name' => __('Warehouse Name'),
|
||||
'type' => __('Warehouse Type'),
|
||||
'company_id' => __('Company Name'),
|
||||
]);
|
||||
$validated['company_id'] = Auth::user()->company_id;
|
||||
|
||||
if (!Auth::user()->isSystemAdmin()) {
|
||||
$validated['company_id'] = Auth::user()->company_id;
|
||||
}
|
||||
|
||||
Warehouse::create($validated);
|
||||
|
||||
if ($request->ajax() || $request->wantsJson()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => __('Warehouse created successfully'),
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->route('admin.warehouses.index')
|
||||
->with('success', __('Warehouse created successfully'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Warehouse $warehouse)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
$rules = [
|
||||
'name' => 'required|string|max:255',
|
||||
'type' => 'required|in:main,branch',
|
||||
'address' => 'nullable|string|max:500',
|
||||
'is_active' => 'required|boolean',
|
||||
];
|
||||
|
||||
if (Auth::user()->isSystemAdmin()) {
|
||||
$rules['company_id'] = 'nullable|exists:companies,id';
|
||||
}
|
||||
|
||||
$validated = $request->validate($rules, [], [
|
||||
'name' => __('Warehouse Name'),
|
||||
'type' => __('Warehouse Type'),
|
||||
'company_id' => __('Company Name'),
|
||||
]);
|
||||
$warehouse->update($validated);
|
||||
|
||||
if ($request->ajax() || $request->wantsJson()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => __('Warehouse updated successfully'),
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->route('admin.warehouses.index')
|
||||
->with('success', __('Warehouse updated successfully'));
|
||||
}
|
||||
@ -108,23 +147,27 @@ class WarehouseController extends Controller
|
||||
|
||||
public function inventory(Request $request)
|
||||
{
|
||||
$tab = $request->input('tab', 'stock');
|
||||
$tab = $request->input('tab', 'stock-in');
|
||||
$warehouses = Warehouse::active()->orderBy('name')->get();
|
||||
|
||||
$data = compact('warehouses', 'tab');
|
||||
|
||||
if ($tab === 'stock') {
|
||||
$query = WarehouseStock::with(['warehouse:id,name,type', 'product:id,name,image_url'])
|
||||
->whereHas('warehouse', fn($q) => $q);
|
||||
$inventory_warehouses = Warehouse::active()->orderBy('name')->get(['id', 'name', 'type']);
|
||||
|
||||
$query = Product::with(['stocks' => function($q) {
|
||||
$q->select('id', 'product_id', 'warehouse_id', 'quantity', 'safety_stock');
|
||||
}, 'translations']);
|
||||
|
||||
if ($request->filled('warehouse_id')) {
|
||||
$query->where('warehouse_id', $request->input('warehouse_id'));
|
||||
$query->whereHas('stocks', fn($q) => $q->where('warehouse_id', $request->input('warehouse_id')));
|
||||
}
|
||||
if ($search = $request->input('search')) {
|
||||
$query->whereHas('product', fn($q) => $q->where('name', 'like', "%{$search}%"));
|
||||
$query->where('name', 'like', "%{$search}%");
|
||||
}
|
||||
|
||||
$data['stocks'] = $query->orderBy('warehouse_id')
|
||||
$data['inventory_warehouses'] = $inventory_warehouses;
|
||||
$data['products'] = $query->orderBy('name')
|
||||
->paginate($request->input('per_page', 15))
|
||||
->withQueryString();
|
||||
|
||||
@ -135,7 +178,7 @@ class WarehouseController extends Controller
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->input('status'));
|
||||
}
|
||||
$data['orders'] = $query->paginate(10)->withQueryString();
|
||||
$data['orders'] = $query->paginate($request->input('per_page', 10))->withQueryString();
|
||||
$data['products'] = Product::orderBy('name')->get(['id', 'name', 'image_url']);
|
||||
|
||||
} elseif ($tab === 'movements') {
|
||||
@ -148,7 +191,15 @@ class WarehouseController extends Controller
|
||||
if ($request->filled('type')) {
|
||||
$query->where('type', $request->input('type'));
|
||||
}
|
||||
$data['movements'] = $query->paginate(15)->withQueryString();
|
||||
$data['movements'] = $query->paginate($request->input('per_page', 15))->withQueryString();
|
||||
}
|
||||
|
||||
if ($request->ajax()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'tab' => $tab,
|
||||
'html' => view('admin.warehouses.partials.tab-' . $tab, $data)->render()
|
||||
]);
|
||||
}
|
||||
|
||||
return view('admin.warehouses.inventory', $data);
|
||||
@ -182,6 +233,13 @@ class WarehouseController extends Controller
|
||||
}
|
||||
});
|
||||
|
||||
if ($request->ajax()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => __('Stock-in order created')
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->route('admin.warehouses.inventory', ['tab' => 'stock-in'])
|
||||
->with('success', __('Stock-in order created'));
|
||||
}
|
||||
@ -288,6 +346,13 @@ class WarehouseController extends Controller
|
||||
}
|
||||
});
|
||||
|
||||
if ($request->ajax()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => __('Transfer order created')
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->route('admin.warehouses.transfers')
|
||||
->with('success', __('Transfer order created'));
|
||||
}
|
||||
@ -451,6 +516,13 @@ class WarehouseController extends Controller
|
||||
}
|
||||
});
|
||||
|
||||
if ($request->ajax()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => __('Replenishment order created')
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->route('admin.warehouses.replenishments')
|
||||
->with('success', __('Replenishment order created'));
|
||||
}
|
||||
@ -506,4 +578,189 @@ class WarehouseController extends Controller
|
||||
|
||||
return back()->with('success', __('Replenishment completed'));
|
||||
}
|
||||
/**
|
||||
* AJAX:取得來源庫存(倉庫或機台)
|
||||
*/
|
||||
public function getStockAjax(Request $request)
|
||||
{
|
||||
$warehouseId = $request->input('warehouse_id');
|
||||
$machineId = $request->input('machine_id');
|
||||
|
||||
try {
|
||||
if ($warehouseId) {
|
||||
// Warehouse uses TenantScoped, findOrFail will respect it
|
||||
$warehouse = Warehouse::findOrFail($warehouseId);
|
||||
$stocks = WarehouseStock::with('product:id,name,image_url')
|
||||
->where('warehouse_id', $warehouse->id)
|
||||
->where('quantity', '>', 0)
|
||||
->get();
|
||||
|
||||
$data = $stocks->map(fn($s) => [
|
||||
'id' => $s->product_id,
|
||||
'name' => $s->product?->name ?? 'Unknown',
|
||||
'quantity' => $s->quantity,
|
||||
'image' => $s->product?->image_url
|
||||
]);
|
||||
} elseif ($machineId) {
|
||||
// Machine uses TenantScoped and machine_access scope
|
||||
$machine = Machine::findOrFail($machineId);
|
||||
$slots = MachineSlot::with('product:id,name,image_url')
|
||||
->where('machine_id', $machine->id)
|
||||
->where('stock', '>', 0)
|
||||
->get()
|
||||
->groupBy('product_id');
|
||||
|
||||
$data = $slots->map(function($group) {
|
||||
$p = $group->first()->product;
|
||||
return [
|
||||
'id' => $group->first()->product_id,
|
||||
'name' => $p?->name ?? 'Unknown',
|
||||
'quantity' => $group->sum('stock'),
|
||||
'image' => $p?->image_url
|
||||
];
|
||||
})->values();
|
||||
} else {
|
||||
return response()->json(['success' => false, 'message' => 'No source provided']);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['success' => false, 'message' => __('Unauthorized or not found')]);
|
||||
}
|
||||
|
||||
return response()->json(['success' => true, 'data' => $data]);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX:取得倉庫商品庫存詳情 (用於滑出面板)
|
||||
*/
|
||||
public function warehouseStocks(Warehouse $warehouse, Request $request)
|
||||
{
|
||||
$query = $warehouse->stocks()->with('product:id,name,image_url');
|
||||
|
||||
if ($search = $request->input('search')) {
|
||||
$query->whereHas('product', fn($q) => $q->where('name', 'like', "%{$search}%"));
|
||||
}
|
||||
|
||||
$stocks = $query->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'warehouse' => [
|
||||
'id' => $warehouse->id,
|
||||
'name' => $warehouse->name,
|
||||
'type' => $warehouse->type,
|
||||
],
|
||||
'stocks' => $stocks->map(fn($s) => [
|
||||
'id' => $s->id,
|
||||
'product_id' => $s->product_id,
|
||||
'product_name' => $s->product?->name ?? 'Unknown',
|
||||
'image_url' => $s->product?->image_url,
|
||||
'quantity' => (int)$s->quantity,
|
||||
'safety_stock' => (int)$s->safety_stock,
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX:取得進貨單詳情
|
||||
*/
|
||||
public function stockInOrderDetails(StockInOrder $order)
|
||||
{
|
||||
$order->load(['warehouse', 'creator', 'items.product']);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'order' => [
|
||||
'id' => $order->id,
|
||||
'order_no' => $order->order_no,
|
||||
'status' => $order->status,
|
||||
'warehouse_name' => $order->warehouse?->name,
|
||||
'creator_name' => $order->creator?->name,
|
||||
'created_at' => $order->created_at?->format('Y-m-d H:i:s'),
|
||||
'completed_at' => $order->completed_at?->format('Y-m-d H:i:s'),
|
||||
'note' => $order->note,
|
||||
],
|
||||
'items' => $order->items->map(fn($item) => [
|
||||
'product_name' => $item->product?->name ?? 'Unknown',
|
||||
'image_url' => $item->product?->image_url,
|
||||
'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.
|
||||
*/
|
||||
public function transferOrderDetails(Request $request, $id)
|
||||
{
|
||||
$order = \App\Models\WarehouseTransfer::with(['fromWarehouse', 'toWarehouse', 'fromMachine', 'creator'])
|
||||
->where('id', $id)
|
||||
->firstOrFail();
|
||||
|
||||
$items = \App\Models\WarehouseTransferItem::with('product')
|
||||
->where('transfer_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,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'order' => [
|
||||
'id' => $order->id,
|
||||
'order_no' => $order->order_no,
|
||||
'status' => $order->status,
|
||||
'type' => $order->type,
|
||||
'from_name' => $order->type === 'warehouse_to_warehouse' ? $order->fromWarehouse?->name : $order->fromMachine?->name,
|
||||
'to_warehouse_name' => $order->toWarehouse?->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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,6 +53,9 @@ class Machine extends Model
|
||||
'serial_no',
|
||||
'model',
|
||||
'location',
|
||||
'address',
|
||||
'latitude',
|
||||
'longitude',
|
||||
'status',
|
||||
'current_page',
|
||||
'door_status',
|
||||
|
||||
@ -90,4 +90,12 @@ class Product extends Model
|
||||
return $this->hasMany(\App\Models\System\Translation::class, 'key', 'name_dictionary_key')
|
||||
->where('group', 'product');
|
||||
}
|
||||
|
||||
/**
|
||||
* 倉庫庫存紀錄
|
||||
*/
|
||||
public function stocks()
|
||||
{
|
||||
return $this->hasMany(\App\Models\Warehouse\WarehouseStock::class);
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,4 +67,12 @@ class Company extends Model
|
||||
{
|
||||
return $this->hasMany(Machine::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope:僅篩選啟用的公司
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('status', 1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,4 +79,12 @@ class Warehouse extends Model
|
||||
{
|
||||
return $this->hasMany(StockInOrder::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 所屬公司
|
||||
*/
|
||||
public function company()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\System\Company::class);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('machines', function (Blueprint $table) {
|
||||
$table->string('address')->nullable()->after('location');
|
||||
$table->decimal('latitude', 10, 8)->nullable()->after('address');
|
||||
$table->decimal('longitude', 11, 8)->nullable()->after('latitude');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('machines', function (Blueprint $table) {
|
||||
$table->dropColumn(['address', 'latitude', 'longitude']);
|
||||
});
|
||||
}
|
||||
};
|
||||
1711
lang/en.json
1711
lang/en.json
File diff suppressed because it is too large
Load Diff
271
lang/ja.json
271
lang/ja.json
@ -1,128 +1,165 @@
|
||||
{
|
||||
"Goods & System Settings": "商品とシステム設定",
|
||||
"Display Material Code": "素材コードを表示",
|
||||
"Enable Points Mechanism": "ポイント機能を有効化",
|
||||
"Taxation System": "税務システム",
|
||||
"Card Terminal System": "カード端末システム",
|
||||
"QR Scan Payment": "QRスキャン決済機能",
|
||||
"Shopping Cart Feature": "ショッピングカート機能",
|
||||
"Welcome Gift Feature": "来店特典機能",
|
||||
"Cash Module Feature": "現金モジュール機能",
|
||||
"Includes Credit Card/Mobile Pay": "クレジットカード/モバイル決済を含む",
|
||||
"E.SUN QR Pay": "玉山QR決済",
|
||||
"LinePay Payment": "LinePay決済",
|
||||
"Coin/Bill Acceptor": "コイン/紙幣機",
|
||||
"System settings updated successfully.": "システム設定が正常に更新されました。",
|
||||
"Update Settings": "設定を更新",
|
||||
"Enabled Features": "有効な機能",
|
||||
"Material Code": "素材コード",
|
||||
"Points": "ポイント",
|
||||
"Electronic Invoice": "電子翻訳",
|
||||
"Actions": "操作",
|
||||
"Add Item": "項目を追加",
|
||||
"Add Warehouse": "倉庫を追加",
|
||||
"Adjustment": "在庫調整",
|
||||
"All Companies": "すべての会社",
|
||||
"All Statuses": "すべてのステータス",
|
||||
"All Types": "すべてのタイプ",
|
||||
"All Warehouses": "すべての倉庫",
|
||||
"Approved At": "承認日時",
|
||||
"Are you sure you want to confirm this stock-in order? This action will update your inventory levels.": "この入庫伝票を確定してもよろしいですか?この操作により在庫レベルが更新されます。",
|
||||
"Branch": "分倉庫",
|
||||
"Branch Warehouses": "分倉庫数",
|
||||
"CREATE STOCK TRANSFERS BETWEEN WAREHOUSES AND MACHINE RETURNS": "倉庫間の転送および機台からの返品伝票を作成",
|
||||
"Cancelled": "キャンセル済み",
|
||||
"Cannot delete warehouse with existing stock": "在庫がある倉庫は削除できません。",
|
||||
"Card System": "カード決済システム",
|
||||
"ESUN": "玉山銀行 (ESUN)",
|
||||
"LinePay": "LinePay",
|
||||
"Shopping Cart": "ショッピングカート",
|
||||
"Welcome Gift": "登録特典",
|
||||
"Card Terminal System": "カード端末システム",
|
||||
"Cash Module": "現金決済モジュール",
|
||||
"Cash Module Feature": "現金モジュール機能",
|
||||
"Coin\/Bill Acceptor": "コイン\/紙幣機",
|
||||
"Completed": "完了",
|
||||
"Confirm": "確認",
|
||||
"Confirm Stock-in Order": "入庫伝票を確認",
|
||||
"Confirm replenishment completed?": "補充が完了したことを確認しますか?",
|
||||
"Confirm this stock-in order?": "この入庫伝票を確認しますか?",
|
||||
"Confirm this transfer?": "この転送を確認しますか?",
|
||||
"Create Replenishment Order": "補充伝票を作成",
|
||||
"Create Transfer Order": "転送伝票を作成",
|
||||
"Create and manage replenishment orders from warehouse to machines": "倉庫から機台への補充伝票の作成と管理",
|
||||
"Create stock transfers between warehouses and machine returns": "倉庫間の転送および機台からの返品伝票を作成",
|
||||
"Created At": "作成日時",
|
||||
"Created By": "作成者",
|
||||
"Damage": "破損",
|
||||
"Delivering": "配送中",
|
||||
"Display Material Code": "素材コードを表示",
|
||||
"Draft": "下書き",
|
||||
"E.SUN QR Pay": "玉山QR決済",
|
||||
"ESUN": "玉山銀行 (ESUN)",
|
||||
"Electronic Invoice": "電子翻訳",
|
||||
"Empty": "空",
|
||||
"Enable Points Mechanism": "ポイント機能を有効化",
|
||||
"Enabled Features": "有効な機能",
|
||||
"Expiry": "有効期限",
|
||||
"Fill Rate": "充填率",
|
||||
"From": "発送元",
|
||||
"From Machine": "発送元機台",
|
||||
"From Warehouse": "発送元倉庫",
|
||||
"Goods & System Settings": "商品とシステム設定",
|
||||
"Image": "画像",
|
||||
"In Stock": "現在庫",
|
||||
"Includes Credit Card\/Mobile Pay": "クレジットカード\/モバイル決済を含む",
|
||||
"Inventory Management": "在庫管理",
|
||||
"LinePay": "LinePay",
|
||||
"LinePay Payment": "LinePay決済",
|
||||
"Loading...": "読み込み中...",
|
||||
"Low Stock": "在庫不足",
|
||||
"Low Stock Alerts": "低在庫アラート",
|
||||
"Machine Inventory Overview": "機台在庫概要",
|
||||
"Machine Replenishment": "機台補充",
|
||||
"Machine Return": "機台返品",
|
||||
"Machine to Warehouse": "機台から倉庫",
|
||||
"Main": "総倉庫",
|
||||
"Main Warehouses": "総倉庫数",
|
||||
"Manage stock levels, stock-in orders, and movement history": "在庫レベル、入庫伝票、および移動履歴の管理",
|
||||
"Manage stock transfers between warehouses and machine returns": "倉庫間の転送および機台からの返品伝票を作成",
|
||||
"Material Code": "素材コード",
|
||||
"Movement History": "移動履歴",
|
||||
"Movement Logs": "移動ログ",
|
||||
"New Replenishment": "新規補充",
|
||||
"New Stock-In Order": "新規入庫伝票",
|
||||
"New Transfer": "新規転送",
|
||||
"No movement records found": "移動履歴が見つかりません",
|
||||
"No products found in this warehouse": "この倉庫に在庫はありません",
|
||||
"No replenishment orders found": "補充伝票が見つかりません",
|
||||
"No slot data": "貨道データなし",
|
||||
"No stock data found": "在庫データが見つかりません",
|
||||
"No stock-in orders found": "入庫伝票が見つかりません",
|
||||
"No transfer orders found": "転送伝票が見つかりません",
|
||||
"Normal": "通常",
|
||||
"Note": "備考",
|
||||
"Note (optional)": "備考 (任意)",
|
||||
"Optional": "任意",
|
||||
"Order Details": "入庫伝票詳細",
|
||||
"Order No.": "伝票番号",
|
||||
"Out of Stock": "在庫切れ",
|
||||
"Pending": "保留中",
|
||||
"Please enter configuration name": "設定名を入力してください",
|
||||
"Please select a company": "会社を選択してください",
|
||||
"Points": "ポイント",
|
||||
"Prepared": "準備済み",
|
||||
"Product": "商品",
|
||||
"Product Details": "商品詳細",
|
||||
"Product ID": "商品ID",
|
||||
"Products": "商品",
|
||||
"Products \/ Stock": "商品 \/ 在庫",
|
||||
"Publish": "配信開始",
|
||||
"QR Scan Payment": "QRスキャン決済機能",
|
||||
"Qty": "数量",
|
||||
"Qty Change": "数量変更",
|
||||
"Quantity": "数量",
|
||||
"Real-time slot-level inventory across all machines": "すべての機台におけるリアルタイムの貨道別在庫",
|
||||
"Replenishment Items": "補充項目",
|
||||
"Replenishment Orders": "機台補充伝票",
|
||||
"Reset Filters": "フィルターをリセット",
|
||||
"Safety Stock": "安全在庫",
|
||||
"Search Product": "商品を検索",
|
||||
"Search by name...": "名称で検索...",
|
||||
"Search machines...": "機台を検索...",
|
||||
"Search products...": "商品を検索...",
|
||||
"Search warehouses...": "倉庫を検索...",
|
||||
"Select Machine": "機台を選択",
|
||||
"Select Product": "商品を選択",
|
||||
"Select Warehouse": "倉庫を選択",
|
||||
"Serial No.": "シリアル番号",
|
||||
"Shopping Cart": "ショッピングカート",
|
||||
"Shopping Cart Feature": "ショッピングカート機能",
|
||||
"Slot": "貨道",
|
||||
"Source Warehouse": "発送元倉庫",
|
||||
"Status": "ステータス",
|
||||
"Stock": "在庫",
|
||||
"Stock In": "入庫",
|
||||
"Stock Levels": "在庫レベル",
|
||||
"Stock Out": "出庫",
|
||||
"Stock flow history": "在庫移動履歴",
|
||||
"Stock-In Management": "入庫管理",
|
||||
"Stock-In Orders": "入庫伝票",
|
||||
"Stock-In Orders Tab": "入庫伝票",
|
||||
"System settings updated successfully.": "システム設定が正常に更新されました。",
|
||||
"Target Machine": "対象機台",
|
||||
"Target Warehouse": "対象倉庫",
|
||||
"Taxation System": "税務システム",
|
||||
"Time": "時間",
|
||||
"To": "配送先",
|
||||
"To Warehouse": "配送先倉庫",
|
||||
"Total Stock": "総在庫",
|
||||
"Total Warehouses": "倉庫総数",
|
||||
"Track stock levels, stock-in orders, and movement history": "在庫レベル、入庫伝票、および移動履歴の追跡",
|
||||
"Track stock-in orders and movement history": "入庫伝票および移動履歴の追跡",
|
||||
"Transfer Audit": "転送監査",
|
||||
"Transfer In": "転送入",
|
||||
"Transfer Orders": "転送伝票",
|
||||
"Transfer Out": "転送出",
|
||||
"Transfer Type": "転送タイプ",
|
||||
"Transfers": "転送",
|
||||
"Unpublish": "配信終了",
|
||||
"Update Settings": "設定を更新",
|
||||
"Video": "動画",
|
||||
"View Details": "詳細を表示",
|
||||
"View Inventory": "在庫を表示",
|
||||
"View Slots": "貨道を表示",
|
||||
"Warehouse": "倉庫",
|
||||
"Warehouse Info": "倉庫情報",
|
||||
"Warehouse Inventory": "倉庫在庫状況",
|
||||
"Warehouse Management": "倉庫管理",
|
||||
"Warehouse Overview": "倉庫概要",
|
||||
"Add Warehouse": "倉庫を追加",
|
||||
"Total Warehouses": "倉庫総数",
|
||||
"Main Warehouses": "総倉庫数",
|
||||
"Branch Warehouses": "分倉庫数",
|
||||
"Low Stock Alerts": "低在庫アラート",
|
||||
"Search warehouses...": "倉庫を検索...",
|
||||
"Main": "総倉庫",
|
||||
"Branch": "分倉庫",
|
||||
"Warehouse Info": "倉庫情報",
|
||||
"Products / Stock": "商品 / 在庫",
|
||||
"Products": "商品",
|
||||
"Stock": "在庫",
|
||||
"Warehouse Transfer": "倉庫転送",
|
||||
"Warehouse created successfully.": "倉庫が正常に作成されました。",
|
||||
"Warehouse updated successfully.": "倉庫が正常に更新されました。",
|
||||
"Warehouse deleted successfully.": "倉庫が正常に削除されました。",
|
||||
"Cannot delete warehouse with existing stock": "在庫がある倉庫は削除できません。",
|
||||
"Inventory Management": "在庫管理",
|
||||
"Track stock levels, stock-in orders, and movement history": "在庫レベル、入庫伝票、および移動履歴の追跡",
|
||||
"New Stock-In Order": "新規入庫伝票",
|
||||
"Stock Levels": "在庫レベル",
|
||||
"Stock-In Orders": "入庫伝票",
|
||||
"Movement History": "移動履歴",
|
||||
"Search products...": "商品を検索...",
|
||||
"All Warehouses": "すべての倉庫",
|
||||
"Product": "商品",
|
||||
"Warehouse": "倉庫",
|
||||
"Quantity": "数量",
|
||||
"Safety Stock": "安全在庫",
|
||||
"Low Stock": "在庫不足",
|
||||
"Out of Stock": "在庫切れ",
|
||||
"Normal": "通常",
|
||||
"No stock data found": "在庫データが見つかりません",
|
||||
"Order No.": "伝票番号",
|
||||
"Created By": "作成者",
|
||||
"Created At": "作成日時",
|
||||
"Draft": "下書き",
|
||||
"Completed": "完了",
|
||||
"Confirm this stock-in order?": "この入庫伝票を確認しますか?",
|
||||
"Confirm": "確認",
|
||||
"No stock-in orders found": "入庫伝票が見つかりません",
|
||||
"Time": "時間",
|
||||
"Qty Change": "数量変更",
|
||||
"No movement records found": "移動履歴が見つかりません",
|
||||
"Target Warehouse": "対象倉庫",
|
||||
"Select Warehouse": "倉庫を選択",
|
||||
"Add Item": "項目を追加",
|
||||
"Select Product": "商品を選択",
|
||||
"Qty": "数量",
|
||||
"Note": "備考",
|
||||
"Optional": "任意",
|
||||
"Transfer Orders": "転送伝票",
|
||||
"Manage stock transfers between warehouses and machine returns": "倉庫間の転送および機台からの返品の管理",
|
||||
"New Transfer": "新規転送",
|
||||
"All Types": "すべてのタイプ",
|
||||
"Warehouse purchase records": "倉庫購入記録",
|
||||
"Warehouse to Warehouse": "倉庫間転送",
|
||||
"Machine to Warehouse": "機台から倉庫",
|
||||
"All Statuses": "すべてのステータス",
|
||||
"Cancelled": "キャンセル済み",
|
||||
"From": "発送元",
|
||||
"To": "配送先",
|
||||
"Confirm this transfer?": "この転送を確認しますか?",
|
||||
"No transfer orders found": "転送伝票が見つかりません",
|
||||
"Transfer Type": "転送タイプ",
|
||||
"From Warehouse": "発送元倉庫",
|
||||
"From Machine": "発送元機台",
|
||||
"Select Machine": "機台を選択",
|
||||
"To Warehouse": "配送先倉庫",
|
||||
"Note (optional)": "備考 (任意)",
|
||||
"Machine Inventory Overview": "機台在庫概要",
|
||||
"Real-time slot-level inventory across all machines": "すべての機台におけるリアルタイムの貨道別在庫",
|
||||
"Search machines...": "機台を検索...",
|
||||
"Serial No.": "シリアル番号",
|
||||
"Total Stock": "総在庫",
|
||||
"Fill Rate": "充填率",
|
||||
"View Slots": "貨道を表示",
|
||||
"Loading...": "読み込み中...",
|
||||
"Empty": "空",
|
||||
"Expiry": "有効期限",
|
||||
"No slot data": "貨道データなし",
|
||||
"Machine Replenishment": "機台補充",
|
||||
"Create and manage replenishment orders from warehouse to machines": "倉庫から機台への補充伝票の作成と管理",
|
||||
"New Replenishment": "新規補充",
|
||||
"Pending": "保留中",
|
||||
"Prepared": "準備済み",
|
||||
"Delivering": "配送中",
|
||||
"Confirm replenishment completed?": "補充が完了したことを確認しますか?",
|
||||
"No replenishment orders found": "補充伝票が見つかりません",
|
||||
"Source Warehouse": "発送元倉庫",
|
||||
"Target Machine": "対象機台",
|
||||
"Replenishment Items": "補充項目",
|
||||
"Slot": "貨道",
|
||||
"Stock In": "入庫",
|
||||
"Stock Out": "出庫",
|
||||
"Adjustment": "在庫調整",
|
||||
"Damage": "破損",
|
||||
"Transfer In": "転送入",
|
||||
"Transfer Out": "転送出"
|
||||
"Warehouse updated successfully.": "倉庫が正常に更新されました。",
|
||||
"Welcome Gift": "登録特典",
|
||||
"Welcome Gift Feature": "来店特典機能"
|
||||
}
|
||||
1899
lang/zh_TW.json
1899
lang/zh_TW.json
File diff suppressed because it is too large
Load Diff
@ -91,7 +91,63 @@ $baseRoute = 'admin.data-config.advertisements';
|
||||
<p class="text-[12px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<!-- Filters Area -->
|
||||
<div class="mb-8">
|
||||
<form method="GET" action="{{ route($baseRoute . '.index') }}"
|
||||
class="flex items-center justify-between"
|
||||
@submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())">
|
||||
<div class="flex items-center gap-4">
|
||||
<input type="hidden" name="tab" value="list">
|
||||
<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 stroke-[2.5]"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<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') }}"
|
||||
placeholder="{{ __('Search by name...') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-80">
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<x-searchable-select name="type"
|
||||
:selected="request('type')"
|
||||
:placeholder="__('All Types')"
|
||||
:hasSearch="false"
|
||||
class="w-52"
|
||||
@change="$el.closest('form').dispatchEvent(new Event('submit', {cancelable: true}))">
|
||||
<option value="image" {{ request('type') === 'image' ? 'selected' : '' }}>{{ __('Image') }}</option>
|
||||
<option value="video" {{ request('type') === 'video' ? 'selected' : '' }}>{{ __('Video') }}</option>
|
||||
</x-searchable-select>
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<x-searchable-select name="company_id"
|
||||
:options="$companies"
|
||||
:selected="request('company_id')"
|
||||
:placeholder="__('All Companies')"
|
||||
class="w-80"
|
||||
@change="$el.closest('form').dispatchEvent(new Event('submit', {cancelable: true}))" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="{{ route($baseRoute . '.index', ['tab' => 'list']) }}"
|
||||
class="btn-luxury-ghost p-2.5"
|
||||
title="{{ __('Reset Filters') }}"
|
||||
@click.prevent="fetchPage($el.getAttribute('href'))">
|
||||
<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="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>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="ajax-content-container" :class="{ 'opacity-30 pointer-events-none transition-opacity duration-300': isLoading }">
|
||||
<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">
|
||||
@ -141,8 +197,8 @@ $baseRoute = 'admin.data-config.advertisements';
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center whitespace-nowrap">
|
||||
<div class="flex flex-col items-center gap-0.5">
|
||||
<span class="text-[11px] font-mono font-bold text-slate-500 dark:text-slate-400 uppercase tracking-tight">{{ __('From') }}: {{ $ad->start_at?->format('Y-m-d H:i') ?? __('Immediate') }}</span>
|
||||
<span class="text-[11px] font-mono font-bold text-slate-500 dark:text-slate-400 uppercase tracking-tight">{{ __('To') }}: {{ $ad->end_at?->format('Y-m-d H:i') ?? __('Indefinite') }}</span>
|
||||
<span class="text-[11px] font-mono font-bold text-slate-500 dark:text-slate-400 uppercase tracking-tight">{{ __('Publish') }}: {{ $ad->start_at?->format('Y-m-d H:i') ?? __('Immediate') }}</span>
|
||||
<span class="text-[11px] font-mono font-bold text-slate-500 dark:text-slate-400 uppercase tracking-tight">{{ __('Unpublish') }}: {{ $ad->end_at?->format('Y-m-d H:i') ?? __('Indefinite') }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
@ -185,7 +241,8 @@ $baseRoute = 'admin.data-config.advertisements';
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-8">
|
||||
{{ $advertisements->links('vendor.pagination.luxury') }}
|
||||
{{ $advertisements->appends(request()->query())->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -481,22 +538,22 @@ $baseRoute = 'admin.data-config.advertisements';
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
// Update table contents
|
||||
const newTableBody = doc.querySelector('tbody');
|
||||
const currentTableBody = document.querySelector('tbody');
|
||||
if (newTableBody && currentTableBody) {
|
||||
currentTableBody.innerHTML = newTableBody.innerHTML;
|
||||
}
|
||||
|
||||
// Update pagination
|
||||
const newPagination = doc.querySelector('.mt-8');
|
||||
const currentPagination = document.querySelector('.mt-8');
|
||||
if (newPagination && currentPagination) {
|
||||
currentPagination.innerHTML = newPagination.innerHTML;
|
||||
// Update main content container
|
||||
const newContent = doc.querySelector('#ajax-content-container');
|
||||
const currentContent = document.querySelector('#ajax-content-container');
|
||||
if (newContent && currentContent) {
|
||||
currentContent.innerHTML = newContent.innerHTML;
|
||||
}
|
||||
|
||||
// Update URL without reload
|
||||
window.history.pushState({}, '', url);
|
||||
|
||||
// Re-initialize Preline UI
|
||||
this.$nextTick(() => {
|
||||
if (window.HSStaticMethods) {
|
||||
window.HSStaticMethods.autoInit();
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching page:', error);
|
||||
|
||||
@ -0,0 +1,193 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ __('Machine Distribution Map') }} | Star Cloud</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Outfit:wght@100;200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Leaflet CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
|
||||
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
|
||||
<style>
|
||||
body { font-family: 'Inter', 'Outfit', sans-serif; }
|
||||
#map { height: 100%; width: 100%; z-index: 1; }
|
||||
.luxury-shadow { box-shadow: 0 20px 50px -12px rgba(0, 0, 0, 0.15); }
|
||||
.glass-panel {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
.dark .glass-panel {
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* Custom Marker Style */
|
||||
.custom-marker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.marker-dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.3);
|
||||
}
|
||||
.marker-online { background-color: #10b981; }
|
||||
.marker-offline { background-color: #64748b; }
|
||||
.marker-error { background-color: #ef4444; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="h-full bg-slate-50 dark:bg-slate-950 overflow-hidden">
|
||||
<div class="flex h-full relative">
|
||||
<!-- Sidebar -->
|
||||
<div class="hidden md:flex flex-col w-96 glass-panel border-r border-slate-200/50 dark:border-slate-800/50 z-20 overflow-hidden">
|
||||
<div class="p-8 border-b border-slate-200/50 dark:border-slate-800/50">
|
||||
<h1 class="text-2xl font-black text-slate-800 dark:text-white tracking-tight outfit-font">
|
||||
Star Cloud
|
||||
</h1>
|
||||
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] mt-2">
|
||||
{{ __('Machine Distribution') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-3 custom-scrollbar">
|
||||
@foreach($machines as $machine)
|
||||
<div class="p-4 rounded-2xl bg-white/50 dark:bg-slate-900/50 border border-slate-100 dark:border-slate-800 hover:border-cyan-500/30 transition-all cursor-pointer group"
|
||||
onclick="focusMachine({{ $machine->latitude }}, {{ $machine->longitude }}, {{ $machine->id }})">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-sm font-black text-slate-800 dark:text-white group-hover:text-cyan-500 transition-colors">
|
||||
{{ $machine->name }}
|
||||
</span>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="w-2 h-2 rounded-full {{ $machine->status === 'online' ? 'bg-emerald-500' : ($machine->status === 'offline' ? 'bg-slate-400' : 'bg-rose-500') }}"></span>
|
||||
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{{ __($machine->status) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs font-medium text-slate-500 dark:text-slate-400 leading-relaxed">
|
||||
{{ $machine->address ?: $machine->location ?: __('No address information') }}
|
||||
</p>
|
||||
<div class="mt-3 flex items-center gap-2">
|
||||
<span class="text-[10px] font-mono text-slate-400 bg-slate-100 dark:bg-slate-800 px-2 py-0.5 rounded shadow-sm">
|
||||
{{ $machine->serial_no }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="p-6 border-t border-slate-200/50 dark:border-slate-800/50 bg-slate-50/50 dark:bg-slate-900/50">
|
||||
<div class="flex items-center justify-between text-[10px] font-black text-slate-400 uppercase tracking-widest">
|
||||
<span>{{ __('Total Machines') }}</span>
|
||||
<span class="text-slate-800 dark:text-white">{{ $machines->count() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map Container -->
|
||||
<div class="flex-1 relative h-full">
|
||||
<div id="map"></div>
|
||||
|
||||
<!-- Floating Top Header (Mobile) -->
|
||||
<div class="md:hidden absolute top-4 left-4 right-4 z-[1000]">
|
||||
<div class="glass-panel p-4 rounded-2xl luxury-shadow border border-slate-200/50 dark:border-slate-800/50 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-lg font-black text-slate-800 dark:text-white tracking-tight outfit-font line-clamp-1">
|
||||
{{ __('Machine Distribution Map') }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-xl bg-cyan-500/10 flex items-center justify-center text-cyan-500 border border-cyan-500/20">
|
||||
<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="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
|
||||
<script>
|
||||
const machines = @json($machines);
|
||||
const map = L.map('map', {
|
||||
zoomControl: false
|
||||
}).setView([23.973875, 120.982024], 8); // Center of Taiwan
|
||||
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
||||
subdomains: 'abcd',
|
||||
maxZoom: 20
|
||||
}).addTo(map);
|
||||
|
||||
L.control.zoom({
|
||||
position: 'bottomright'
|
||||
}).addTo(map);
|
||||
|
||||
const markers = {};
|
||||
|
||||
machines.forEach(machine => {
|
||||
if (machine.latitude && machine.longitude) {
|
||||
const statusClass = `marker-${machine.status}`;
|
||||
const icon = L.divIcon({
|
||||
className: 'custom-marker',
|
||||
html: `<div class="marker-dot ${statusClass}"></div>`,
|
||||
iconSize: [20, 20],
|
||||
iconAnchor: [10, 10]
|
||||
});
|
||||
|
||||
const marker = L.marker([machine.latitude, machine.longitude], { icon: icon }).addTo(map);
|
||||
|
||||
const popupContent = `
|
||||
<div class="p-2 min-w-[200px]">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div class="w-2 h-2 rounded-full ${machine.status === 'online' ? 'bg-emerald-500' : (machine.status === 'offline' ? 'bg-slate-400' : 'bg-rose-500')}"></div>
|
||||
<span class="text-sm font-black text-slate-800">${machine.name}</span>
|
||||
</div>
|
||||
<p class="text-[11px] font-medium text-slate-500 mb-2">${machine.address || machine.location || ''}</p>
|
||||
<div class="flex items-center justify-between border-t border-slate-100 pt-2 mt-2">
|
||||
<span class="text-[9px] font-black text-slate-400 uppercase tracking-widest">${machine.serial_no}</span>
|
||||
<span class="text-[9px] font-bold text-cyan-600 uppercase tracking-widest">${machine.status.toUpperCase()}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
marker.bindPopup(popupContent, {
|
||||
maxWidth: 300,
|
||||
className: 'luxury-popup'
|
||||
});
|
||||
|
||||
markers[machine.id] = marker;
|
||||
}
|
||||
});
|
||||
|
||||
function focusMachine(lat, lng, id) {
|
||||
map.flyTo([lat, lng], 15, {
|
||||
duration: 1.5
|
||||
});
|
||||
setTimeout(() => {
|
||||
if (markers[id]) {
|
||||
markers[id].openPopup();
|
||||
}
|
||||
}, 1600);
|
||||
}
|
||||
|
||||
// Adjust map on window resize
|
||||
window.addEventListener('resize', () => {
|
||||
map.invalidateSize();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -130,6 +130,7 @@
|
||||
<label class="block text-xs font-bold text-slate-400 uppercase tracking-[0.15em] mb-2">{{ __('Location') }}</label>
|
||||
<input type="text" name="location" value="{{ old('location', $machine->location) }}" class="luxury-input w-full" placeholder="{{ __('e.g., Taipei Station') }}">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-400 uppercase tracking-[0.15em] mb-2">{{ __('Machine Model') }} <span class="text-rose-500">*</span></label>
|
||||
<x-searchable-select
|
||||
@ -159,6 +160,37 @@
|
||||
@error('company_id') <p class="mt-1 text-xs text-rose-500 font-bold uppercase tracking-wider">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div x-data="machineGeocoding()" class="col-span-full grid grid-cols-1 md:grid-cols-4 gap-6 pt-6 border-t border-slate-100 dark:border-slate-800">
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-xs font-bold text-slate-400 uppercase tracking-[0.15em] mb-2">{{ __('Address') }}</label>
|
||||
<div class="flex gap-2">
|
||||
<input type="text" name="address" x-model="address" class="luxury-input w-full" placeholder="{{ __('Enter full address') }}">
|
||||
<button type="button" @click="geocode()" :disabled="loading" class="btn-luxury-ghost py-2 px-3 flex items-center gap-2 whitespace-nowrap min-w-[120px] justify-center">
|
||||
<template x-if="!loading">
|
||||
<div class="flex items-center gap-2">
|
||||
<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="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
||||
<span>{{ __('Resolve Coordinates') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="loading">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="animate-spin h-4 w-4 text-indigo-500" 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>
|
||||
<span>{{ __('Searching...') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-400 uppercase tracking-[0.15em] mb-2">{{ __('Latitude') }}</label>
|
||||
<input type="number" step="any" name="latitude" value="{{ old('latitude', $machine->latitude) }}" class="luxury-input w-full" placeholder="{{ __('e.g., 25.0330') }}">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-400 uppercase tracking-[0.15em] mb-2">{{ __('Longitude') }}</label>
|
||||
<input type="number" step="any" name="longitude" value="{{ old('longitude', $machine->longitude) }}" class="luxury-input w-full" placeholder="{{ __('e.g., 121.5654') }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -388,3 +420,46 @@
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
function machineGeocoding() {
|
||||
return {
|
||||
address: @js($machine->address ?? ''),
|
||||
loading: false,
|
||||
async geocode() {
|
||||
if (!this.address) {
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message: @js(__('Please enter address first')), type: 'error' } }));
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await fetch(@js(route('admin.basic-settings.machines.geocode')), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ address: this.address })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
document.querySelector('input[name=latitude]').value = parseFloat(data.lat).toFixed(6);
|
||||
document.querySelector('input[name=longitude]').value = parseFloat(data.lon).toFixed(6);
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message: @js(__('Location found!')), type: 'success' } }));
|
||||
} else {
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message: @js(__('Location not found')), type: 'error' } }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Geocoding error:', error);
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message: @js(__('Error searching location')), type: 'error' } }));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
|
||||
@ -309,6 +309,12 @@
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Management of operational parameters and models') }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="{{ route('machines.distribution') }}" target="_blank" class="btn-luxury-ghost flex items-center gap-2">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 6.75V15m6-10.5v.106c0 .707-.555 1.296-1.244 1.333a5.232 5.232 0 0 0-4.834 4.834C8.889 11.418 8.299 12 7.591 12H7.5m0 0v7.5m0-7.5h.75m.75 0h.375c.49 0 .959.122 1.374.339a5.251 5.251 0 0 1 2.625 4.547v.105c0 .708.555 1.296 1.244 1.333a5.232 5.232 0 0 0 4.834-4.834c.038-.689.627-1.279 1.335-1.279h.75M15 6.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 18.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0Z" />
|
||||
</svg>
|
||||
<span>{{ __('Machine Distribution') }}</span>
|
||||
</a>
|
||||
<button x-show="tab === 'machines'" x-cloak @click="showCreateMachineModal = true" class="btn-luxury-primary flex items-center gap-2">
|
||||
<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" />
|
||||
@ -474,6 +480,37 @@
|
||||
@endforeach
|
||||
</x-searchable-select>
|
||||
</div>
|
||||
|
||||
<div x-data="machineGeocoding()" class="col-span-full grid grid-cols-1 md:grid-cols-4 gap-6 pt-6 border-t border-slate-100 dark:border-slate-800">
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Address') }}</label>
|
||||
<div class="flex gap-2">
|
||||
<input type="text" name="address" x-model="address" class="luxury-input w-full" placeholder="{{ __('Enter full address') }}">
|
||||
<button type="button" @click="geocode()" :disabled="loading" class="btn-luxury-ghost py-2 px-3 flex items-center gap-2 whitespace-nowrap min-w-[120px] justify-center">
|
||||
<template x-if="!loading">
|
||||
<div class="flex items-center gap-2">
|
||||
<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="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
||||
<span>{{ __('Resolve Coordinates') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="loading">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="animate-spin h-4 w-4 text-indigo-500" 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>
|
||||
<span>{{ __('Searching...') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Latitude') }}</label>
|
||||
<input type="number" step="any" name="latitude" class="luxury-input w-full" placeholder="{{ __('e.g., 25.0330') }}">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Longitude') }}</label>
|
||||
<input type="number" step="any" name="longitude" class="luxury-input w-full" placeholder="{{ __('e.g., 121.5654') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">
|
||||
@ -1155,4 +1192,46 @@
|
||||
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
function machineGeocoding() {
|
||||
return {
|
||||
address: '',
|
||||
loading: false,
|
||||
async geocode() {
|
||||
if (!this.address) {
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message: @js(__('Please enter address first')), type: 'error' } }));
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await fetch(@js(route('admin.basic-settings.machines.geocode')), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ address: this.address })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
document.querySelector('#create_modal input[name=latitude]').value = parseFloat(data.lat).toFixed(6);
|
||||
document.querySelector('#create_modal input[name=longitude]').value = parseFloat(data.lon).toFixed(6);
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message: @js(__('Location found!')), type: 'success' } }));
|
||||
} else {
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message: @js(__('Location not found')), type: 'error' } }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Geocoding error:', error);
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message: @js(__('Error searching location')), type: 'error' } }));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
@ -416,7 +416,6 @@ $roleSelectConfig = [
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Modal -->
|
||||
<div x-show="showModal" class="fixed inset-0 z-[100] overflow-y-auto" x-cloak>
|
||||
|
||||
@ -1,28 +1,157 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-3 pb-20" x-data="{
|
||||
<div class="relative space-y-3 pb-20" x-data="{
|
||||
showModal: false,
|
||||
editing: false,
|
||||
isDeleteConfirmOpen: false,
|
||||
isStatusConfirmOpen: false,
|
||||
deleteFormAction: '',
|
||||
toggleFormAction: '',
|
||||
currentWarehouse: { id: '', name: '', type: 'branch', address: '', is_active: 1 },
|
||||
isLoading: false,
|
||||
isSubmitting: false,
|
||||
currentWarehouse: { id: '', company_id: '', name: '', type: 'branch', address: '', is_active: 1 },
|
||||
openCreateModal() {
|
||||
this.editing = false;
|
||||
this.currentWarehouse = { id: '', name: '', type: 'branch', address: '', is_active: 1 };
|
||||
this.currentWarehouse = { id: '', company_id: '{{ auth()->user()->company_id }}', name: '', type: 'branch', address: '', is_active: 1 };
|
||||
this.showModal = true;
|
||||
},
|
||||
openEditModal(wh) {
|
||||
this.editing = true;
|
||||
this.currentWarehouse = { ...wh };
|
||||
// Ensure searchable-select updates for company_id if present
|
||||
this.$nextTick(() => {
|
||||
if (window.HSSelect && document.getElementById('company_select')) {
|
||||
window.HSSelect.getInstance('#company_select')?.setValue(wh.company_id?.toString() || '');
|
||||
}
|
||||
});
|
||||
this.showModal = true;
|
||||
},
|
||||
submitConfirmedForm() {
|
||||
this.$refs.statusToggleForm.submit();
|
||||
},
|
||||
async submitForm() {
|
||||
if (this.isSubmitting) return;
|
||||
this.isSubmitting = true;
|
||||
|
||||
const form = this.$refs.warehouseForm;
|
||||
const formData = new FormData(form);
|
||||
const action = form.action;
|
||||
const method = formData.get('_method') || 'POST';
|
||||
|
||||
try {
|
||||
const response = await fetch(action, {
|
||||
method: method,
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name=\'csrf-token\']').content,
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
window.Alpine.store('toast').show(data.message || '{{ __('Operation successful') }}', 'success');
|
||||
this.showModal = false;
|
||||
this.fetchPage(window.location.href);
|
||||
} else if (response.status === 422) {
|
||||
const errors = data.errors;
|
||||
Object.values(errors).forEach(errGroup => {
|
||||
errGroup.forEach(msg => {
|
||||
window.Alpine.store('toast').show(msg, 'error');
|
||||
});
|
||||
});
|
||||
} else {
|
||||
window.Alpine.store('toast').show(data.message || '{{ __('Operation failed') }}', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Submit failed:', error);
|
||||
window.Alpine.store('toast').show('{{ __('Network error') }}', 'error');
|
||||
} finally {
|
||||
this.isSubmitting = false;
|
||||
}
|
||||
},
|
||||
async fetchPage(url) {
|
||||
if (!url) return;
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'text/html'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Network response was not ok');
|
||||
|
||||
const html = await response.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
const newContent = doc.querySelector('#ajax-content-container');
|
||||
const currentContent = document.querySelector('#ajax-content-container');
|
||||
if (newContent && currentContent) {
|
||||
currentContent.innerHTML = newContent.innerHTML;
|
||||
}
|
||||
|
||||
window.history.pushState({}, '', url);
|
||||
this.$nextTick(() => {
|
||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching page:', error);
|
||||
window.showToast?.('載入失敗,請重試', 'error');
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
showInventoryPanel: false,
|
||||
inventoryLoading: false,
|
||||
inventoryStocks: [],
|
||||
inventorySearch: '',
|
||||
activeWarehouse: { id: '', name: '' },
|
||||
async openInventoryPanel(wh) {
|
||||
this.activeWarehouse = wh;
|
||||
this.showInventoryPanel = true;
|
||||
this.inventorySearch = '';
|
||||
await this.fetchInventory();
|
||||
},
|
||||
async fetchInventory() {
|
||||
this.inventoryLoading = true;
|
||||
try {
|
||||
const url = `/admin/warehouses/${this.activeWarehouse.id}/inventory-ajax?search=${this.inventorySearch}`;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
this.inventoryStocks = data.stocks;
|
||||
}
|
||||
} catch (e) { console.error(e); }
|
||||
finally { this.inventoryLoading = false; }
|
||||
}
|
||||
}">
|
||||
}" @ajax:navigate.window.prevent="fetchPage($event.detail.url); $event.stopImmediatePropagation();">
|
||||
<!-- Global Loading Overlay -->
|
||||
<div x-show="isLoading"
|
||||
class="absolute inset-0 z-[60] flex flex-col items-center justify-center bg-white/40 dark:bg-slate-900/40 backdrop-blur-[1px] 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-[12px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{
|
||||
__('Loading Data') }}...</p>
|
||||
</div>
|
||||
{{-- Header --}}
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
@ -30,7 +159,9 @@
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Manage your warehouses, including main and branch warehouses') }}</p>
|
||||
</div>
|
||||
<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>
|
||||
<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>{{ __('Add Warehouse') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
@ -56,27 +187,39 @@
|
||||
</div>
|
||||
|
||||
{{-- Table Card --}}
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 400ms">
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 400ms"
|
||||
id="ajax-content-container"
|
||||
:class="{ 'opacity-30 pointer-events-none transition-opacity duration-300': isLoading }">
|
||||
{{-- Filters --}}
|
||||
<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="relative group"
|
||||
@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">
|
||||
<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" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
<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" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<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="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) }}">
|
||||
</form>
|
||||
|
||||
<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">
|
||||
<a href="{{ route('admin.warehouses.index') }}"
|
||||
<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">
|
||||
<a href="{{ route('admin.warehouses.index') }}" @click.prevent="fetchPage($el.getAttribute('href'))"
|
||||
class="px-5 py-2 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ !request()->filled('type') && !request()->filled('status') ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-lg shadow-cyan-500/10' : 'text-slate-400 dark:text-slate-500 hover:text-slate-600 dark:hover:text-slate-300' }}">
|
||||
{{ __('All') }}
|
||||
</a>
|
||||
<a href="{{ route('admin.warehouses.index', ['type' => 'main']) }}"
|
||||
@click.prevent="fetchPage($el.getAttribute('href'))"
|
||||
class="px-5 py-2 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ request('type') === 'main' ? 'bg-white dark:bg-cyan-500/10 text-cyan-500 shadow-lg shadow-cyan-500/10' : 'text-slate-400 dark:text-slate-500 hover:text-cyan-500/80' }}">
|
||||
{{ __('Main') }}
|
||||
</a>
|
||||
<a href="{{ route('admin.warehouses.index', ['type' => 'branch']) }}"
|
||||
@click.prevent="fetchPage($el.getAttribute('href'))"
|
||||
class="px-5 py-2 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ request('type') === 'branch' ? 'bg-white dark:bg-indigo-500/10 text-indigo-500 shadow-lg shadow-indigo-500/10' : 'text-slate-400 dark:text-slate-500 hover:text-indigo-500/80' }}">
|
||||
{{ __('Branch') }}
|
||||
</a>
|
||||
@ -88,58 +231,103 @@
|
||||
<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 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Warehouse Info') }}</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">{{ __('Type') }}</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">{{ __('Products / Stock') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Status') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</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">
|
||||
{{ __('Warehouse Info') }}</th>
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<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-left">
|
||||
{{ __('Company Name') }}</th>
|
||||
@endif
|
||||
<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">
|
||||
{{ __('Type') }}</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">
|
||||
{{ __('Products / Stock') }}</th>
|
||||
<th
|
||||
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
|
||||
{{ __('Status') }}</th>
|
||||
<th
|
||||
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">
|
||||
{{ __('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||
@forelse($warehouses as $warehouse)
|
||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||
<td class="px-6 py-5">
|
||||
<td class="px-6 py-5 cursor-pointer group/item" @click="openInventoryPanel({ id: '{{ $warehouse->id }}', name: '{{ addslashes($warehouse->name) }}' })">
|
||||
<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">
|
||||
<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="M8.25 21v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21m0 0h4.5V3.545M12.75 21h7.5V10.75M2.25 21h1.5m18 0h-18M2.25 9l4.5-1.636M18.75 3l-1.5.545m0 6.205 3 1m1.5.5-1.5-.5M6.75 7.364V3h-3v18m3-13.636 10.5-3.819" /></svg>
|
||||
<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">
|
||||
<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="M8.25 21v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21m0 0h4.5V3.545M12.75 21h7.5V10.75M2.25 21h1.5m18 0h-18M2.25 9l4.5-1.636M18.75 3l-1.5.545m0 6.205 3 1m1.5.5-1.5-.5M6.75 7.364V3h-3v18m3-13.636 10.5-3.819" />
|
||||
</svg>
|
||||
</div>
|
||||
<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">{{ $warehouse->name }}</span>
|
||||
<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">{{
|
||||
$warehouse->name }}</span>
|
||||
@if($warehouse->address)
|
||||
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 mt-0.5 tracking-wide">{{ $warehouse->address }}</span>
|
||||
<span
|
||||
class="text-xs font-bold text-slate-500 dark:text-slate-400 mt-0.5 tracking-wide">{{
|
||||
$warehouse->address }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<td class="px-6 py-5 whitespace-nowrap">
|
||||
<span class="text-sm font-bold text-slate-600 dark:text-slate-300">
|
||||
{{ $warehouse->company->name ?? __('System Default') }}
|
||||
</span>
|
||||
</td>
|
||||
@endif
|
||||
<td class="px-6 py-5 text-center">
|
||||
@if($warehouse->type === 'main')
|
||||
<div class="mt-1 flex flex-col items-center gap-1.5">
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-xl text-xs font-black bg-cyan-400/20 text-cyan-400 border border-cyan-400/30 uppercase tracking-widest shadow-sm shadow-cyan-400/10">{{ __('Main') }}</span>
|
||||
<span
|
||||
class="inline-flex items-center px-2.5 py-1 rounded-xl text-xs font-black bg-cyan-400/20 text-cyan-400 border border-cyan-400/30 uppercase tracking-widest shadow-sm shadow-cyan-400/10">{{
|
||||
__('Main') }}</span>
|
||||
</div>
|
||||
@else
|
||||
<div class="mt-1 flex flex-col items-center gap-1.5">
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-xl text-xs font-black bg-indigo-400/20 text-indigo-400 border border-indigo-400/30 uppercase tracking-widest shadow-sm shadow-indigo-400/10">{{ __('Branch') }}</span>
|
||||
<span
|
||||
class="inline-flex items-center px-2.5 py-1 rounded-xl text-xs font-black bg-indigo-400/20 text-indigo-400 border border-indigo-400/30 uppercase tracking-widest shadow-sm shadow-indigo-400/10">{{
|
||||
__('Branch') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-5 text-center">
|
||||
<div class="flex items-center justify-center gap-x-3">
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-base font-extrabold text-slate-800 dark:text-white">{{ $warehouse->products_count ?? 0 }}</span>
|
||||
<span class="text-[11px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Products') }}</span>
|
||||
<span class="text-base font-extrabold text-slate-800 dark:text-white">{{
|
||||
$warehouse->products_count ?? 0 }}</span>
|
||||
<span
|
||||
class="text-[11px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{
|
||||
__('Products') }}</span>
|
||||
</div>
|
||||
<div class="w-px h-6 bg-slate-100 dark:bg-slate-800"></div>
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-base font-extrabold text-slate-800 dark:text-white">{{ (int)($warehouse->total_stock ?? 0) }}</span>
|
||||
<span class="text-[11px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Stock') }}</span>
|
||||
<span class="text-base font-extrabold text-slate-800 dark:text-white">{{
|
||||
(int)($warehouse->total_stock ?? 0) }}</span>
|
||||
<span
|
||||
class="text-[11px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{
|
||||
__('Stock') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-5 text-center">
|
||||
@if($warehouse->is_active)
|
||||
<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 tracking-widest uppercase shadow-sm shadow-emerald-500/5">{{ __('Active') }}</span>
|
||||
<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 tracking-widest uppercase shadow-sm shadow-emerald-500/5">{{
|
||||
__('Active') }}</span>
|
||||
@else
|
||||
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-slate-400/10 text-slate-400 border border-slate-400/20 tracking-widest uppercase">{{ __('Disabled') }}</span>
|
||||
<span
|
||||
class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-slate-400/10 text-slate-400 border border-slate-400/20 tracking-widest uppercase">{{
|
||||
__('Disabled') }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-5 text-right">
|
||||
@ -149,36 +337,68 @@
|
||||
@click="toggleFormAction = '{{ route('admin.warehouses.toggle-status', $warehouse->id) }}'; isStatusConfirmOpen = true"
|
||||
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-amber-500 hover:bg-amber-500/5 transition-all border border-transparent hover:border-amber-500/20"
|
||||
title="{{ __('Disable') }}">
|
||||
<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="M15.75 5.25v13.5m-7.5-13.5v13.5" /></svg>
|
||||
<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="M15.75 5.25v13.5m-7.5-13.5v13.5" />
|
||||
</svg>
|
||||
</button>
|
||||
@else
|
||||
<button type="button"
|
||||
@click="toggleFormAction = '{{ route('admin.warehouses.toggle-status', $warehouse->id) }}'; $nextTick(() => $refs.statusToggleForm.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"
|
||||
title="{{ __('Enable') }}">
|
||||
<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="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 0 1 0 1.971l-11.54 6.347c-.75.412-1.667-.13-1.667-.986V5.653z" /></svg>
|
||||
<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="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 0 1 0 1.971l-11.54 6.347c-.75.412-1.667-.13-1.667-.986V5.653z" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
<button @click="openEditModal({{ json_encode($warehouse) }})"
|
||||
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="{{ __('Edit') }}">
|
||||
<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="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" /></svg>
|
||||
<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="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button"
|
||||
@click="deleteFormAction = '{{ route('admin.warehouses.destroy', $warehouse->id) }}'; isDeleteConfirmOpen = true"
|
||||
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" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" /></svg>
|
||||
<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="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button"
|
||||
@click="openInventoryPanel({ id: '{{ $warehouse->id }}', name: '{{ addslashes($warehouse->name) }}' })"
|
||||
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 Inventory') }}">
|
||||
<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>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="px-6 py-20 text-center">
|
||||
<td colspan="{{ auth()->user()->isSystemAdmin() ? 6 : 5 }}" class="px-6 py-20 text-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="w-16 h-16 rounded-2xl bg-slate-50 dark:bg-slate-900 flex items-center justify-center text-slate-300 mb-4">
|
||||
<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="M8.25 21v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21m0 0h4.5V3.545M12.75 21h7.5V10.75M2.25 21h1.5m18 0h-18M2.25 9l4.5-1.636M18.75 3l-1.5.545m0 6.205 3 1m1.5.5-1.5-.5M6.75 7.364V3h-3v18m3-13.636 10.5-3.819" /></svg>
|
||||
<div
|
||||
class="w-16 h-16 rounded-2xl bg-slate-50 dark:bg-slate-900 flex items-center justify-center text-slate-300 mb-4">
|
||||
<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="M8.25 21v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21m0 0h4.5V3.545M12.75 21h7.5V10.75M2.25 21h1.5m18 0h-18M2.25 9l4.5-1.636M18.75 3l-1.5.545m0 6.205 3 1m1.5.5-1.5-.5M6.75 7.364V3h-3v18m3-13.636 10.5-3.819" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-slate-400 font-bold">{{ __('No warehouses found') }}</p>
|
||||
</div>
|
||||
@ -198,26 +418,47 @@
|
||||
{{-- Create/Edit Modal --}}
|
||||
<div x-show="showModal" class="fixed inset-0 z-[100] 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="showModal" 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="showModal = false"></div>
|
||||
<div x-show="showModal" 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="showModal = false">
|
||||
</div>
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
||||
<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="inline-block px-8 py-10 overflow-visible text-left align-bottom transition-all transform luxury-card rounded-3xl dark:bg-slate-900 border-slate-200/50 dark:border-slate-700/50 shadow-2xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||
<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="inline-block px-8 py-10 overflow-visible text-left align-bottom transform luxury-card rounded-3xl dark:bg-slate-900 border-slate-200/50 dark:border-slate-700/50 shadow-2xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight" x-text="editing ? '{{ __('Edit Warehouse') }}' : '{{ __('Add Warehouse') }}'"></h3>
|
||||
<button @click="showModal = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight"
|
||||
x-text="editing ? '{{ __('Edit Warehouse') }}' : '{{ __('Add Warehouse') }}'"></h3>
|
||||
<button @click="showModal = false"
|
||||
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<form x-ref="warehouseForm" :action="editing ? '{{ url('admin/warehouses') }}/' + currentWarehouse.id : '{{ route('admin.warehouses.store') }}'" method="POST" class="space-y-6">
|
||||
<form x-ref="warehouseForm"
|
||||
:action="editing ? '{{ url('admin/warehouses') }}/' + currentWarehouse.id : '{{ route('admin.warehouses.store') }}'"
|
||||
method="POST" @submit.prevent="submitForm()" class="space-y-6">
|
||||
@csrf
|
||||
<input type="hidden" name="_method" :value="editing ? 'PUT' : 'POST'">
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Warehouse Name') }} <span class="text-rose-500">*</span></label>
|
||||
<input type="text" name="name" x-model="currentWarehouse.name" required class="luxury-input w-full" placeholder="{{ __('e.g. Main Warehouse') }}">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
|
||||
__('Warehouse Name') }} <span class="text-rose-500">*</span></label>
|
||||
<input type="text" name="name" x-model="currentWarehouse.name"
|
||||
class="luxury-input w-full" placeholder="{{ __('e.g. Main Warehouse') }}">
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Warehouse Type') }} <span class="text-rose-500">*</span></label>
|
||||
<div class="flex p-1.5 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-700 w-fit">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
|
||||
__('Warehouse Type') }} <span class="text-rose-500">*</span></label>
|
||||
<div
|
||||
class="flex p-1.5 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-700 w-fit">
|
||||
<button type="button" @click="currentWarehouse.type = 'main'"
|
||||
:class="currentWarehouse.type === 'main' ? 'bg-cyan-500 text-white shadow-lg shadow-cyan-500/20' : 'text-slate-400 hover:text-slate-600'"
|
||||
class="px-5 py-2 rounded-lg text-xs font-bold uppercase tracking-widest transition-all">
|
||||
@ -231,29 +472,57 @@
|
||||
</div>
|
||||
<input type="hidden" name="type" :value="currentWarehouse.type">
|
||||
</div>
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Address') }}</label>
|
||||
<input type="text" name="address" x-model="currentWarehouse.address" class="luxury-input w-full" placeholder="{{ __('Optional') }}">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
|
||||
__('Company Name') }}</label>
|
||||
<x-searchable-select id="company_select" name="company_id" :options="$companies"
|
||||
x-model="currentWarehouse.company_id" :placeholder="__('Select Company')"
|
||||
class="w-full" />
|
||||
</div>
|
||||
@else
|
||||
<input type="hidden" name="company_id" :value="currentWarehouse.company_id">
|
||||
@endif
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
|
||||
__('Address') }}</label>
|
||||
<input type="text" name="address" x-model="currentWarehouse.address"
|
||||
class="luxury-input w-full" placeholder="{{ __('Optional') }}">
|
||||
</div>
|
||||
<template x-if="editing">
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Status') }}</label>
|
||||
<div class="flex p-1.5 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-700 w-fit">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
|
||||
__('Status') }}</label>
|
||||
<div
|
||||
class="flex p-1.5 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-700 w-fit">
|
||||
<button type="button" @click="currentWarehouse.is_active = 1"
|
||||
:class="currentWarehouse.is_active == 1 ? 'bg-emerald-500 text-white shadow-lg shadow-emerald-500/20' : 'text-slate-400 hover:text-slate-600'"
|
||||
class="px-5 py-2 rounded-lg text-xs font-bold uppercase tracking-widest transition-all">{{ __('Active') }}</button>
|
||||
class="px-5 py-2 rounded-lg text-xs font-bold uppercase tracking-widest transition-all">{{
|
||||
__('Active') }}</button>
|
||||
<button type="button" @click="currentWarehouse.is_active = 0"
|
||||
:class="currentWarehouse.is_active == 0 ? 'bg-rose-500 text-white shadow-lg shadow-rose-500/20' : 'text-slate-400 hover:text-slate-600'"
|
||||
class="px-5 py-2 rounded-lg text-xs font-bold uppercase tracking-widest transition-all">{{ __('Disabled') }}</button>
|
||||
class="px-5 py-2 rounded-lg text-xs font-bold uppercase tracking-widest transition-all">{{
|
||||
__('Disabled') }}</button>
|
||||
</div>
|
||||
<input type="hidden" name="is_active" :value="currentWarehouse.is_active ? 1 : 0">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex justify-end gap-x-4 pt-8">
|
||||
<button type="button" @click="showModal = false" class="btn-luxury-ghost px-8">{{ __('Cancel') }}</button>
|
||||
<button type="submit" class="btn-luxury-primary px-12">
|
||||
<span x-text="editing ? '{{ __('Update') }}' : '{{ __('Create') }}'"></span>
|
||||
<button type="button" @click="showModal = false" class="btn-luxury-ghost px-8">{{ __('Cancel')
|
||||
}}</button>
|
||||
<button type="submit" class="btn-luxury-primary px-12" :disabled="isSubmitting">
|
||||
<template x-if="!isSubmitting">
|
||||
<span x-text="editing ? '{{ __('Update') }}' : '{{ __('Create') }}'"></span>
|
||||
</template>
|
||||
<template x-if="isSubmitting">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
||||
<span>{{ __('Processing...') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@ -268,5 +537,122 @@
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
<!-- Inventory Offcanvas Panel -->
|
||||
<div x-show="showInventoryPanel" class="fixed inset-0 z-[100] overflow-hidden" style="display: none;"
|
||||
aria-labelledby="inventory-panel-title" role="dialog" aria-modal="true" x-cloak>
|
||||
|
||||
<!-- Background backdrop -->
|
||||
<div x-show="showInventoryPanel" x-transition:enter="ease-in-out duration-300"
|
||||
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="ease-in-out duration-300" 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="showInventoryPanel = false">
|
||||
</div>
|
||||
|
||||
<div class="fixed inset-y-0 right-0 max-w-full flex">
|
||||
<!-- Sliding panel -->
|
||||
<div x-show="showInventoryPanel"
|
||||
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="w-screen max-w-2xl">
|
||||
|
||||
<div class="h-full flex flex-col bg-white dark:bg-slate-900 shadow-2xl">
|
||||
<!-- Header -->
|
||||
<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 gap-4">
|
||||
<div class="min-w-0 flex-1">
|
||||
<h2 id="inventory-panel-title"
|
||||
class="text-xl sm:text-2xl font-black text-slate-800 dark:text-white font-display flex items-center gap-2 sm:gap-3">
|
||||
<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="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
<span class="truncate">{{ __('Warehouse Inventory') }}</span>
|
||||
</h2>
|
||||
<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="activeWarehouse.name" class="text-cyan-600 dark:text-cyan-400"></span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" @click="showInventoryPanel = false"
|
||||
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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="mt-6 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>
|
||||
<input type="text" x-model="inventorySearch" @input.debounce.300ms="fetchInventory()"
|
||||
placeholder="{{ __('Search products...') }}"
|
||||
class="luxury-input py-2.5 pl-12 pr-6 block w-full bg-white dark:bg-slate-800">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="flex-1 overflow-y-auto p-6 sm:p-8">
|
||||
<div class="relative min-h-[200px]">
|
||||
<!-- Loading State -->
|
||||
<div x-show="inventoryLoading"
|
||||
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>
|
||||
|
||||
<!-- Inventory List -->
|
||||
<div class="space-y-4">
|
||||
<template x-for="item in inventoryStocks" :key="item.id">
|
||||
<div class="luxury-card p-4 rounded-2xl flex items-center gap-4 group/item transition-all hover:border-cyan-500/30">
|
||||
<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">
|
||||
<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="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</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-slate-800 dark:text-white" x-text="item.quantity"></p>
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('In Stock') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Empty State -->
|
||||
<template x-if="inventoryStocks.length === 0 && !inventoryLoading">
|
||||
<div class="py-12 text-center">
|
||||
<p class="text-slate-400 font-bold uppercase tracking-widest">{{ __('No products found in this warehouse') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@ -1,275 +1,636 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-3 pb-20" x-data="{
|
||||
activeTab: '{{ $tab }}',
|
||||
showStockInModal: false,
|
||||
items: [{ product_id: '', quantity: 1 }],
|
||||
addItem() { this.items.push({ product_id: '', quantity: 1 }); },
|
||||
removeItem(i) { if (this.items.length > 1) this.items.splice(i, 1); },
|
||||
}">
|
||||
{{-- Header --}}
|
||||
<script>
|
||||
function inventoryManager() {
|
||||
return {
|
||||
activeTab: '{{ $tab }}',
|
||||
tabLoading: null,
|
||||
loading: false,
|
||||
showStockInModal: false,
|
||||
showConfirmModal: false,
|
||||
pendingForm: null,
|
||||
items: [{ product_id: '', quantity: 1 }],
|
||||
|
||||
// Order Details (Renamed to match index.blade.php style)
|
||||
showOrderDetails: false,
|
||||
detailsLoading: false,
|
||||
activeOrder: null,
|
||||
activeItems: [],
|
||||
|
||||
openStockInModal() {
|
||||
this.items = [{ product_id: '', quantity: 1 }];
|
||||
this.showStockInModal = true;
|
||||
this.$nextTick(() => {
|
||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
||||
});
|
||||
},
|
||||
|
||||
async openOrderDetails(id) {
|
||||
console.log('openOrderDetails triggered for ID:', id);
|
||||
this.showOrderDetails = true;
|
||||
this.detailsLoading = true;
|
||||
this.activeOrder = null;
|
||||
this.activeItems = [];
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/warehouses/inventory/stock-in/${id}/details`, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
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;
|
||||
}
|
||||
},
|
||||
|
||||
init() {
|
||||
this.$watch('activeTab', (newTab, oldTab) => {
|
||||
const container = document.getElementById('tab-' + newTab + '-container');
|
||||
if (container && container.innerHTML.trim() === '') {
|
||||
this.fetchTabData(newTab);
|
||||
}
|
||||
|
||||
// Update URL
|
||||
const url = new URL(window.location.origin + window.location.pathname);
|
||||
url.searchParams.set('tab', newTab);
|
||||
window.history.pushState({}, '', url);
|
||||
});
|
||||
},
|
||||
|
||||
addItem() {
|
||||
this.items.push({ product_id: '', quantity: 1 });
|
||||
this.$nextTick(() => {
|
||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
||||
});
|
||||
},
|
||||
removeItem(i) { if (this.items.length > 1) this.items.splice(i, 1); },
|
||||
|
||||
async fetchTabData(tab, url = null) {
|
||||
this.tabLoading = tab;
|
||||
const container = document.getElementById('tab-' + tab + '-container');
|
||||
|
||||
if (!url) {
|
||||
const form = container?.querySelector('form');
|
||||
let params = new URLSearchParams();
|
||||
params.set('tab', tab);
|
||||
params.set('_ajax', '1');
|
||||
|
||||
if (form) {
|
||||
const formData = new FormData(form);
|
||||
formData.forEach((value, key) => {
|
||||
if (value.trim() !== '') params.set(key, value);
|
||||
});
|
||||
}
|
||||
url = `${window.location.pathname}?${params.toString()}`;
|
||||
} else {
|
||||
const urlObj = new URL(url, window.location.origin);
|
||||
urlObj.searchParams.set('tab', tab);
|
||||
urlObj.searchParams.set('_ajax', '1');
|
||||
url = urlObj.toString();
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
if (container) {
|
||||
container.innerHTML = data.html;
|
||||
this.$nextTick(() => {
|
||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit();
|
||||
});
|
||||
}
|
||||
|
||||
const historyUrl = new URL(url, window.location.origin);
|
||||
historyUrl.searchParams.delete('_ajax');
|
||||
window.history.pushState({}, '', historyUrl.toString());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
window.showToast?.('{{ __("Loading failed") }}', 'error');
|
||||
} finally {
|
||||
this.tabLoading = null;
|
||||
}
|
||||
},
|
||||
|
||||
async submitStockIn() {
|
||||
const form = document.getElementById('stockInForm');
|
||||
const warehouseId = form.querySelector('[name="warehouse_id"]')?.value;
|
||||
|
||||
if (!warehouseId) {
|
||||
window.showToast?.('{{ __("Please select target warehouse") }}', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.items.length === 0) {
|
||||
window.showToast?.('{{ __("Please add at least one item") }}', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
let hasError = false;
|
||||
this.items.forEach(item => {
|
||||
if (!item.product_id || !item.quantity || item.quantity < 1) {
|
||||
hasError = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasError) {
|
||||
window.showToast?.('{{ __("Please select product and valid quantity for all items") }}', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const formData = new FormData(form);
|
||||
const response = await fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
window.showToast?.(result.message, 'success');
|
||||
this.showStockInModal = false;
|
||||
this.fetchTabData('stock-in');
|
||||
} else {
|
||||
window.showToast?.(result.message || '{{ __("Validation failed") }}', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
window.showToast?.('{{ __("System Error") }}', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
handleFilterSubmit(tab) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('page');
|
||||
this.fetchTabData(tab);
|
||||
},
|
||||
confirmStockIn(form) {
|
||||
this.pendingForm = form;
|
||||
this.showConfirmModal = true;
|
||||
},
|
||||
|
||||
async doConfirmStockIn() {
|
||||
if (!this.pendingForm) return;
|
||||
|
||||
const form = this.pendingForm;
|
||||
this.showConfirmModal = false;
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(form.action, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: new FormData(form)
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success || response.ok) {
|
||||
window.showToast?.('{{ __("Confirmed") }}', 'success');
|
||||
this.fetchTabData('stock-in');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
window.showToast?.('{{ __("Operation failed") }}', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
this.pendingForm = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative space-y-2 pb-20" x-data="inventoryManager()" data-tab="{{ $tab }}"
|
||||
data-index-url="{{ route('admin.warehouses.inventory') }}"
|
||||
@ajax:navigate.window.prevent="fetchTabData(activeTab, $event.detail.url)">
|
||||
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Inventory Management') }}</h1>
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Track stock levels, stock-in orders, and movement history') }}</p>
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">
|
||||
{{ __('Track stock-in orders and movement history') }}
|
||||
</p>
|
||||
</div>
|
||||
<div x-show="activeTab === 'stock-in'">
|
||||
<button @click="showStockInModal = true; items = [{ product_id: '', quantity: 1 }]" 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 Stock-In Order') }}</span>
|
||||
</button>
|
||||
<div class="flex items-center gap-3">
|
||||
<template x-if="activeTab === 'stock-in'">
|
||||
<button type="button" @click="openStockInModal()"
|
||||
class="btn-luxury-primary transition-all duration-300">
|
||||
<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 Stock-In Order') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Tab Navigation --}}
|
||||
<div class="flex items-center p-1.5 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">
|
||||
<a href="{{ route('admin.warehouses.inventory', ['tab' => 'stock']) }}"
|
||||
class="px-6 py-2.5 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ $tab === 'stock' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-lg shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600' }}">
|
||||
{{ __('Stock Levels') }}
|
||||
</a>
|
||||
<a href="{{ route('admin.warehouses.inventory', ['tab' => 'stock-in']) }}"
|
||||
class="px-6 py-2.5 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ $tab === 'stock-in' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-lg shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600' }}">
|
||||
<div class="flex items-center gap-1 p-1.5 bg-slate-100 dark:bg-slate-900/50 rounded-2xl w-fit border border-slate-200/50 dark:border-slate-800/50"
|
||||
aria-label="Tabs">
|
||||
<button type="button" @click="activeTab = 'stock'"
|
||||
:class="activeTab === 'stock' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200'"
|
||||
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all duration-300">
|
||||
{{ __('Current Stocks') }}
|
||||
</button>
|
||||
<button type="button" @click="activeTab = 'stock-in'"
|
||||
:class="activeTab === 'stock-in' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200'"
|
||||
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all duration-300">
|
||||
{{ __('Stock-In Orders') }}
|
||||
</a>
|
||||
<a href="{{ route('admin.warehouses.inventory', ['tab' => 'movements']) }}"
|
||||
class="px-6 py-2.5 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ $tab === 'movements' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-lg shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600' }}">
|
||||
</button>
|
||||
<button type="button" @click="activeTab = 'movements'"
|
||||
:class="activeTab === 'movements' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200'"
|
||||
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all duration-300">
|
||||
{{ __('Movement History') }}
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Tab: Stock Levels --}}
|
||||
@if($tab === 'stock')
|
||||
<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">
|
||||
<input type="hidden" name="tab" value="stock">
|
||||
<div class="relative group">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none"><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" value="{{ request('search') }}" class="py-2.5 pl-12 pr-6 block w-64 luxury-input" placeholder="{{ __('Search products...') }}">
|
||||
<!-- Tab Contents -->
|
||||
<div class="mt-6">
|
||||
<!-- Stock List Tab -->
|
||||
<div x-show="activeTab === 'stock'" class="relative min-h-[300px]"
|
||||
x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4"
|
||||
x-transition:enter-end="opacity-100 translate-y-0" x-cloak>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<div x-show="tabLoading === 'stock'" 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>
|
||||
<div class="min-w-[200px]">
|
||||
<select name="warehouse_id" class="luxury-select w-full" onchange="this.form.submit()">
|
||||
<option value="">{{ __('All Warehouses') }}</option>
|
||||
@foreach($warehouses as $wh)
|
||||
<option value="{{ $wh->id }}" {{ request('warehouse_id') == $wh->id ? 'selected' : '' }}>{{ $wh->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<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 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Product') }}</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">{{ __('Warehouse') }}</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">{{ __('Quantity') }}</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">{{ __('Safety Stock') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Status') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||
@forelse($stocks as $stock)
|
||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||
<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 overflow-hidden">
|
||||
@if($stock->product?->image_url)
|
||||
<img src="{{ $stock->product->image_url }}" class="w-full h-full object-cover">
|
||||
@else
|
||||
<svg class="w-5 h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" /></svg>
|
||||
@endif
|
||||
</div>
|
||||
<span class="font-extrabold text-slate-800 dark:text-slate-100">{{ $stock->product?->name ?? '-' }}</span>
|
||||
<div id="tab-stock-container" class="relative">
|
||||
@if($tab === 'stock') @include('admin.warehouses.partials.tab-stock') @endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Stock-In Orders Tab -->
|
||||
<div x-show="activeTab === 'stock-in'" class="relative min-h-[300px]"
|
||||
x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4"
|
||||
x-transition:enter-end="opacity-100 translate-y-0" x-cloak>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<div x-show="tabLoading === 'stock-in'" 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>
|
||||
|
||||
<div id="tab-stock-in-container" class="relative">
|
||||
@if($tab === 'stock-in') @include('admin.warehouses.partials.tab-stock-in') @endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Movements Tab -->
|
||||
<div x-show="activeTab === 'movements'" class="relative min-h-[300px]"
|
||||
x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4"
|
||||
x-transition:enter-end="opacity-100 translate-y-0" x-cloak>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<div x-show="tabLoading === 'movements'" 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>
|
||||
|
||||
<div id="tab-movements-container" class="relative">
|
||||
@if($tab === 'movements') @include('admin.warehouses.partials.tab-movements') @endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stock-In Modal -->
|
||||
<div x-show="showStockInModal" 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">
|
||||
<!-- Background Backdrop -->
|
||||
<div x-show="showStockInModal" 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="showStockInModal = false"></div>
|
||||
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:min-h-screen" aria-hidden="true">​</span>
|
||||
|
||||
<!-- Modal Panel -->
|
||||
<div x-show="showStockInModal"
|
||||
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 Stock-In Order') }}
|
||||
</h3>
|
||||
<p class="text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">
|
||||
{{ __('Add new inventory items to your warehouse') }}
|
||||
</p>
|
||||
</div>
|
||||
<button @click="showStockInModal = 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="stockInForm" action="{{ route('admin.warehouses.inventory.stock-in.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">
|
||||
{{ __('Target Warehouse') }} <span class="text-rose-500">*</span>
|
||||
</label>
|
||||
<x-searchable-select
|
||||
name="warehouse_id"
|
||||
:options="$warehouses ?? []"
|
||||
:selected="request('warehouse_id')"
|
||||
:placeholder="__('Select Warehouse')"
|
||||
/>
|
||||
</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 py-3" placeholder="{{ __('Optional remarks') }}">
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-5">
|
||||
<span class="text-sm font-bold text-slate-600 dark:text-slate-300">{{ $stock->warehouse?->name ?? '-' }}</span>
|
||||
@if($stock->warehouse?->type === 'main')
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-lg text-xs font-black bg-cyan-400/20 text-cyan-400 border border-cyan-400/30 uppercase tracking-widest ml-2 shadow-sm shadow-cyan-400/10">{{ __('Main') }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-5 text-center">
|
||||
<span class="text-lg font-black {{ $stock->quantity <= $stock->safety_stock && $stock->safety_stock > 0 ? 'text-rose-500' : 'text-slate-800 dark:text-white' }}">{{ $stock->quantity }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-5 text-center">
|
||||
<span class="text-sm font-bold text-slate-500">{{ $stock->safety_stock }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-5 text-center">
|
||||
@if($stock->safety_stock > 0 && $stock->quantity <= $stock->safety_stock)
|
||||
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-rose-500/10 text-rose-500 border border-rose-500/20 tracking-widest uppercase">{{ __('Low Stock') }}</span>
|
||||
@elseif($stock->quantity === 0)
|
||||
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-slate-400/10 text-slate-400 border border-slate-400/20 tracking-widest uppercase">{{ __('Out of Stock') }}</span>
|
||||
@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 tracking-widest uppercase">{{ __('Normal') }}</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="5" class="px-6 py-20 text-center text-slate-400 font-bold">{{ __('No stock data found') }}</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">{{ $stocks->links('vendor.pagination.luxury') }}</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Tab: Stock-In Orders --}}
|
||||
@if($tab === 'stock-in')
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
<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">{{ __('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">{{ __('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">{{ __('Created At') }}</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"><span class="font-mono font-extrabold text-slate-800 dark:text-slate-100">{{ $order->order_no }}</span></td>
|
||||
<td class="px-6 py-5"><span class="font-bold text-slate-600 dark:text-slate-300">{{ $order->warehouse?->name }}</span></td>
|
||||
<td class="px-6 py-5 text-center">
|
||||
@if($order->status === 'draft')
|
||||
<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 tracking-widest uppercase">{{ __('Draft') }}</span>
|
||||
@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 tracking-widest uppercase">{{ __('Completed') }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-5"><span class="text-sm font-bold text-slate-500">{{ $order->creator?->name ?? '-' }}</span></td>
|
||||
<td class="px-6 py-5"><span class="text-sm font-bold text-slate-500">{{ $order->created_at?->format('Y-m-d H:i') }}</span></td>
|
||||
<td class="px-6 py-5 text-right">
|
||||
@if($order->status === 'draft')
|
||||
<form action="{{ route('admin.warehouses.inventory.stock-in.confirm', $order->id) }}" method="POST" class="inline" onsubmit="return confirm('{{ __('Confirm this stock-in order?') }}')">
|
||||
@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="6" class="px-6 py-20 text-center text-slate-400 font-bold">{{ __('No stock-in 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>
|
||||
@endif
|
||||
|
||||
{{-- Tab: Movement History --}}
|
||||
@if($tab === 'movements')
|
||||
<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">
|
||||
<input type="hidden" name="tab" value="movements">
|
||||
<select name="warehouse_id" class="luxury-select min-w-[200px]" onchange="this.form.submit()">
|
||||
<option value="">{{ __('All Warehouses') }}</option>
|
||||
@foreach($warehouses as $wh)
|
||||
<option value="{{ $wh->id }}" {{ request('warehouse_id') == $wh->id ? 'selected' : '' }}>{{ $wh->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<select name="type" class="luxury-select min-w-[180px]" onchange="this.form.submit()">
|
||||
<option value="">{{ __('All Types') }}</option>
|
||||
@foreach(\App\Models\Warehouse\StockMovement::TYPE_LABELS as $key => $label)
|
||||
<option value="{{ $key }}" {{ request('type') === $key ? 'selected' : '' }}>{{ __($label) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</form>
|
||||
|
||||
<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">{{ __('Time') }}</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">{{ __('Product') }}</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 text-center">{{ __('Qty Change') }}</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">{{ __('Operator') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||
@forelse($movements as $mv)
|
||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||
<td class="px-6 py-4 text-sm font-bold text-slate-500">{{ $mv->created_at?->format('m-d H:i') }}</td>
|
||||
<td class="px-6 py-4 font-bold text-slate-700 dark:text-slate-200">{{ $mv->warehouse?->name }}</td>
|
||||
<td class="px-6 py-4 font-bold text-slate-700 dark:text-slate-200">{{ $mv->product?->name }}</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
@php $typeColor = in_array($mv->type, ['in', 'transfer_in']) ? 'emerald' : (in_array($mv->type, ['out', 'transfer_out']) ? 'rose' : 'amber'); @endphp
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black bg-{{ $typeColor }}-500/10 text-{{ $typeColor }}-500 border border-{{ $typeColor }}-500/20 tracking-widest uppercase">{{ __(\App\Models\Warehouse\StockMovement::TYPE_LABELS[$mv->type] ?? $mv->type) }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center font-mono font-extrabold {{ in_array($mv->type, ['in', 'transfer_in']) ? 'text-emerald-500' : 'text-rose-500' }}">
|
||||
{{ in_array($mv->type, ['in', 'transfer_in']) ? '+' : '-' }}{{ $mv->quantity }}
|
||||
<span class="text-xs font-bold text-slate-400 ml-1">({{ $mv->before_qty }}→{{ $mv->after_qty }})</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm font-bold text-slate-500">{{ $mv->creator?->name ?? '-' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="6" class="px-6 py-20 text-center text-slate-400 font-bold">{{ __('No movement records found') }}</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">{{ $movements->links('vendor.pagination.luxury') }}</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Stock-In Modal --}}
|
||||
<div x-show="showStockInModal" class="fixed inset-0 z-[100] overflow-y-auto" x-cloak>
|
||||
<div class="flex items-center justify-center min-h-screen px-4">
|
||||
<div x-show="showStockInModal" 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="showStockInModal = false"></div>
|
||||
<div x-show="showStockInModal" x-transition class="relative inline-block px-8 py-10 luxury-card rounded-3xl shadow-2xl sm:max-w-2xl w-full z-10">
|
||||
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight mb-8">{{ __('New Stock-In Order') }}</h3>
|
||||
<form action="{{ route('admin.warehouses.inventory.stock-in.store') }}" method="POST" class="space-y-6">
|
||||
@csrf
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Target Warehouse') }}</label>
|
||||
<select name="warehouse_id" required class="luxury-select w-full">
|
||||
<option value="">{{ __('Select Warehouse') }}</option>
|
||||
@foreach($warehouses ?? [] as $wh)
|
||||
<option value="{{ $wh->id }}">{{ $wh->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Note') }}</label>
|
||||
<input type="text" name="note" class="luxury-input w-full" placeholder="{{ __('Optional') }}">
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Items') }}</label>
|
||||
<button type="button" @click="addItem()" class="text-xs font-black text-cyan-500 hover:text-cyan-400 uppercase tracking-widest">+ {{ __('Add Item') }}</button>
|
||||
</div>
|
||||
<template x-for="(item, index) in items" :key="index">
|
||||
<div class="flex items-center gap-3 p-4 bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-100 dark:border-slate-800">
|
||||
<select :name="'items['+index+'][product_id]'" x-model="item.product_id" required class="luxury-select flex-1">
|
||||
<option value="">{{ __('Select Product') }}</option>
|
||||
@foreach($products ?? [] as $p)
|
||||
<option value="{{ $p->id }}">{{ $p->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<input type="number" :name="'items['+index+'][quantity]'" x-model="item.quantity" min="1" required class="luxury-input w-24 text-center" placeholder="{{ __('Qty') }}">
|
||||
<button type="button" @click="removeItem(index)" class="p-2 text-slate-400 hover:text-rose-500 transition-colors" x-show="items.length > 1">
|
||||
<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="M6 18 18 6M6 6l12 12" /></svg>
|
||||
|
||||
<!-- Products Section -->
|
||||
<div class="space-y-4 pt-4">
|
||||
<div class="flex items-center justify-between px-1">
|
||||
<h4 class="text-sm font-black text-slate-800 dark:text-white uppercase tracking-widest">{{ __('Product List') }}</h4>
|
||||
<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">
|
||||
<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>
|
||||
{{ __('Add Product') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<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="flex-1 space-y-2">
|
||||
<label class="block text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Product') }}</label>
|
||||
<x-searchable-select
|
||||
x-bind:name="'items['+index+'][product_id]'"
|
||||
:options="$products ?? []"
|
||||
placeholder="{{ __('Select Product') }}"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-32 space-y-2">
|
||||
<label class="block text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Quantity') }}</label>
|
||||
<input type="number" x-model="item.quantity" x-bind:name="'items['+index+'][quantity]'" min="1"
|
||||
class="luxury-input py-2.5">
|
||||
</div>
|
||||
<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">
|
||||
<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>
|
||||
</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="showStockInModal = false" class="btn-luxury-ghost px-8">
|
||||
{{ __('Cancel') }}
|
||||
</button>
|
||||
<button type="button" @click="submitStockIn()"
|
||||
:disabled="loading"
|
||||
class="btn-luxury-primary px-12 relative flex items-center justify-center">
|
||||
<span :class="loading ? 'opacity-0' : ''">{{ __('Confirm Stock-In') }}</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>
|
||||
|
||||
<x-confirm-modal
|
||||
alpineVar="showConfirmModal"
|
||||
confirmAction="doConfirmStockIn()"
|
||||
iconType="info"
|
||||
confirmColor="emerald"
|
||||
:title="__('Confirm Stock-in Order')"
|
||||
:message="__('Are you sure you want to confirm this stock-in order? This action will update your inventory levels.')"
|
||||
:confirmText="__('Confirm')"
|
||||
/>
|
||||
|
||||
<!-- Order Details Slide-over (Rewritten to match index.blade.php structure) -->
|
||||
<div x-show="showOrderDetails" class="fixed inset-0 z-[100] overflow-hidden" style="display: none;" x-cloak>
|
||||
<div class="absolute inset-0 overflow-hidden">
|
||||
<!-- Backdrop -->
|
||||
<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 transition-opacity"
|
||||
@click="showOrderDetails = false"></div>
|
||||
|
||||
<div class="fixed inset-y-0 right-0 max-w-full flex">
|
||||
<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="w-screen max-w-2xl">
|
||||
|
||||
<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 -->
|
||||
<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 gap-4">
|
||||
<div class="min-w-0 flex-1">
|
||||
<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">
|
||||
<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="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
<span class="truncate">{{ __('Order 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>
|
||||
<span class="mx-1">•</span>
|
||||
<span x-text="activeOrder.warehouse_name"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<button type="button" @click="showOrderDetails = false"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 overflow-y-auto p-6 sm:p-8">
|
||||
<div class="relative min-h-[200px]">
|
||||
<!-- Loading State -->
|
||||
<div x-show="detailsLoading"
|
||||
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>
|
||||
|
||||
<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 dark:text-slate-500 uppercase tracking-widest">{{ __('Status') }}</p>
|
||||
<div>
|
||||
<template x-if="activeOrder.status === 'draft'">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-lg text-[10px] font-black bg-amber-500/10 text-amber-500 border border-amber-500/20 uppercase tracking-widest">{{ __('Draft') }}</span>
|
||||
</template>
|
||||
<template x-if="activeOrder.status === 'completed'">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-lg text-[10px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 uppercase tracking-widest">{{ __('Completed') }}</span>
|
||||
</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="activeOrder.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="activeOrder.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">{{ __('Approved At') }}</p>
|
||||
<p class="text-[11px] 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-[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="activeOrder.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">{{ __('Product Details') }}</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="luxury-card p-4 rounded-2xl flex items-center gap-4 group/item transition-all hover:border-cyan-500/30">
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-4 pt-6">
|
||||
<button type="button" @click="showStockInModal = false" class="btn-luxury-ghost px-8">{{ __('Cancel') }}</button>
|
||||
<button type="submit" class="btn-luxury-primary px-12">{{ __('Create') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@endsection
|
||||
@ -30,7 +30,7 @@
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
<form action="{{ route('admin.warehouses.machine-inventory') }}" method="GET" class="mb-8">
|
||||
<div class="relative group">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none"><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>
|
||||
<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>
|
||||
</form>
|
||||
|
||||
@ -0,0 +1,120 @@
|
||||
<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 justify-between mb-8 gap-4"
|
||||
@submit.prevent="handleFilterSubmit('movements')">
|
||||
<input type="hidden" name="tab" value="movements">
|
||||
|
||||
<div class="flex flex-col md:flex-row gap-4 items-center">
|
||||
<div class="w-64">
|
||||
<x-searchable-select
|
||||
name="warehouse_id"
|
||||
:options="$warehouses"
|
||||
:selected="request('warehouse_id')"
|
||||
:placeholder="__('All Warehouses')"
|
||||
x-on:change="handleFilterSubmit('movements')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-64">
|
||||
<x-searchable-select
|
||||
name="type"
|
||||
:selected="request('type')"
|
||||
:placeholder="__('All Types')"
|
||||
x-on:change="handleFilterSubmit('movements')"
|
||||
>
|
||||
@foreach(\App\Models\Warehouse\StockMovement::TYPE_LABELS as $key => $label)
|
||||
<option value="{{ $key }}" {{ request('type') === $key ? 'selected' : '' }} data-title="{{ __($label) }}">
|
||||
{{ __($label) }}
|
||||
</option>
|
||||
@endforeach
|
||||
</x-searchable-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<button type="button" @click="handleFilterSubmit('movements')" class="btn-luxury-secondary px-6">
|
||||
{{ __('Filter') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<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 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Product / Stock') }}</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">{{ __('Warehouse') }}</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">{{ __('Type') }}</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">{{ __('Qty Change') }}</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">{{ __('Time') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Operator') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||
@forelse($movements as $mv)
|
||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||
<td class="px-6 py-5">
|
||||
<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-indigo-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="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors">
|
||||
{{ $mv->product?->name }}
|
||||
</span>
|
||||
<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">
|
||||
ID: {{ $mv->product_id }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-5">
|
||||
<span class="text-sm font-bold text-slate-600 dark:text-slate-300 uppercase tracking-widest">
|
||||
{{ $mv->warehouse?->name }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-5 text-center whitespace-nowrap">
|
||||
@php $typeColor = in_array($mv->type, ['in', 'transfer_in']) ? 'emerald' : (in_array($mv->type, ['out', 'transfer_out']) ? 'rose' : 'amber'); @endphp
|
||||
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-{{ $typeColor }}-500/10 text-{{ $typeColor }}-500 border border-{{ $typeColor }}-500/20 tracking-widest uppercase shadow-sm shadow-{{ $typeColor }}-500/5">{{ __(\App\Models\Warehouse\StockMovement::TYPE_LABELS[$mv->type] ?? $mv->type) }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-5 text-center font-mono font-extrabold {{ in_array($mv->type, ['in', 'transfer_in']) ? 'text-emerald-500' : 'text-rose-500' }}">
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-lg tracking-tighter">{{ in_array($mv->type, ['in', 'transfer_in']) ? '+' : '-' }}{{ $mv->quantity }}</span>
|
||||
<div class="text-[10px] font-bold text-slate-400 dark:text-slate-500 tracking-tight">({{ $mv->before_qty }} → {{ $mv->after_qty }})</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-5 text-center whitespace-nowrap">
|
||||
<span class="text-[11px] font-mono font-bold text-slate-500 dark:text-slate-400 tracking-tighter uppercase">
|
||||
{{ $mv->created_at?->format('Y-m-d H:i') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-5 text-right whitespace-nowrap">
|
||||
<span class="text-xs font-bold text-slate-600 dark:text-slate-300 uppercase tracking-widest">
|
||||
{{ $mv->creator?->name ?? '-' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-20 text-center">
|
||||
<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">
|
||||
<svg class="w-8 h-8 text-slate-300 dark:text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-slate-400 font-bold uppercase tracking-widest text-xs">{{ __('No movement records found') }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-8 py-6 border-t border-slate-50 dark:border-slate-800/50">
|
||||
{{ $movements->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
</div>
|
||||
139
resources/views/admin/warehouses/partials/tab-stock-in.blade.php
Normal file
139
resources/views/admin/warehouses/partials/tab-stock-in.blade.php
Normal file
@ -0,0 +1,139 @@
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
{{-- Filters --}}
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-4">
|
||||
<form action="{{ route('admin.warehouses.inventory') }}" method="GET" class="relative group"
|
||||
@submit.prevent="handleFilterSubmit('stock-in')">
|
||||
<input type="hidden" name="tab" value="stock-in">
|
||||
<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" value="{{ request('search') }}"
|
||||
class="py-2.5 pl-12 pr-6 block w-64 luxury-input" placeholder="{{ __('Search order number...') }}">
|
||||
</form>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<button type="button" @click="handleFilterSubmit('stock-in')" class="btn-luxury-secondary px-6">
|
||||
{{ __('Filter') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Table --}}
|
||||
<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 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
|
||||
{{ __('Order Info') }}
|
||||
</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">
|
||||
{{ __('Warehouse') }}
|
||||
</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
|
||||
{{ __('Status') }}
|
||||
</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
|
||||
{{ __('Date') }}
|
||||
</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">
|
||||
{{ __('Actions') }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-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">
|
||||
<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">
|
||||
{{ $order->order_no }}
|
||||
</span>
|
||||
@if($order->note)
|
||||
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 mt-0.5 tracking-wide">
|
||||
{{ Str::limit($order->note, 30) }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-5 whitespace-nowrap">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm font-bold text-slate-600 dark:text-slate-300 uppercase tracking-widest">
|
||||
{{ $order->warehouse?->name }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-5 text-center whitespace-nowrap">
|
||||
@if($order->status === 'draft')
|
||||
<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') }}
|
||||
</span>
|
||||
@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">
|
||||
{{ __('Completed') }}
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-5 text-center whitespace-nowrap">
|
||||
<span class="text-[11px] font-mono font-bold text-slate-500 dark:text-slate-400 tracking-tighter">
|
||||
{{ $order->created_at->format('Y-m-d H:i') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-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-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')
|
||||
<form action="{{ route('admin.warehouses.inventory.stock-in.confirm', $order) }}" method="POST" class="inline" @submit.prevent="confirmStockIn($el)">
|
||||
@csrf
|
||||
<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"
|
||||
title="{{ __('Confirm Stock-In') }}">
|
||||
<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>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="px-6 py-20 text-center">
|
||||
<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">
|
||||
<svg class="w-8 h-8 text-slate-300 dark:text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-slate-400 font-bold uppercase tracking-widest text-xs">{{ __('No stock-in orders found') }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 py-6 border-t border-slate-50 dark:border-slate-800/50">
|
||||
{{ $orders->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,97 @@
|
||||
<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"
|
||||
@submit.prevent="handleFilterSubmit('stock')">
|
||||
<input type="hidden" name="tab" value="stock">
|
||||
<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>
|
||||
<input type="text" name="search" value="{{ request('search') }}" class="py-2.5 pl-12 pr-6 block w-64 luxury-input" placeholder="{{ __('Search products...') }}">
|
||||
</div>
|
||||
<div class="min-w-[240px]">
|
||||
<x-searchable-select
|
||||
name="warehouse_id"
|
||||
:options="$warehouses"
|
||||
:selected="request('warehouse_id')"
|
||||
:placeholder="__('Filter by Warehouse Presence')"
|
||||
@change="handleFilterSubmit('stock')"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="overflow-x-auto -mx-8 px-8 custom-scrollbar">
|
||||
<table class="w-full text-left border-separate border-spacing-y-0 min-w-max">
|
||||
<thead>
|
||||
<tr class="bg-slate-50/50 dark:bg-white/[0.02]">
|
||||
<th class="px-6 py-5 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 sticky left-0 bg-white dark:bg-[#1e293b] z-20">{{ __('Product') }}</th>
|
||||
@foreach($inventory_warehouses as $w)
|
||||
<th class="px-6 py-5 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">
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-slate-800 dark:text-slate-200">{{ $w->name }}</span>
|
||||
<span class="text-[9px] opacity-60 tracking-normal mt-0.5">{{ $w->type === 'main' ? __('Main') : __('Branch') }}</span>
|
||||
</div>
|
||||
</th>
|
||||
@endforeach
|
||||
<th class="px-6 py-5 text-xs font-bold text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center bg-cyan-50/30 dark:bg-cyan-500/5">{{ __('Total') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-white/[0.05]">
|
||||
@forelse($products as $product)
|
||||
@php $totalQuantity = $product->stocks->sum('quantity'); @endphp
|
||||
<tr class="group hover:bg-slate-50 dark:hover:bg-white/[0.03] transition-all duration-300">
|
||||
<td class="px-6 py-4 sticky left-0 bg-white dark:bg-[#1e293b] group-hover:bg-slate-50 dark:group-hover:bg-white/[0.03] z-10 transition-colors shadow-[4px_0_12px_-4px_rgba(0,0,0,0.05)] dark:shadow-[4px_0_12px_-4px_rgba(0,0,0,0.2)]">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-11 h-11 rounded-2xl bg-slate-100 dark:bg-slate-800/50 flex items-center justify-center overflow-hidden border border-slate-200/50 dark:border-white/5 group-hover:border-cyan-500/30 transition-colors">
|
||||
@if($product->image_url)
|
||||
<img src="{{ $product->image_url }}" class="w-full h-full object-cover">
|
||||
@else
|
||||
<svg class="w-5 h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" /></svg>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ $product->localized_name }}</span>
|
||||
<span class="text-[10px] font-bold text-slate-400 mt-0.5 tracking-tight">{{ $product->barcode ?? '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@foreach($inventory_warehouses as $w)
|
||||
@php
|
||||
$stock = $product->stocks->firstWhere('warehouse_id', $w->id);
|
||||
$isLow = $stock && $stock->safety_stock > 0 && $stock->quantity <= $stock->safety_stock;
|
||||
@endphp
|
||||
<td class="px-6 py-4 text-center">
|
||||
@if($stock && $stock->quantity > 0)
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-base font-black {{ $isLow ? 'text-rose-500 drop-shadow-[0_0_8px_rgba(244,63,94,0.2)]' : 'text-slate-700 dark:text-slate-200' }}">
|
||||
{{ $stock->quantity }}
|
||||
</span>
|
||||
@if($isLow)
|
||||
<span class="text-[9px] font-black text-rose-500 uppercase tracking-tighter opacity-80">{{ __('Low') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<span class="text-sm font-bold text-slate-300 dark:text-slate-700/50">-</span>
|
||||
@endif
|
||||
</td>
|
||||
@endforeach
|
||||
<td class="px-6 py-4 text-center bg-cyan-50/30 dark:bg-cyan-500/5 group-hover:bg-cyan-100/30 dark:group-hover:bg-cyan-500/10 transition-colors">
|
||||
<span class="text-xl font-mono font-black text-cyan-600 dark:text-cyan-400 drop-shadow-[0_0_10px_rgba(6,182,212,0.1)]">
|
||||
{{ $totalQuantity }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="{{ count($inventory_warehouses) + 2 }}" class="px-6 py-20 text-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="w-16 h-16 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center mb-4">
|
||||
<svg class="w-8 h-8 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</div>
|
||||
<p class="text-slate-400 font-bold tracking-widest uppercase">{{ __('No products found') }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">{{ $products->links('vendor.pagination.luxury') }}</div>
|
||||
</div>
|
||||
@ -1,19 +1,163 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-3 pb-20" x-data="{
|
||||
showModal: false,
|
||||
items: [{ product_id: '', slot_no: '', quantity: 1 }],
|
||||
addItem() { this.items.push({ product_id: '', slot_no: '', quantity: 1 }); },
|
||||
removeItem(i) { if (this.items.length > 1) this.items.splice(i, 1); },
|
||||
}">
|
||||
<script>
|
||||
function replenishmentManager() {
|
||||
return {
|
||||
showModal: false,
|
||||
fromId: '',
|
||||
availableProducts: [],
|
||||
allProducts: @json(\App\Models\Product\Product::orderBy('name')->get(['id', 'name'])),
|
||||
isLoadingProducts: false,
|
||||
items: [{ product_id: '', slot_no: '', quantity: 1 }],
|
||||
loading: false,
|
||||
|
||||
// Details Panel State
|
||||
showDetailsPanel: false,
|
||||
detailsLoading: false,
|
||||
selectedOrder: null,
|
||||
orderItems: [],
|
||||
|
||||
async openDetails(id) {
|
||||
this.showDetailsPanel = true;
|
||||
this.detailsLoading = true;
|
||||
this.selectedOrder = null;
|
||||
this.orderItems = [];
|
||||
|
||||
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(() => {
|
||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
openCreateModal() {
|
||||
this.items = [{ product_id: '', slot_no: '', quantity: 1 }];
|
||||
this.showModal = true;
|
||||
this.$nextTick(() => {
|
||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
||||
});
|
||||
},
|
||||
|
||||
addItem() {
|
||||
this.items.push({ product_id: '', slot_no: '', quantity: 1 });
|
||||
this.$nextTick(() => {
|
||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
||||
});
|
||||
},
|
||||
removeItem(i) { if (this.items.length > 1) this.items.splice(i, 1); },
|
||||
|
||||
async fetchStock() {
|
||||
this.availableProducts = [];
|
||||
if (!this.fromId || this.fromId.trim() === '') return;
|
||||
|
||||
this.isLoadingProducts = true;
|
||||
try {
|
||||
const res = await fetch('{{ route('admin.warehouses.ajax.stock') }}?warehouse_id=' + this.fromId);
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
this.availableProducts = json.data;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
this.isLoadingProducts = false;
|
||||
this.$nextTick(() => {
|
||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async submitReplenishment() {
|
||||
const form = document.getElementById('replenishmentForm');
|
||||
const warehouseId = form.querySelector('[name="warehouse_id"]')?.value;
|
||||
const machineId = form.querySelector('[name="machine_id"]')?.value;
|
||||
|
||||
if (!warehouseId) {
|
||||
window.showToast?.('{{ __("Please select source warehouse") }}', 'error');
|
||||
return;
|
||||
}
|
||||
if (!machineId) {
|
||||
window.showToast?.('{{ __("Please select target machine") }}', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.items.length === 0) {
|
||||
window.showToast?.('{{ __("Please add at least one item") }}', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
let hasError = false;
|
||||
this.items.forEach(item => {
|
||||
if (!item.slot_no || !item.product_id || !item.quantity || item.quantity < 1) {
|
||||
hasError = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasError) {
|
||||
window.showToast?.('{{ __("Please select slot, product and valid quantity for all items") }}', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const formData = new FormData(form);
|
||||
const response = await fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
window.showToast?.(result.message, 'success');
|
||||
this.showModal = false;
|
||||
window.location.reload();
|
||||
} else {
|
||||
window.showToast?.(result.message || '{{ __("Validation failed") }}', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
window.showToast?.('{{ __("System Error") }}', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-3 pb-10" x-data="replenishmentManager()" @open-details.window="openDetails($event.detail.id)">
|
||||
|
||||
{{-- Header --}}
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
<button @click="showModal = true; items = [{ product_id: '', slot_no: '', quantity: 1 }]" class="btn-luxury-primary">
|
||||
<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>
|
||||
@ -52,29 +196,45 @@
|
||||
<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"><span class="font-mono font-extrabold text-slate-800 dark:text-slate-100">{{ $order->order_no }}</span></td>
|
||||
<td class="px-6 py-5">
|
||||
<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-5"><span class="font-bold text-slate-600 dark:text-slate-300">{{ $order->warehouse?->name }}</span></td>
|
||||
<td class="px-6 py-5 text-center"><span class="font-black text-slate-800 dark:text-white">{{ $order->items_count }}</span></td>
|
||||
<td class="px-6 py-5 text-center">
|
||||
@php $sColor = match($order->status) { 'pending' => 'amber', 'prepared' => 'cyan', 'delivering' => 'indigo', 'completed' => 'emerald', 'cancelled' => 'slate', default => 'slate' }; @endphp
|
||||
<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-5"><span class="text-sm font-bold text-slate-500">{{ $order->creator?->name ?? '-' }}</span></td>
|
||||
<td class="px-6 py-5 text-right">
|
||||
@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>
|
||||
@else
|
||||
<span class="text-xs font-bold text-slate-400">{{ $order->completed_at?->format('Y-m-d H:i') }}</span>
|
||||
@endif
|
||||
<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
|
||||
@ -87,62 +247,299 @@
|
||||
</div>
|
||||
|
||||
{{-- New Replenishment Modal --}}
|
||||
<div x-show="showModal" class="fixed inset-0 z-[100] overflow-y-auto" x-cloak>
|
||||
<div class="flex items-center justify-center min-h-screen px-4">
|
||||
<div x-show="showModal" x-transition class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm" @click="showModal = false"></div>
|
||||
<div x-show="showModal" x-transition class="relative inline-block px-8 py-10 luxury-card rounded-3xl shadow-2xl sm:max-w-2xl w-full z-10">
|
||||
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight mb-8">{{ __('New Replenishment') }}</h3>
|
||||
<form action="{{ route('admin.warehouses.replenishments.store') }}" method="POST" class="space-y-6">
|
||||
<div x-show="showModal" 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">
|
||||
<!-- Background Backdrop -->
|
||||
<div x-show="showModal" 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="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-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Source Warehouse') }}</label>
|
||||
<select name="warehouse_id" required class="luxury-select w-full">
|
||||
<option value="">{{ __('Select Warehouse') }}</option>
|
||||
@foreach($warehouses as $wh)
|
||||
<option value="{{ $wh->id }}">{{ $wh->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<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-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Target Machine') }}</label>
|
||||
<select name="machine_id" required class="luxury-select w-full">
|
||||
<option value="">{{ __('Select Machine') }}</option>
|
||||
@foreach($machines as $m)
|
||||
<option value="{{ $m->id }}">{{ $m->name }} ({{ $m->serial_no }})</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<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>
|
||||
<input type="text" name="note" class="luxury-input w-full" placeholder="{{ __('Note (optional)') }}">
|
||||
{{-- Items --}}
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Replenishment Items') }}</label>
|
||||
<button type="button" @click="addItem()" class="text-xs font-black text-cyan-500 hover:text-cyan-400 uppercase tracking-widest">+ {{ __('Add Item') }}</button>
|
||||
</div>
|
||||
<template x-for="(item, index) in items" :key="index">
|
||||
<div class="flex items-center gap-3 p-4 bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-100 dark:border-slate-800">
|
||||
<input type="number" :name="'items['+index+'][slot_no]'" x-model="item.slot_no" min="1" required class="luxury-input w-20 text-center" placeholder="{{ __('Slot') }}">
|
||||
<select :name="'items['+index+'][product_id]'" x-model="item.product_id" required class="luxury-select flex-1">
|
||||
<option value="">{{ __('Select Product') }}</option>
|
||||
@foreach($machines->count() > 0 ? \App\Models\Product\Product::orderBy('name')->get(['id', 'name']) : [] as $p)
|
||||
<option value="{{ $p->id }}">{{ $p->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<input type="number" :name="'items['+index+'][quantity]'" x-model="item.quantity" min="1" required class="luxury-input w-20 text-center" placeholder="{{ __('Qty') }}">
|
||||
<button type="button" @click="removeItem(index)" class="p-2 text-slate-400 hover:text-rose-500 transition-colors" x-show="items.length > 1">
|
||||
<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="M6 18 18 6M6 6l12 12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<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>
|
||||
<div class="flex justify-end gap-4 pt-6">
|
||||
<button type="button" @click="showModal = false" class="btn-luxury-ghost px-8">{{ __('Cancel') }}</button>
|
||||
<button type="submit" class="btn-luxury-primary px-12">{{ __('Create') }}</button>
|
||||
|
||||
{{-- 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>
|
||||
|
||||
@ -1,20 +1,172 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-3 pb-20" x-data="{
|
||||
showModal: false,
|
||||
transferType: 'warehouse_to_warehouse',
|
||||
items: [{ product_id: '', quantity: 1 }],
|
||||
addItem() { this.items.push({ product_id: '', quantity: 1 }); },
|
||||
removeItem(i) { if (this.items.length > 1) this.items.splice(i, 1); },
|
||||
}">
|
||||
<script>
|
||||
function transferManager() {
|
||||
return {
|
||||
showModal: false,
|
||||
transferType: 'warehouse_to_warehouse',
|
||||
fromId: '',
|
||||
availableProducts: [],
|
||||
allProducts: @json($products ?? []),
|
||||
isLoadingProducts: false,
|
||||
items: [{ product_id: '', quantity: 1 }],
|
||||
loading: false,
|
||||
|
||||
// Details Panel State
|
||||
showDetailsPanel: false,
|
||||
selectedOrder: null,
|
||||
orderItems: [],
|
||||
loadingDetails: false,
|
||||
|
||||
openCreateModal() {
|
||||
this.items = [{ product_id: '', quantity: 1 }];
|
||||
this.showModal = true;
|
||||
this.$nextTick(() => {
|
||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
||||
});
|
||||
},
|
||||
|
||||
async openDetails(id) {
|
||||
this.loadingDetails = true;
|
||||
this.showDetailsPanel = true;
|
||||
this.selectedOrder = null;
|
||||
this.orderItems = [];
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/warehouses/transfers/${id}/details`);
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.selectedOrder = result.order;
|
||||
this.orderItems = result.items;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
} finally {
|
||||
this.loadingDetails = false;
|
||||
this.$nextTick(() => {
|
||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
addItem() {
|
||||
this.items.push({ product_id: '', quantity: 1 });
|
||||
this.$nextTick(() => {
|
||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
||||
});
|
||||
},
|
||||
removeItem(i) { if (this.items.length > 1) this.items.splice(i, 1); },
|
||||
|
||||
async fetchStock() {
|
||||
this.availableProducts = [];
|
||||
if (!this.fromId || this.fromId.trim() === '') return;
|
||||
|
||||
this.isLoadingProducts = true;
|
||||
try {
|
||||
const params = this.transferType === 'warehouse_to_warehouse'
|
||||
? 'warehouse_id=' + this.fromId
|
||||
: 'machine_id=' + this.fromId;
|
||||
const res = await fetch('{{ route('admin.warehouses.ajax.stock') }}?' + params);
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
this.availableProducts = json.data;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
this.isLoadingProducts = false;
|
||||
this.$nextTick(() => {
|
||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
resetFrom() {
|
||||
this.fromId = '';
|
||||
this.availableProducts = [];
|
||||
this.$nextTick(() => {
|
||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
||||
});
|
||||
},
|
||||
|
||||
async submitTransfer() {
|
||||
const form = document.getElementById('transferForm');
|
||||
const type = this.transferType;
|
||||
const fromId = form.querySelector(type === 'warehouse_to_warehouse' ? '[name="from_warehouse_id"]' : '[name="from_machine_id"]')?.value;
|
||||
const toId = form.querySelector('[name="to_warehouse_id"]')?.value;
|
||||
|
||||
if (!fromId) {
|
||||
window.showToast?.('{{ __("Please select source") }}', 'error');
|
||||
return;
|
||||
}
|
||||
if (!toId) {
|
||||
window.showToast?.('{{ __("Please select target warehouse") }}', 'error');
|
||||
return;
|
||||
}
|
||||
if (fromId === toId && type === 'warehouse_to_warehouse') {
|
||||
window.showToast?.('{{ __("Source and target warehouse cannot be the same") }}', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.items.length === 0) {
|
||||
window.showToast?.('{{ __("Please add at least one item") }}', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
let hasError = false;
|
||||
this.items.forEach(item => {
|
||||
if (!item.product_id || !item.quantity || item.quantity < 1) {
|
||||
hasError = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasError) {
|
||||
window.showToast?.('{{ __("Please select product and valid quantity for all items") }}', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const formData = new FormData(form);
|
||||
const response = await fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
window.showToast?.(result.message, 'success');
|
||||
this.showModal = false;
|
||||
window.location.reload();
|
||||
} else {
|
||||
window.showToast?.(result.message || '{{ __("Validation failed") }}', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
window.showToast?.('{{ __("System Error") }}', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-3 pb-10" x-data="transferManager()">
|
||||
|
||||
{{-- Header --}}
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Transfer Orders') }}</h1>
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Manage stock transfers between warehouses and machine returns') }}</p>
|
||||
</div>
|
||||
<button @click="showModal = true; items = [{ product_id: '', quantity: 1 }]" class="btn-luxury-primary">
|
||||
<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 Transfer') }}</span>
|
||||
</button>
|
||||
@ -23,17 +175,29 @@
|
||||
{{-- Table --}}
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
<form action="{{ route('admin.warehouses.transfers') }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 mb-8">
|
||||
<select name="type" class="luxury-select min-w-[200px]" onchange="this.form.submit()">
|
||||
<option value="">{{ __('All Types') }}</option>
|
||||
<option value="warehouse_to_warehouse" {{ request('type') === 'warehouse_to_warehouse' ? 'selected' : '' }}>{{ __('Warehouse to Warehouse') }}</option>
|
||||
<option value="machine_to_warehouse" {{ request('type') === 'machine_to_warehouse' ? 'selected' : '' }}>{{ __('Machine to Warehouse') }}</option>
|
||||
</select>
|
||||
<select name="status" class="luxury-select min-w-[160px]" onchange="this.form.submit()">
|
||||
<option value="">{{ __('All Statuses') }}</option>
|
||||
<option value="draft" {{ request('status') === 'draft' ? 'selected' : '' }}>{{ __('Draft') }}</option>
|
||||
<option value="completed" {{ request('status') === 'completed' ? 'selected' : '' }}>{{ __('Completed') }}</option>
|
||||
<option value="cancelled" {{ request('status') === 'cancelled' ? 'selected' : '' }}>{{ __('Cancelled') }}</option>
|
||||
</select>
|
||||
<div class="flex-1 md:max-w-[220px]">
|
||||
<x-searchable-select
|
||||
name="type"
|
||||
:selected="request('type')"
|
||||
:placeholder="__('All Types')"
|
||||
onchange="this.form.submit()"
|
||||
>
|
||||
<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>
|
||||
</x-searchable-select>
|
||||
</div>
|
||||
<div class="flex-1 md:max-w-[180px]">
|
||||
<x-searchable-select
|
||||
name="status"
|
||||
:selected="request('status')"
|
||||
:placeholder="__('All Statuses')"
|
||||
onchange="this.form.submit()"
|
||||
>
|
||||
<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="cancelled" {{ request('status') === 'cancelled' ? 'selected' : '' }} data-title="{{ __('Cancelled') }}">{{ __('Cancelled') }}</option>
|
||||
</x-searchable-select>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
@ -52,7 +216,7 @@
|
||||
<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"><span class="font-mono font-extrabold text-slate-800 dark:text-slate-100">{{ $order->order_no }}</span></td>
|
||||
<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>
|
||||
@ -60,16 +224,23 @@
|
||||
<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-5 font-bold text-slate-600 dark:text-slate-300">
|
||||
<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-5 font-bold text-slate-600 dark:text-slate-300">{{ $order->toWarehouse?->name ?? '-' }}</td>
|
||||
<td class="px-6 py-5 text-center font-black text-slate-800 dark:text-white">{{ $order->items->count() }}</td>
|
||||
<td class="px-6 py-5 text-center">
|
||||
@php $sColor = match($order->status) { 'draft' => 'amber', 'completed' => 'emerald', 'cancelled' => 'slate', default => 'cyan' }; @endphp
|
||||
<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-5 text-right">
|
||||
<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')
|
||||
@ -90,83 +261,336 @@
|
||||
</div>
|
||||
|
||||
{{-- New Transfer Modal --}}
|
||||
<div x-show="showModal" class="fixed inset-0 z-[100] overflow-y-auto" x-cloak>
|
||||
<div class="flex items-center justify-center min-h-screen px-4">
|
||||
<div x-show="showModal" x-transition class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm" @click="showModal = false"></div>
|
||||
<div x-show="showModal" x-transition class="relative inline-block px-8 py-10 luxury-card rounded-3xl shadow-2xl sm:max-w-2xl w-full z-10">
|
||||
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight mb-8">{{ __('New Transfer') }}</h3>
|
||||
<form action="{{ route('admin.warehouses.transfers.store') }}" method="POST" class="space-y-6">
|
||||
<div x-show="showModal" 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">
|
||||
<!-- Background Backdrop -->
|
||||
<div x-show="showModal" 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="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 Transfer') }}
|
||||
</h3>
|
||||
<p class="text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">
|
||||
{{ __('Create stock transfers between warehouses and machine returns') }}
|
||||
</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="transferForm" action="{{ route('admin.warehouses.transfers.store') }}" method="POST" class="space-y-6 pb-20">
|
||||
@csrf
|
||||
|
||||
{{-- Type Toggle --}}
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Transfer Type') }}</label>
|
||||
<div class="flex p-1.5 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-700 w-fit">
|
||||
<button type="button" @click="transferType = 'warehouse_to_warehouse'"
|
||||
:class="transferType === 'warehouse_to_warehouse' ? 'bg-indigo-500 text-white shadow-lg' : 'text-slate-400 hover:text-slate-600'"
|
||||
class="px-5 py-2 rounded-lg text-xs font-bold uppercase tracking-widest transition-all">{{ __('Warehouse to Warehouse') }}</button>
|
||||
<button type="button" @click="transferType = 'machine_to_warehouse'"
|
||||
:class="transferType === 'machine_to_warehouse' ? 'bg-amber-500 text-white shadow-lg' : 'text-slate-400 hover:text-slate-600'"
|
||||
class="px-5 py-2 rounded-lg text-xs font-bold uppercase tracking-widest transition-all">{{ __('Machine to Warehouse') }}</button>
|
||||
<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">
|
||||
{{ __('Transfer Type') }}
|
||||
</label>
|
||||
<div class="flex p-1.5 bg-slate-100 dark:bg-slate-900/50 rounded-2xl border border-slate-200/50 dark:border-slate-800/50 w-fit">
|
||||
<button type="button" @click="transferType = 'warehouse_to_warehouse'; resetFrom()"
|
||||
:class="transferType === 'warehouse_to_warehouse' ? 'bg-white dark:bg-slate-800 text-indigo-500 shadow-sm' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-300'"
|
||||
class="px-6 py-2.5 rounded-xl text-xs font-black uppercase tracking-widest transition-all duration-300">
|
||||
{{ __('Warehouse to Warehouse') }}
|
||||
</button>
|
||||
<button type="button" @click="transferType = 'machine_to_warehouse'; resetFrom()"
|
||||
:class="transferType === 'machine_to_warehouse' ? 'bg-white dark:bg-slate-800 text-amber-500 shadow-sm' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-300'"
|
||||
class="px-6 py-2.5 rounded-xl text-xs font-black uppercase tracking-widest transition-all duration-300">
|
||||
{{ __('Machine to Warehouse') }}
|
||||
</button>
|
||||
</div>
|
||||
<input type="hidden" name="type" :value="transferType">
|
||||
</div>
|
||||
{{-- From --}}
|
||||
<div x-show="transferType === 'warehouse_to_warehouse'" class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('From Warehouse') }}</label>
|
||||
<select name="from_warehouse_id" class="luxury-select w-full">
|
||||
<option value="">{{ __('Select Warehouse') }}</option>
|
||||
@foreach($warehouses as $wh)
|
||||
<option value="{{ $wh->id }}">{{ $wh->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div x-show="transferType === 'machine_to_warehouse'" class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('From Machine') }}</label>
|
||||
<select name="from_machine_id" class="luxury-select w-full">
|
||||
<option value="">{{ __('Select Machine') }}</option>
|
||||
@foreach($machines as $m)
|
||||
<option value="{{ $m->id }}">{{ $m->name }} ({{ $m->serial_no }})</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
{{-- To --}}
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('To Warehouse') }}</label>
|
||||
<select name="to_warehouse_id" required class="luxury-select w-full">
|
||||
<option value="">{{ __('Select Warehouse') }}</option>
|
||||
@foreach($warehouses as $wh)
|
||||
<option value="{{ $wh->id }}">{{ $wh->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<input type="text" name="note" class="luxury-input w-full" placeholder="{{ __('Note (optional)') }}">
|
||||
{{-- Items --}}
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Items') }}</label>
|
||||
<button type="button" @click="addItem()" class="text-xs font-black text-cyan-500 hover:text-cyan-400 uppercase tracking-widest">+ {{ __('Add Item') }}</button>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{{-- From --}}
|
||||
<div x-show="transferType === 'warehouse_to_warehouse'" class="space-y-3">
|
||||
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] pl-1">
|
||||
{{ __('From Warehouse') }} <span class="text-rose-500">*</span>
|
||||
</label>
|
||||
<x-searchable-select
|
||||
name="from_warehouse_id"
|
||||
:options="$warehouses ?? []"
|
||||
:placeholder="__('Select Warehouse')"
|
||||
class="w-full"
|
||||
x-model="fromId"
|
||||
@change="fetchStock()"
|
||||
/>
|
||||
</div>
|
||||
<div x-show="transferType === 'machine_to_warehouse'" class="space-y-3">
|
||||
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] pl-1">
|
||||
{{ __('From Machine') }} <span class="text-rose-500">*</span>
|
||||
</label>
|
||||
<x-searchable-select
|
||||
name="from_machine_id"
|
||||
:options="$machines->map(fn($m) => (object)['id' => $m->id, 'name' => $m->name . ' (' . $m->serial_no . ')'])"
|
||||
:placeholder="__('Select Machine')"
|
||||
class="w-full"
|
||||
x-model="fromId"
|
||||
@change="fetchStock()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{-- To --}}
|
||||
<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">
|
||||
{{ __('To Warehouse') }} <span class="text-rose-500">*</span>
|
||||
</label>
|
||||
<x-searchable-select
|
||||
name="to_warehouse_id"
|
||||
:options="$warehouses ?? []"
|
||||
:placeholder="__('Select Warehouse')"
|
||||
class="w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<template x-for="(item, index) in items" :key="index">
|
||||
<div class="flex items-center gap-3 p-4 bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-100 dark:border-slate-800">
|
||||
<select :name="'items['+index+'][product_id]'" x-model="item.product_id" required class="luxury-select flex-1">
|
||||
<option value="">{{ __('Select Product') }}</option>
|
||||
@foreach($products as $p)
|
||||
<option value="{{ $p->id }}">{{ $p->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<input type="number" :name="'items['+index+'][quantity]'" x-model="item.quantity" min="1" required class="luxury-input w-24 text-center">
|
||||
<button type="button" @click="removeItem(index)" class="p-2 text-slate-400 hover:text-rose-500 transition-colors" x-show="items.length > 1">
|
||||
<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="M6 18 18 6M6 6l12 12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex justify-end gap-4 pt-6">
|
||||
<button type="button" @click="showModal = false" class="btn-luxury-ghost px-8">{{ __('Cancel') }}</button>
|
||||
<button type="submit" class="btn-luxury-primary px-12">{{ __('Create') }}</button>
|
||||
|
||||
<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-4">
|
||||
<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]">
|
||||
{{ __('Item List') }} <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="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-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">
|
||||
<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="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-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>
|
||||
<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="submitTransfer()"
|
||||
: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>
|
||||
{{-- Details Offcanvas --}}
|
||||
<div x-show="showDetailsPanel"
|
||||
class="fixed inset-0 z-[120] overflow-hidden"
|
||||
style="display: none;"
|
||||
x-cloak>
|
||||
<div class="absolute inset-0 overflow-hidden">
|
||||
<div class="absolute inset-0 bg-slate-900/40 backdrop-blur-sm transition-opacity"
|
||||
@click="showDetailsPanel = false"
|
||||
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"></div>
|
||||
|
||||
<div class="fixed inset-y-0 right-0 flex max-w-full pl-10">
|
||||
<div class="w-screen max-w-md transform transition duration-500 ease-in-out sm:max-w-lg"
|
||||
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">
|
||||
|
||||
<div class="flex h-full flex-col bg-white dark:bg-slate-900 shadow-2xl border-l border-slate-100 dark:border-slate-800">
|
||||
<!-- 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="flex items-start justify-between">
|
||||
<div>
|
||||
<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>
|
||||
<p class="text-xs font-bold text-slate-400 uppercase tracking-[0.2em]">{{ __('Transfer Details') }}</p>
|
||||
</div>
|
||||
<button @click="showDetailsPanel = false" class="p-2 rounded-full hover:bg-white dark:hover:bg-slate-800 text-slate-400 transition-all">
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 overflow-y-auto custom-scrollbar p-8">
|
||||
<template x-if="loadingDetails">
|
||||
<div class="flex flex-col items-center justify-center h-64 space-y-4">
|
||||
<div class="w-12 h-12 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin"></div>
|
||||
<p class="text-sm font-bold text-slate-400 animate-pulse">{{ __('Fetching data...') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="!loadingDetails && selectedOrder">
|
||||
<div class="space-y-8 animate-luxury-in">
|
||||
<!-- Info Cards -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<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">{{ __('Status') }}</p>
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black tracking-widest uppercase"
|
||||
: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 class="flex-shrink-0 text-slate-300">
|
||||
<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>
|
||||
</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">
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('To') }}</p>
|
||||
<p class="font-bold text-slate-800 dark:text-white" x-text="selectedOrder.to_warehouse_name || '-'"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<h4 class="text-xs font-black text-slate-400 uppercase tracking-[0.2em] pl-1">{{ __('Order Items') }}</h4>
|
||||
<div class="space-y-2">
|
||||
<template x-for="item in orderItems" :key="item.product_id">
|
||||
<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 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">
|
||||
<img :src="item.image_url || '/images/placeholder-product.png'" class="w-full h-full object-cover rounded-lg" alt="">
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-bold text-slate-800 dark:text-slate-200 truncate" x-text="item.product_name"></p>
|
||||
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest" x-text="'ID: ' + item.product_id"></p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-sm font-black text-slate-800 dark:text-white" x-text="'x' + item.quantity"></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</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">
|
||||
<div class="flex justify-between items-center">
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Created By') }}</p>
|
||||
<p class="text-xs font-bold text-slate-600 dark:text-slate-300" x-text="selectedOrder.creator_name"></p>
|
||||
</div>
|
||||
<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>
|
||||
</template>
|
||||
<template x-if="selectedOrder.note">
|
||||
<div class="pt-2 border-t border-slate-200 dark:border-slate-700">
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Note') }}</p>
|
||||
<p class="text-xs font-bold text-slate-600 dark:text-slate-300 italic" x-text="selectedOrder.note"></p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -13,39 +13,49 @@
|
||||
];
|
||||
|
||||
if ($routeName && $routeName !== 'admin.dashboard') {
|
||||
// 定義大模組映射表 (路由前綴 => 大模組名稱)
|
||||
// 定義大模組映射表 (路由前綴 => [Label, IndexRoute])
|
||||
$moduleMap = [
|
||||
'profile' => __('Profile Settings'),
|
||||
'admin.members' => __('Member Management'),
|
||||
'admin.membership-tiers' => __('Member Management'),
|
||||
'admin.deposit-bonus-rules' => __('Member Management'),
|
||||
'admin.point-rules' => __('Member Management'),
|
||||
'admin.gift-definitions' => __('Member Management'),
|
||||
'admin.machines' => __('Machine Management'),
|
||||
'admin.app' => __('APP Management'),
|
||||
'admin.warehouses' => __('Warehouse Management'),
|
||||
'admin.sales' => __('Sales Management'),
|
||||
'admin.analysis' => __('Analysis Management'),
|
||||
'admin.audit' => __('Audit Management'),
|
||||
'admin.data-config' => __('Data Configuration'),
|
||||
'admin.remote' => __('Remote Management'),
|
||||
'admin.line' => __('Line Management'),
|
||||
'admin.reservation' => __('Reservation System'),
|
||||
'admin.special-permission' => __('Special Permission'),
|
||||
'admin.permission' => __('Permission Settings'),
|
||||
'admin.basic-settings' => __('Basic Settings'),
|
||||
'admin.maintenance' => __('Machine Management'),
|
||||
'profile' => [__('Profile Settings'), 'profile.edit'],
|
||||
'admin.members' => [__('Member Management'), 'admin.members.index'],
|
||||
'admin.membership-tiers' => [__('Member Management'), 'admin.membership-tiers.index'],
|
||||
'admin.deposit-bonus-rules' => [__('Member Management'), 'admin.deposit-bonus-rules.index'],
|
||||
'admin.point-rules' => [__('Member Management'), 'admin.point-rules.index'],
|
||||
'admin.gift-definitions' => [__('Member Management'), 'admin.gift-definitions.index'],
|
||||
'admin.machines' => [__('Machine Management'), 'admin.machines.index'],
|
||||
'admin.app' => [__('APP Management'), 'admin.app-configs.index'],
|
||||
'admin.warehouses' => [__('Warehouse Management'), 'admin.warehouses.index'],
|
||||
'admin.sales' => [__('Sales Management'), 'admin.sales.index'],
|
||||
'admin.analysis' => [__('Analysis Management'), 'admin.analysis.machine-reports'],
|
||||
'admin.audit' => [__('Audit Management'), 'admin.audit.purchases'],
|
||||
'admin.data-config' => [__('Data Configuration'), 'admin.data-config.products.index'],
|
||||
'admin.remote' => [__('Remote Management'), 'admin.remote.index'],
|
||||
'admin.line' => [__('Line Management'), 'admin.line.official-account'],
|
||||
'admin.reservation' => [__('Reservation System'), 'admin.reservation.reservations'],
|
||||
'admin.special-permission' => [__('Special Permission'), 'admin.special-permission.clear-stock'],
|
||||
'admin.permission' => [__('Permission Settings'), 'admin.permission.accounts'],
|
||||
'admin.basic-settings' => [__('Basic Settings'), 'admin.basic-settings.machines.index'],
|
||||
'admin.maintenance' => [__('Machine Management'), 'admin.maintenance.index'],
|
||||
];
|
||||
|
||||
// 1. 找出所屬大模組
|
||||
$foundModule = null;
|
||||
foreach ($moduleMap as $prefix => $label) {
|
||||
foreach ($moduleMap as $prefix => $data) {
|
||||
if (str_starts_with($routeName, $prefix)) {
|
||||
$foundModule = [
|
||||
'label' => $label,
|
||||
'url' => '#',
|
||||
'active' => false
|
||||
];
|
||||
$label = $data[0];
|
||||
$indexRoute = $data[1];
|
||||
|
||||
$url = '#';
|
||||
try {
|
||||
if ($indexRoute && Route::has($indexRoute)) {
|
||||
$url = route($indexRoute);
|
||||
}
|
||||
} catch (\Exception $e) {}
|
||||
|
||||
$foundModule = [
|
||||
'label' => $label,
|
||||
'url' => $url,
|
||||
'active' => false
|
||||
];
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -54,62 +64,15 @@
|
||||
$links[] = $foundModule;
|
||||
}
|
||||
|
||||
// 2. 處理中間層級與具體頁面
|
||||
// 2. 處理具體頁面
|
||||
$segments = explode('.', $routeName);
|
||||
$lastSegment = end($segments);
|
||||
|
||||
// 如果路由有四段以上 (如 admin.permission.companies.index),則處理中間一段
|
||||
if (count($segments) >= 4) {
|
||||
$midSegment = $segments[2];
|
||||
$midLabel = match($midSegment) {
|
||||
'companies' => __('Customer Management'),
|
||||
'members' => __('Member List'),
|
||||
'machines' => __('Machine Settings'),
|
||||
'products' => __('Product Management'),
|
||||
'machine-models' => __('Machine Model Settings'),
|
||||
'accounts' => __('Account Management'),
|
||||
'sub-accounts' => __('Account Management'),
|
||||
'roles' => __('Roles'),
|
||||
'sub-account-roles' => __('Sub Account Roles'),
|
||||
'payment-configs' => __('Customer Payment Config'),
|
||||
'warehouses' => __('Warehouse List'),
|
||||
'sales' => __('Sales Records'),
|
||||
'maintenance' => __('Maintenance Records'),
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($midLabel) {
|
||||
$links[] = [
|
||||
'label' => $midLabel,
|
||||
'url' => match($midSegment) {
|
||||
'maintenance' => route('admin.maintenance.index'),
|
||||
'machines' => str_contains($routeName, 'basic-settings') ? route('admin.basic-settings.machines.index') : '#',
|
||||
'products' => route('admin.data-config.products.index'),
|
||||
'accounts' => route('admin.permission.accounts'),
|
||||
'sub-accounts' => route('admin.data-config.sub-accounts'),
|
||||
'sub-account-roles' => route('admin.data-config.sub-account-roles'),
|
||||
default => '#',
|
||||
},
|
||||
'active' => $lastSegment === 'index'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 特殊處理:當只有三段且第二段是 maintenance 時,增加中間層級
|
||||
if (count($segments) === 3 && $segments[1] === 'maintenance' && $lastSegment !== 'index') {
|
||||
$links[] = [
|
||||
'label' => __('Maintenance Records'),
|
||||
'url' => route('admin.maintenance.index'),
|
||||
'active' => false
|
||||
];
|
||||
}
|
||||
|
||||
// 3. 處理最後一個動作/頁面
|
||||
$pageLabel = match($lastSegment) {
|
||||
'index' => match($segments[1] ?? '') {
|
||||
'members' => __('Member List'),
|
||||
'machines' => __('Machine List'),
|
||||
'warehouses' => __('Warehouse List (All)'),
|
||||
'warehouses' => __('Warehouse Overview'),
|
||||
'sales' => __('Sales Records'),
|
||||
'analysis' => __('Analysis Management'),
|
||||
'audit' => __('Audit Management'),
|
||||
@ -119,8 +82,13 @@
|
||||
'advertisements' => __('Advertisement Management'),
|
||||
default => null,
|
||||
},
|
||||
'remote' => __('Command Center'),
|
||||
'stock' => __('Stock & Expiry'),
|
||||
'remote' => __('Command Center'),
|
||||
'basic-settings' => match($segments[2] ?? '') {
|
||||
'machines' => __('Machine Settings'),
|
||||
'payment-configs' => __('Payment Config'),
|
||||
'machine-models' => __('Machine Models'),
|
||||
default => null,
|
||||
},
|
||||
default => null,
|
||||
},
|
||||
'edit' => str_starts_with($routeName, 'profile') ? __('Profile') : __('Edit'),
|
||||
@ -136,15 +104,10 @@
|
||||
'questionnaire' => __('Questionnaire'),
|
||||
'games' => __('Games'),
|
||||
'timer' => __('Timer'),
|
||||
'personal' => __('Warehouse List (Individual)'),
|
||||
'stock-management' => __('Stock Management'),
|
||||
'transfers' => __('Transfers'),
|
||||
'purchases' => __('Purchases'),
|
||||
'replenishments' => __('Replenishments'),
|
||||
'replenishment-records' => __('Replenishment Records'),
|
||||
'machine-stock' => __('Stock & Expiry'),
|
||||
'staff-stock' => __('Staff Stock'),
|
||||
'returns' => __('Returns'),
|
||||
'inventory' => __('Inventory Management'),
|
||||
'machine-inventory' => __('Machine Inventory'),
|
||||
'transfers' => __('Transfer Orders'),
|
||||
'replenishments' => __('Machine Replenishment'),
|
||||
'pickup-codes' => __('Pickup Codes'),
|
||||
'orders' => __('Orders'),
|
||||
'promotions' => __('Promotions'),
|
||||
@ -161,12 +124,6 @@
|
||||
'sub-account-roles' => __('Sub Account Roles'),
|
||||
'points' => __('Point Settings'),
|
||||
'badges' => __('Badge Settings'),
|
||||
'restart' => __('Machine Restart'),
|
||||
'restart-card-reader' => __('Card Reader Restart'),
|
||||
'checkout' => __('Remote Checkout'),
|
||||
'lock' => __('Remote Lock'),
|
||||
'change' => __('Remote Change'),
|
||||
'dispense' => __('Remote Dispense'),
|
||||
'official-account' => __('Line Official Account'),
|
||||
'coupons' => __('Line Coupons'),
|
||||
'stores' => __('Store Management'),
|
||||
@ -176,18 +133,7 @@
|
||||
'clear-stock' => __('Clear Stock'),
|
||||
'apk-versions' => __('APK Versions'),
|
||||
'discord-notifications' => __('Discord Notifications'),
|
||||
'app-features' => __('APP Features'),
|
||||
'roles' => __('Roles'),
|
||||
'others' => __('Others'),
|
||||
'ai-prediction' => __('AI Prediction'),
|
||||
'data-config' => __('Data Configuration Permissions'),
|
||||
'sales' => __('Sales Permissions'),
|
||||
'machines' => __('Machine Management Permissions'),
|
||||
'warehouses' => __('Warehouse Permissions'),
|
||||
'analysis' => __('Analysis Permissions'),
|
||||
'audit' => __('Audit Permissions'),
|
||||
'remote' => __('Command Center'),
|
||||
'line' => __('Line Permissions'),
|
||||
'stock' => __('Stock & Expiry'),
|
||||
default => null,
|
||||
};
|
||||
@ -199,17 +145,24 @@
|
||||
];
|
||||
}
|
||||
|
||||
// 確保最後一個 link 是 active 的
|
||||
// 確保最後一個 link 是 active 的,並過濾重複標籤
|
||||
if (!empty($links)) {
|
||||
$links[count($links) - 1]['active'] = true;
|
||||
// 檢查是否有相鄰重複的 Label (例如 Dashboard > Dashboard)
|
||||
if (count($links) > 1 && $links[count($links)-1]['label'] === $links[count($links)-2]['label']) {
|
||||
array_pop($links);
|
||||
$links[count($links)-1]['active'] = true;
|
||||
// 檢查是否有相鄰重複的 Label (例如 Dashboard > Dashboard 或 Warehouse Management > Warehouse Overview)
|
||||
// 如果後面的標籤與前面相同,則移除前面的,保留具體的
|
||||
$newLinks = [];
|
||||
foreach ($links as $link) {
|
||||
if (!empty($newLinks) && end($newLinks)['label'] === $link['label']) {
|
||||
array_pop($newLinks);
|
||||
}
|
||||
$newLinks[] = $link;
|
||||
}
|
||||
$links = $newLinks;
|
||||
|
||||
// 重置所有 active 為 false,最後一個為 true
|
||||
foreach ($links as &$l) $l['active'] = false;
|
||||
$links[count($links) - 1]['active'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@endphp
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
@props([
|
||||
'message' => __('Are you sure you want to delete this item? This action cannot be undone.'),
|
||||
'title' => __('Confirm Deletion')
|
||||
'message' => __('Are you sure you want to delete this item? This action cannot be undone.'),
|
||||
'title' => __('Confirm Deletion')
|
||||
])
|
||||
|
||||
<template x-teleport="body">
|
||||
@ -9,14 +9,16 @@
|
||||
<div x-show="isDeleteConfirmOpen" 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"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 transition-opacity bg-slate-900/60 backdrop-blur-sm"
|
||||
@click="isDeleteConfirmOpen = false"></div>
|
||||
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
||||
|
||||
<div x-show="isDeleteConfirmOpen" 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: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">
|
||||
@ -30,7 +32,8 @@
|
||||
</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">
|
||||
<h3
|
||||
class="text-xl font-black text-slate-800 dark:text-white leading-6 tracking-tight font-display uppercase">
|
||||
{{ $title }}
|
||||
</h3>
|
||||
<div class="mt-4">
|
||||
@ -57,4 +60,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
@ -23,7 +23,7 @@
|
||||
"searchWrapperClasses" => "sticky top-0 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md p-2 z-10",
|
||||
"toggleClasses" => "hs-select-toggle luxury-select-toggle",
|
||||
"toggleTemplate" => '<button type="button" aria-expanded="false"><span class="me-2" data-icon></span><span class="text-slate-800 dark:text-slate-200" data-title></span><div class="ms-auto"><svg class="size-4 text-slate-400 transition-transform duration-300" 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-[100] animate-luxury-in",
|
||||
"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] animate-luxury-in",
|
||||
"optionClasses" => "hs-select-option py-2.5 px-3 mb-0.5 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 flex items-center justify-between transition-all duration-300",
|
||||
"optionTemplate" => '<div class="flex items-center justify-between w-full"><span data-title></span><span class="hs-select-active-indicator hidden text-cyan-500"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg></span></div>'
|
||||
];
|
||||
|
||||
@ -21,6 +21,9 @@ Route::get('/', function () {
|
||||
return redirect()->route('login');
|
||||
});
|
||||
|
||||
// 公開機台分布地圖 (無需登入)
|
||||
Route::get('/machines/distribution', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'distribution'])->name('machines.distribution');
|
||||
|
||||
Route::get('/dashboard', function () {
|
||||
return redirect()->route('admin.dashboard');
|
||||
})->middleware(['auth', 'verified'])->name('dashboard');
|
||||
@ -81,11 +84,13 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
|
||||
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::patch('/inventory/stock-in/{stockInOrder}/confirm', [App\Http\Controllers\Admin\WarehouseController::class, 'confirmStockIn'])->name('inventory.stock-in.confirm');
|
||||
Route::get('/inventory/stock-in/{order}/details', [App\Http\Controllers\Admin\WarehouseController::class, 'stockInOrderDetails'])->name('inventory.stock-in.details');
|
||||
|
||||
// 模組 3:調撥單
|
||||
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::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');
|
||||
|
||||
// 模組 4:機台庫存總覽
|
||||
Route::get('/machine-inventory', [App\Http\Controllers\Admin\WarehouseController::class, 'machineInventory'])->name('machine-inventory');
|
||||
@ -95,6 +100,11 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
|
||||
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::patch('/replenishments/{replenishmentOrder}/confirm', [App\Http\Controllers\Admin\WarehouseController::class, 'confirmReplenishment'])->name('replenishments.confirm');
|
||||
Route::get('/replenishments/{order}/details', [App\Http\Controllers\Admin\WarehouseController::class, 'replenishmentOrderDetails'])->name('replenishments.details');
|
||||
|
||||
// AJAX 庫存查詢
|
||||
Route::get('/ajax/stock', [App\Http\Controllers\Admin\WarehouseController::class, 'getStockAjax'])->name('ajax.stock');
|
||||
Route::get('/{warehouse}/inventory-ajax', [App\Http\Controllers\Admin\WarehouseController::class, 'warehouseStocks'])->name('inventory-ajax');
|
||||
});
|
||||
|
||||
// 6. 銷售管理
|
||||
@ -203,6 +213,9 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
|
||||
Route::put('/{machine}', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'update'])->name('update');
|
||||
Route::post('/', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'store'])->name('store');
|
||||
Route::post('/{machine}/regenerate-token', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'regenerateToken'])->name('regenerate-token');
|
||||
|
||||
// 地址轉座標 (Geocoding Proxy)
|
||||
Route::post('/geocode', [App\Http\Controllers\Admin\GeocodingController::class, 'resolve'])->name('geocode');
|
||||
});
|
||||
|
||||
// 客戶金流設定
|
||||
|
||||
253
scratch/dedupe_lang.py
Normal file
253
scratch/dedupe_lang.py
Normal file
@ -0,0 +1,253 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
files = {
|
||||
'zh_TW': '/home/mama/projects/star-cloud/lang/zh_TW.json',
|
||||
'en': '/home/mama/projects/star-cloud/lang/en.json',
|
||||
'ja': '/home/mama/projects/star-cloud/lang/ja.json'
|
||||
}
|
||||
|
||||
new_keys = {
|
||||
"CREATE STOCK TRANSFERS BETWEEN WAREHOUSES AND MACHINE RETURNS": {
|
||||
"zh_TW": "建立倉庫間調撥或機台退庫單",
|
||||
"en": "Create stock transfers between warehouses and machine returns",
|
||||
"ja": "倉庫間の転送および機台からの返品伝票を作成"
|
||||
},
|
||||
"Create and manage replenishment orders from warehouse to machines": {
|
||||
"zh_TW": "建立並管理從倉庫到機台的補貨單",
|
||||
"en": "Create and manage replenishment orders from warehouse to machines",
|
||||
"ja": "倉庫から機台への補充伝票の作成と管理"
|
||||
},
|
||||
"Create stock transfers between warehouses and machine returns": {
|
||||
"zh_TW": "建立倉庫間調撥或機台退庫單",
|
||||
"en": "Create stock transfers between warehouses and machine returns",
|
||||
"ja": "倉庫間の転送および機台からの返品伝票を作成"
|
||||
},
|
||||
"Create Transfer Order": {
|
||||
"zh_TW": "建立調撥單",
|
||||
"en": "Create Transfer Order",
|
||||
"ja": "転送伝票を作成"
|
||||
},
|
||||
"Create Replenishment Order": {
|
||||
"zh_TW": "建立補貨單",
|
||||
"en": "Create Replenishment Order",
|
||||
"ja": "補充伝票を作成"
|
||||
},
|
||||
"New Transfer": {
|
||||
"zh_TW": "新增調撥單",
|
||||
"en": "New Transfer",
|
||||
"ja": "新規転送"
|
||||
},
|
||||
"New Replenishment": {
|
||||
"zh_TW": "新增補貨單",
|
||||
"en": "New Replenishment",
|
||||
"ja": "新規補充"
|
||||
},
|
||||
"New Stock-In Order": {
|
||||
"zh_TW": "新增進貨單",
|
||||
"en": "New Stock-In Order",
|
||||
"ja": "新規入庫伝票"
|
||||
},
|
||||
"No transfer orders found": {
|
||||
"zh_TW": "查無調撥單紀錄",
|
||||
"en": "No transfer orders found",
|
||||
"ja": "転送伝票が見つかりません"
|
||||
},
|
||||
"No replenishment orders found": {
|
||||
"zh_TW": "查無補貨單紀錄",
|
||||
"en": "No replenishment orders found",
|
||||
"ja": "補充伝票が見つかりません"
|
||||
},
|
||||
"No stock data found": {
|
||||
"zh_TW": "查無庫存資料",
|
||||
"en": "No stock data found",
|
||||
"ja": "在庫データが見つかりません"
|
||||
},
|
||||
"No stock-in orders found": {
|
||||
"zh_TW": "查無進貨單紀錄",
|
||||
"en": "No stock-in orders found",
|
||||
"ja": "入庫伝票が見つかりません"
|
||||
},
|
||||
"No movement records found": {
|
||||
"zh_TW": "查無異動紀錄",
|
||||
"en": "No movement records found",
|
||||
"ja": "移動履歴が見つかりません"
|
||||
},
|
||||
"Replenishment Orders": {
|
||||
"zh_TW": "機台補貨單",
|
||||
"en": "Replenishment Orders",
|
||||
"ja": "機台補充伝票"
|
||||
},
|
||||
"Search products...": {
|
||||
"zh_TW": "搜尋商品...",
|
||||
"en": "Search products...",
|
||||
"ja": "商品を検索..."
|
||||
},
|
||||
"Search Product": {
|
||||
"zh_TW": "搜尋商品",
|
||||
"en": "Search Product",
|
||||
"ja": "商品を検索"
|
||||
},
|
||||
"Select Warehouse": {
|
||||
"zh_TW": "選擇倉庫",
|
||||
"en": "Select Warehouse",
|
||||
"ja": "倉庫を選択"
|
||||
},
|
||||
"Select Machine": {
|
||||
"zh_TW": "選擇機台",
|
||||
"en": "Select Machine",
|
||||
"ja": "機台を選択"
|
||||
},
|
||||
"Select Product": {
|
||||
"zh_TW": "選擇商品",
|
||||
"en": "Select Product",
|
||||
"ja": "商品を選択"
|
||||
},
|
||||
"Source Warehouse": {
|
||||
"zh_TW": "來源倉庫",
|
||||
"en": "Source Warehouse",
|
||||
"ja": "発送元倉庫"
|
||||
},
|
||||
"Target Warehouse": {
|
||||
"zh_TW": "目標倉庫",
|
||||
"en": "Target Warehouse",
|
||||
"ja": "対象倉庫"
|
||||
},
|
||||
"Stock-In Orders": {
|
||||
"zh_TW": "進貨單管理",
|
||||
"en": "Stock-In Orders",
|
||||
"ja": "入庫伝票"
|
||||
},
|
||||
"Stock-In Management": {
|
||||
"zh_TW": "進貨管理",
|
||||
"en": "Stock-In Management",
|
||||
"ja": "入庫管理"
|
||||
},
|
||||
"Stock flow history": {
|
||||
"zh_TW": "庫存流向歷史",
|
||||
"en": "Stock flow history",
|
||||
"ja": "在庫移動履歴"
|
||||
},
|
||||
"Stock-In Orders Tab": {
|
||||
"zh_TW": "進貨單管理",
|
||||
"en": "Stock-In Orders",
|
||||
"ja": "入庫伝票"
|
||||
},
|
||||
"Transfer Orders": {
|
||||
"zh_TW": "調撥單管理",
|
||||
"en": "Transfer Orders",
|
||||
"ja": "転送伝票"
|
||||
},
|
||||
"Track stock-in orders and movement history": {
|
||||
"zh_TW": "追蹤進貨單據與庫存異動紀錄",
|
||||
"en": "Track stock-in orders and movement history",
|
||||
"ja": "入庫伝票および移動履歴の追跡"
|
||||
},
|
||||
"Warehouse to Warehouse": {
|
||||
"zh_TW": "倉庫對倉庫",
|
||||
"en": "Warehouse to Warehouse",
|
||||
"ja": "倉庫間転送"
|
||||
},
|
||||
"Warehouse Transfer": {
|
||||
"zh_TW": "倉庫調撥",
|
||||
"en": "Warehouse Transfer",
|
||||
"ja": "倉庫転送"
|
||||
},
|
||||
"Warehouse Management": {
|
||||
"zh_TW": "倉儲管理",
|
||||
"en": "Warehouse Management",
|
||||
"ja": "倉庫管理"
|
||||
},
|
||||
"Warehouse purchase records": {
|
||||
"zh_TW": "倉庫採購進貨紀錄",
|
||||
"en": "Warehouse purchase records",
|
||||
"ja": "倉庫購入記録"
|
||||
},
|
||||
"Machine to Warehouse": {
|
||||
"zh_TW": "機台對倉庫",
|
||||
"en": "Machine to Warehouse",
|
||||
"ja": "機台から倉庫"
|
||||
},
|
||||
"Machine Return": {
|
||||
"zh_TW": "機台退庫",
|
||||
"en": "Machine Return",
|
||||
"ja": "機台返品"
|
||||
},
|
||||
"Machine Replenishment": {
|
||||
"zh_TW": "機台補貨單",
|
||||
"en": "Machine Replenishment",
|
||||
"ja": "機台補充"
|
||||
},
|
||||
"Movement History": {
|
||||
"zh_TW": "異動紀錄",
|
||||
"en": "Movement History",
|
||||
"ja": "移動履歴"
|
||||
},
|
||||
"Movement Logs": {
|
||||
"zh_TW": "異動紀錄",
|
||||
"en": "Movement Logs",
|
||||
"ja": "移動ログ"
|
||||
},
|
||||
"Manage stock transfers between warehouses and machine returns": {
|
||||
"zh_TW": "建立倉庫間調撥或機台退庫單",
|
||||
"en": "Manage stock transfers between warehouses and machine returns",
|
||||
"ja": "倉庫間の転送および機台からの返品伝票を作成"
|
||||
},
|
||||
"Main": {
|
||||
"zh_TW": "總倉",
|
||||
"en": "Main",
|
||||
"ja": "総倉庫"
|
||||
},
|
||||
"Note": {
|
||||
"zh_TW": "備註",
|
||||
"en": "Note",
|
||||
"ja": "備考"
|
||||
},
|
||||
"Transfer Audit": {
|
||||
"zh_TW": "調撥單",
|
||||
"en": "Transfer Audit",
|
||||
"ja": "転送監査"
|
||||
},
|
||||
"Transfers": {
|
||||
"zh_TW": "調撥單",
|
||||
"en": "Transfers",
|
||||
"ja": "転送"
|
||||
},
|
||||
"Inventory Management": {
|
||||
"zh_TW": "庫存管理",
|
||||
"en": "Inventory Management",
|
||||
"ja": "在庫管理"
|
||||
},
|
||||
"Manage stock levels, stock-in orders, and movement history": {
|
||||
"zh_TW": "追蹤庫存水位、進貨單與異動紀錄",
|
||||
"en": "Manage stock levels, stock-in orders, and movement history",
|
||||
"ja": "在庫レベル、入庫伝票、および移動履歴の管理"
|
||||
},
|
||||
"Track stock levels, stock-in orders, and movement history": {
|
||||
"zh_TW": "追蹤庫存水位、進貨單與異動紀錄",
|
||||
"en": "Track stock levels, stock-in orders, and movement history",
|
||||
"ja": "在庫レベル、入庫伝票、および移動履歴の追跡"
|
||||
}
|
||||
}
|
||||
|
||||
for lang, path in files.items():
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Update menu.warehouses
|
||||
if lang == 'zh_TW':
|
||||
data['menu.warehouses'] = "倉儲管理"
|
||||
|
||||
# Add/Update keys
|
||||
for k, v in new_keys.items():
|
||||
if lang in v:
|
||||
data[k] = v[lang]
|
||||
elif lang == 'en':
|
||||
data[k] = k
|
||||
|
||||
# Sort keys and write back
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=4, sort_keys=True)
|
||||
66
scratch/test_breadcrumbs.php
Normal file
66
scratch/test_breadcrumbs.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
// Mocking Route and Laravel environment for breadcrumb logic test
|
||||
|
||||
function __($str) { return $str; }
|
||||
|
||||
class Route {
|
||||
public static $currentName = '';
|
||||
public static function currentRouteName() { return self::$currentName; }
|
||||
}
|
||||
|
||||
function testBreadcrumbs($routeName) {
|
||||
Route::$currentName = $routeName;
|
||||
$links = [];
|
||||
$links[] = [
|
||||
'label' => __('Dashboard'),
|
||||
'url' => '/admin/dashboard',
|
||||
'active' => $routeName === 'admin.dashboard'
|
||||
];
|
||||
|
||||
$moduleMap = [
|
||||
'admin.warehouses' => __('Warehouse Management'),
|
||||
// ... simplified
|
||||
];
|
||||
|
||||
$foundModule = null;
|
||||
foreach ($moduleMap as $prefix => $label) {
|
||||
if (str_starts_with($routeName, $prefix)) {
|
||||
$foundModule = [
|
||||
'label' => $label,
|
||||
'url' => '#',
|
||||
'active' => false
|
||||
];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($foundModule) {
|
||||
$links[] = $foundModule;
|
||||
}
|
||||
|
||||
$segments = explode('.', $routeName);
|
||||
$lastSegment = end($segments);
|
||||
|
||||
// Simplifed page labels
|
||||
$pageLabel = match($lastSegment) {
|
||||
'inventory' => __('Inventory Management'),
|
||||
'index' => 'Warehouse Overview',
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($pageLabel) {
|
||||
$links[] = [
|
||||
'label' => $pageLabel,
|
||||
'active' => true
|
||||
];
|
||||
}
|
||||
|
||||
echo "Route: $routeName\n";
|
||||
foreach ($links as $link) {
|
||||
echo " - " . $link['label'] . ($link['active'] ? " (active)" : "") . "\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
testBreadcrumbs('admin.warehouses.inventory');
|
||||
testBreadcrumbs('admin.warehouses.index');
|
||||
Loading…
Reference in New Issue
Block a user