[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:
sky121113 2026-04-23 17:34:22 +08:00
parent 2f86bb90a2
commit 39a246d3c5
31 changed files with 5655 additions and 2557 deletions

View File

@ -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();

View File

@ -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'));
}
}

View 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,
];
}
}

View File

@ -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
]);
}
}

View File

@ -53,6 +53,9 @@ class Machine extends Model
'serial_no',
'model',
'location',
'address',
'latitude',
'longitude',
'status',
'current_page',
'door_status',

View File

@ -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);
}
}

View File

@ -67,4 +67,12 @@ class Company extends Model
{
return $this->hasMany(Machine::class);
}
/**
* Scope僅篩選啟用的公司
*/
public function scopeActive($query)
{
return $query->where('status', 1);
}
}

View File

@ -79,4 +79,12 @@ class Warehouse extends Model
{
return $this->hasMany(StockInOrder::class);
}
/**
* 所屬公司
*/
public function company()
{
return $this->belongsTo(\App\Models\System\Company::class);
}
}

View File

@ -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']);
});
}
};

File diff suppressed because it is too large Load Diff

View File

@ -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": "来店特典機能"
}

File diff suppressed because it is too large Load Diff

View File

@ -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);

View File

@ -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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <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>

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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">&#8203;</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

View File

@ -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">&#8203;</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

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -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">&#8203;</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>

View File

@ -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">&#8203;</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>

View File

@ -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

View File

@ -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">&#8203;</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>

View File

@ -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>'
];

View File

@ -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
View 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)

View 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');