[FEAT] 倉儲管理系統模組實作與全站 UI/UX 標準化優化
1. 實作倉儲管理系統 (Warehouse Management):包含總覽、庫存、調撥、機台庫存與補貨模組。 2. 建立倉儲相關資料結構:新增 warehouses, warehouse_stocks, stock_in_orders, stock_movements, transfer_orders, replenishment_orders 等資料表。 3. 優化商品管理 (Product Management) 介面:調整商品與類別管理的分頁、搜尋與編輯邏輯,並導入極簡奢華風 UI。 4. 增強遠端機台管理 (Remote Machine Management):優化機台清單、庫存監控與遠端出貨介面,提升載入效能。 5. 更新全站導覽選單 (Sidebar):整合倉儲管理入口,並修復側邊欄語法錯誤。 6. 標準化分頁組件 (Pagination):套用 luxury 奢華風分頁樣式。 7. 補齊多語系語系檔 (i18n):更新 zh_TW.json,包含倉儲與商品模組相關詞彙。
This commit is contained in:
parent
40aec884b0
commit
315c226560
@ -329,3 +329,148 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範
|
||||
---
|
||||
> [!TIP]
|
||||
> 當遇到未定義的 UI 區塊時,優先參考 `admin.dashboard.blade.php` 的卡片與即時動態實作方式進行衍生。
|
||||
|
||||
## 13. 加載指示器規範 (Loading Indicator Pattern)
|
||||
|
||||
當使用 AJAX 進行局部頁面刷新或異步操作時,必須使用統一的「極奢華加載遮罩」。
|
||||
|
||||
### 13.1 標準 HTML 結構
|
||||
```html
|
||||
<!-- Loading Spinner Overlay -->
|
||||
<div x-show="isLoadingTable"
|
||||
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-300"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="absolute inset-0 z-50 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-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 13.2 設計要點
|
||||
1. **多重動畫**:結合 `animate-spin` (不同速度/方向) 與 `animate-pulse`,營造出高科技且細膩的層次感。
|
||||
2. **顏色規範**:主要使用 `cyan-500` 作為亮點,搭配 `slate` 系列的半透明背景。
|
||||
3. **字體細節**:文字使用極細小尺寸 (`text-[10px]`)、極粗字重 (`font-black`) 並大幅度拉開字距 (`tracking-[0.4em]`)。
|
||||
4. **背景質感**:使用 `backdrop-blur-[1px]` 與 `bg-white/40` (或 `dark:bg-slate-900/40`),確保遮罩具有玻璃質感但不完全阻斷視線。
|
||||
5. **防止閃爍**: 必須加上 `x-cloak` 配合對應的 CSS 樣式。
|
||||
|
||||
## 14. 頁面佈局與頁籤間距規範 (Page Layout & Tab Spacing)
|
||||
|
||||
為了確保管理後台全站視覺的「高度整合感」與「緊湊性」,頁面頂部結構與頁籤導覽必須遵循以下間距規範。
|
||||
|
||||
### 14.1 核心佈局間距 (Core Spacing Scale)
|
||||
- **根容器間距 (Root Space)**: 頂部 Header、Tabs 與內容區域之間的垂直間距統一使用 **`space-y-2`**。
|
||||
- **標題與描述 (Title & Subtitle)**: 描述文字應緊貼標題,使用 `mt-1`。
|
||||
- **頁籤外層間距 (Tabs Outer)**: 頁籤容器與 Header 之間應由根容器的 `space-y-2` 自然推開,不額外加 `mt`。
|
||||
|
||||
### 14.2 頁籤導覽樣式 (Tab Navigation Styles)
|
||||
- **容器內距**: 使用 `p-1.5` 的圓角膠囊背景。
|
||||
- **按鈕內距**: 頁籤按鈕統一使用 **`px-8 py-3`**。
|
||||
- **內容間隙**: 頁籤下方內容區與頁籤列之間,建議不額外添加 `mt-6` 等大間距。若內容區域內部需要層次感,使用 `space-y-3` 即可。
|
||||
|
||||
### 14.3 實作範例結構 (Reference Structure)
|
||||
```html
|
||||
<div class="space-y-2">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-black">標題</h1>
|
||||
<p class="text-sm mt-1">描述文字</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs Container -->
|
||||
<div class="flex items-center p-1.5 bg-slate-100 rounded-2xl w-fit border border-slate-200/50">
|
||||
<button class="px-8 py-3 rounded-xl text-sm font-black active">Tab A</button>
|
||||
<button class="px-8 py-3 rounded-xl text-sm font-black">Tab B</button>
|
||||
</div>
|
||||
|
||||
<!-- Content Area (AJAX Container) -->
|
||||
<div class="space-y-3">
|
||||
<!-- 內容卡片 -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 14.4 統計卡片間距 (Stats Grid Spacing)
|
||||
- **網格間距**: 頂部統計卡片網格 (Grid) 建議使用 **`gap-4`** (16px),避免使用過大的 `gap-6` 或 `gap-8` 導致版面鬆散。
|
||||
- **卡片內距**: 統計卡片內部統一使用 `p-6`。
|
||||
|
||||
### 15. Premium AJAX Interactions & Loaders
|
||||
|
||||
To maintain a seamless "SPA-like" experience without full-page reloads, all administrative modules must follow these AJAX patterns.
|
||||
|
||||
#### 15.1 The Premium Multi-Ring Loader
|
||||
Used for table overlays and state transitions. It features a dual-spin ring with a pulsing core icon.
|
||||
|
||||
```html
|
||||
<!-- Loading Overlay Pattern -->
|
||||
<div x-show="isLoading"
|
||||
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-300"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="absolute inset-0 z-50 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">
|
||||
<!-- Outer: Fast spin -->
|
||||
<div class="absolute inset-0 rounded-full border-2 border-transparent border-t-cyan-500 border-r-cyan-500/30 animate-spin"></div>
|
||||
<!-- Inner: Slow reverse spin -->
|
||||
<div class="absolute inset-2 rounded-full border border-cyan-500/10 animate-spin" style="animation-duration: 3s; direction: reverse;"></div>
|
||||
<!-- Core: Pulsing Icon -->
|
||||
<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">Syncing Data...</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### 15.2 Standard fetchPage Pattern
|
||||
All lists should implement the following `fetchPage` method in their `x-data` and wrap the dynamic content in `#ajax-content-container`.
|
||||
|
||||
```javascript
|
||||
async fetchPage(url) {
|
||||
if (!url || this.isLoadingTable) return;
|
||||
this.isLoadingTable = true;
|
||||
try {
|
||||
const res = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||
const html = await res.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const newContent = doc.querySelector('#ajax-content-container');
|
||||
if (newContent) {
|
||||
document.querySelector('#ajax-content-container').innerHTML = newContent.innerHTML;
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('AJAX Load Failed:', e);
|
||||
} finally {
|
||||
this.isLoadingTable = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 15.3 Triggering AJAX Navigation
|
||||
- **Forms**: Use `@submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())"`.
|
||||
- **Selects**: Use `onchange="this.dispatchEvent(new CustomEvent('ajax:navigate', { detail: { url: ... }, bubbles: true }))"`.
|
||||
- **Links/Pagination**: Use a delegated `@click` listener on the container or `@ajax:navigate.prevent.stop`.
|
||||
@ -34,8 +34,8 @@ class MqttSyncAuth extends Command
|
||||
\Illuminate\Support\Facades\Redis::hSet($gatewayKey, 'password', $gatewayPass);
|
||||
$this->info("Gateway auth synced.");
|
||||
|
||||
// 2. 同步所有機台的認證資料
|
||||
$machines = Machine::withoutGlobalScopes()->get();
|
||||
// 2. 同步所有機台的認證資料 (僅限未刪除的)
|
||||
$machines = Machine::get();
|
||||
$this->info("Syncing " . $machines->count() . " machines to Redis...");
|
||||
|
||||
$bar = $this->output->createProgressBar($machines->count());
|
||||
|
||||
@ -81,11 +81,13 @@ class PaymentConfigController extends AdminController
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'company_id' => 'required|exists:companies,id',
|
||||
'settings' => 'required|array',
|
||||
]);
|
||||
|
||||
$paymentConfig->update([
|
||||
'name' => $request->name,
|
||||
'company_id' => $request->company_id,
|
||||
'settings' => $request->settings,
|
||||
'updater_id' => auth()->id(),
|
||||
]);
|
||||
|
||||
@ -178,6 +178,13 @@ class CompanyController extends Controller
|
||||
if (isset($validated['settings'])) {
|
||||
$validated['settings']['enable_material_code'] = filter_var($validated['settings']['enable_material_code'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||
$validated['settings']['enable_points'] = filter_var($validated['settings']['enable_points'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||
$validated['settings']['tax_invoice'] = filter_var($validated['settings']['tax_invoice'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||
$validated['settings']['card_terminal'] = filter_var($validated['settings']['card_terminal'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||
$validated['settings']['scan_pay_esun'] = filter_var($validated['settings']['scan_pay_esun'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||
$validated['settings']['scan_pay_linepay'] = filter_var($validated['settings']['scan_pay_linepay'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||
$validated['settings']['shopping_cart'] = filter_var($validated['settings']['shopping_cart'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||
$validated['settings']['welcome_gift'] = filter_var($validated['settings']['welcome_gift'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||
$validated['settings']['cash_module'] = filter_var($validated['settings']['cash_module'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($validated, $company) {
|
||||
@ -205,6 +212,33 @@ class CompanyController extends Controller
|
||||
return redirect()->back()->with('success', __('Customer updated successfully.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新客戶的系統設定
|
||||
*/
|
||||
public function updateSettings(Request $request, Company $company)
|
||||
{
|
||||
$settings = $request->input('settings', []);
|
||||
|
||||
$formattedSettings = [
|
||||
'enable_material_code' => filter_var($settings['enable_material_code'] ?? false, FILTER_VALIDATE_BOOLEAN),
|
||||
'enable_points' => filter_var($settings['enable_points'] ?? false, FILTER_VALIDATE_BOOLEAN),
|
||||
'tax_invoice' => filter_var($settings['tax_invoice'] ?? false, FILTER_VALIDATE_BOOLEAN),
|
||||
'card_terminal' => filter_var($settings['card_terminal'] ?? false, FILTER_VALIDATE_BOOLEAN),
|
||||
'scan_pay_esun' => filter_var($settings['scan_pay_esun'] ?? false, FILTER_VALIDATE_BOOLEAN),
|
||||
'scan_pay_linepay' => filter_var($settings['scan_pay_linepay'] ?? false, FILTER_VALIDATE_BOOLEAN),
|
||||
'shopping_cart' => filter_var($settings['shopping_cart'] ?? false, FILTER_VALIDATE_BOOLEAN),
|
||||
'welcome_gift' => filter_var($settings['welcome_gift'] ?? false, FILTER_VALIDATE_BOOLEAN),
|
||||
'cash_module' => filter_var($settings['cash_module'] ?? false, FILTER_VALIDATE_BOOLEAN),
|
||||
];
|
||||
|
||||
// Ensure we force save the JSON cast properly
|
||||
$company->settings = $formattedSettings;
|
||||
$company->save();
|
||||
|
||||
return redirect()->to(route('admin.permission.companies.index') . '?tab=settings')
|
||||
->with('success', __('System settings updated successfully.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 切換客戶狀態
|
||||
*/
|
||||
|
||||
@ -112,9 +112,12 @@ class MachinePermissionController extends AdminController
|
||||
|
||||
$user->machines()->sync($request->machine_ids ?? []);
|
||||
|
||||
$message = __('Machine permissions updated successfully.');
|
||||
session()->flash('success', $message);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => __('Permissions updated successfully'),
|
||||
'message' => $message,
|
||||
'assigned_machines' => $user->machines()->select('machines.id', 'machines.name', 'machines.serial_no')->get()
|
||||
]);
|
||||
}
|
||||
|
||||
@ -265,7 +265,10 @@ class ProductController extends Controller
|
||||
DB::beginTransaction();
|
||||
|
||||
$dictKey = $product->name_dictionary_key ?: \Illuminate\Support\Str::uuid()->toString();
|
||||
$company_id = $product->company_id;
|
||||
// Determine company_id: prioritized from request (for sys admin) then from product's current company
|
||||
$company_id = (auth()->user()->isSystemAdmin() && $request->filled('company_id'))
|
||||
? $request->company_id
|
||||
: $product->company_id;
|
||||
|
||||
// 更新或建立多語系翻譯(繞過 TenantScoped,避免系統管理員操作租戶資料時被過濾)
|
||||
foreach ($request->names as $locale => $name) {
|
||||
@ -292,6 +295,7 @@ class ProductController extends Controller
|
||||
}
|
||||
|
||||
$data = [
|
||||
'company_id' => $company_id,
|
||||
'category_id' => $request->category_id,
|
||||
'name' => $request->names['zh_TW'] ?? ($product->name ?? 'Untitled'),
|
||||
'name_dictionary_key' => $dictKey,
|
||||
|
||||
@ -3,137 +3,507 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\Machine\MachineSlot;
|
||||
use App\Models\Product\Product;
|
||||
use App\Models\Warehouse\ReplenishmentOrder;
|
||||
use App\Models\Warehouse\ReplenishmentOrderItem;
|
||||
use App\Models\Warehouse\StockInOrder;
|
||||
use App\Models\Warehouse\StockInOrderItem;
|
||||
use App\Models\Warehouse\StockMovement;
|
||||
use App\Models\Warehouse\TransferOrder;
|
||||
use App\Models\Warehouse\TransferOrderItem;
|
||||
use App\Models\Warehouse\Warehouse;
|
||||
use App\Models\Warehouse\WarehouseStock;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class WarehouseController extends Controller
|
||||
{
|
||||
// 倉庫列表(全部)
|
||||
public function index()
|
||||
// ─── 模組 1:倉庫總覽 ───
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '倉庫列表(全部)',
|
||||
'description' => '顯示所有倉庫的資訊與庫存狀態',
|
||||
'features' => [
|
||||
'查看所有倉庫列表',
|
||||
'即時庫存數量顯示',
|
||||
'倉庫狀態監控',
|
||||
'快速搜尋與篩選',
|
||||
]
|
||||
]);
|
||||
$query = Warehouse::query()
|
||||
->withCount('stocks as products_count')
|
||||
->withSum('stocks as total_stock', 'quantity');
|
||||
|
||||
if ($search = $request->input('search')) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('address', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('type')) {
|
||||
$query->where('type', $request->input('type'));
|
||||
}
|
||||
|
||||
$warehouses = $query->orderBy('type')->orderBy('name')
|
||||
->paginate($request->input('per_page', 10))
|
||||
->withQueryString();
|
||||
|
||||
$stats = [
|
||||
'total' => Warehouse::count(),
|
||||
'main' => Warehouse::main()->count(),
|
||||
'branch' => Warehouse::branch()->count(),
|
||||
'low_stock' => WarehouseStock::lowStock()
|
||||
->whereHas('warehouse', fn($q) => $q)->count(),
|
||||
];
|
||||
|
||||
return view('admin.warehouses.index', compact('warehouses', 'stats'));
|
||||
}
|
||||
|
||||
// 倉庫列表(個人)
|
||||
public function personal()
|
||||
public function store(Request $request)
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '倉庫列表(個人)',
|
||||
'description' => '顯示個人負責的倉庫資訊',
|
||||
'features' => [
|
||||
'查看個人負責倉庫',
|
||||
'個人庫存管理',
|
||||
'權限範圍內的操作',
|
||||
]
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'type' => 'required|in:main,branch',
|
||||
'address' => 'nullable|string|max:500',
|
||||
]);
|
||||
$validated['company_id'] = Auth::user()->company_id;
|
||||
Warehouse::create($validated);
|
||||
|
||||
return redirect()->route('admin.warehouses.index')
|
||||
->with('success', __('Warehouse created successfully'));
|
||||
}
|
||||
|
||||
// 庫存管理單
|
||||
public function stockManagement()
|
||||
public function update(Request $request, Warehouse $warehouse)
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '庫存管理單',
|
||||
'description' => '倉庫庫存異動管理',
|
||||
'features' => [
|
||||
'庫存盤點',
|
||||
'庫存調整',
|
||||
'異動記錄查詢',
|
||||
]
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'type' => 'required|in:main,branch',
|
||||
'address' => 'nullable|string|max:500',
|
||||
'is_active' => 'required|boolean',
|
||||
]);
|
||||
$warehouse->update($validated);
|
||||
|
||||
return redirect()->route('admin.warehouses.index')
|
||||
->with('success', __('Warehouse updated successfully'));
|
||||
}
|
||||
|
||||
// 調撥單
|
||||
public function transfers()
|
||||
public function destroy(Warehouse $warehouse)
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '調撥單',
|
||||
'description' => '倉庫間商品調撥作業',
|
||||
'features' => [
|
||||
'建立調撥單',
|
||||
'調撥單審核',
|
||||
'調撥歷史記錄',
|
||||
]
|
||||
]);
|
||||
if ($warehouse->stocks()->where('quantity', '>', 0)->exists()) {
|
||||
return redirect()->route('admin.warehouses.index')
|
||||
->with('error', __('Cannot delete warehouse with existing stock'));
|
||||
}
|
||||
$warehouse->delete();
|
||||
|
||||
return redirect()->route('admin.warehouses.index')
|
||||
->with('success', __('Warehouse deleted successfully'));
|
||||
}
|
||||
|
||||
// 採購單
|
||||
public function purchases()
|
||||
public function toggleStatus(Warehouse $warehouse)
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '採購單',
|
||||
'description' => '商品採購申請與管理',
|
||||
'features' => [
|
||||
'建立採購申請',
|
||||
'採購單追蹤',
|
||||
'供應商管理',
|
||||
]
|
||||
]);
|
||||
$warehouse->update(['is_active' => !$warehouse->is_active]);
|
||||
$status = $warehouse->is_active ? __('enabled') : __('disabled');
|
||||
|
||||
return redirect()->route('admin.warehouses.index')
|
||||
->with('success', __('Warehouse :status successfully', ['status' => $status]));
|
||||
}
|
||||
|
||||
// 機台補貨單
|
||||
public function replenishments()
|
||||
// ─── 模組 2:庫存管理 ───
|
||||
|
||||
public function inventory(Request $request)
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '機台補貨單',
|
||||
'description' => '機台補貨工單建立與管理',
|
||||
'features' => [
|
||||
'建立補貨單',
|
||||
'補貨排程',
|
||||
'補貨人員指派',
|
||||
]
|
||||
]);
|
||||
$tab = $request->input('tab', 'stock');
|
||||
$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);
|
||||
|
||||
if ($request->filled('warehouse_id')) {
|
||||
$query->where('warehouse_id', $request->input('warehouse_id'));
|
||||
}
|
||||
if ($search = $request->input('search')) {
|
||||
$query->whereHas('product', fn($q) => $q->where('name', 'like', "%{$search}%"));
|
||||
}
|
||||
|
||||
$data['stocks'] = $query->orderBy('warehouse_id')
|
||||
->paginate($request->input('per_page', 15))
|
||||
->withQueryString();
|
||||
|
||||
} elseif ($tab === 'stock-in') {
|
||||
$query = StockInOrder::with(['warehouse:id,name', 'creator:id,name'])
|
||||
->latest();
|
||||
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->input('status'));
|
||||
}
|
||||
$data['orders'] = $query->paginate(10)->withQueryString();
|
||||
$data['products'] = Product::orderBy('name')->get(['id', 'name', 'image_url']);
|
||||
|
||||
} elseif ($tab === 'movements') {
|
||||
$query = StockMovement::with(['warehouse:id,name', 'product:id,name', 'creator:id,name'])
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
if ($request->filled('warehouse_id')) {
|
||||
$query->where('warehouse_id', $request->input('warehouse_id'));
|
||||
}
|
||||
if ($request->filled('type')) {
|
||||
$query->where('type', $request->input('type'));
|
||||
}
|
||||
$data['movements'] = $query->paginate(15)->withQueryString();
|
||||
}
|
||||
|
||||
return view('admin.warehouses.inventory', $data);
|
||||
}
|
||||
|
||||
// 機台補貨紀錄
|
||||
public function replenishmentRecords()
|
||||
/**
|
||||
* 建立入庫單
|
||||
*/
|
||||
public function storeStockIn(Request $request)
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '機台補貨紀錄',
|
||||
'description' => '個別機台的補貨歷史記錄',
|
||||
$validated = $request->validate([
|
||||
'warehouse_id' => 'required|exists:warehouses,id',
|
||||
'note' => 'nullable|string|max:500',
|
||||
'items' => 'required|array|min:1',
|
||||
'items.*.product_id' => 'required|exists:products,id',
|
||||
'items.*.quantity' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
DB::transaction(function () use ($validated) {
|
||||
$order = StockInOrder::create([
|
||||
'company_id' => Auth::user()->company_id,
|
||||
'warehouse_id' => $validated['warehouse_id'],
|
||||
'order_no' => 'SI-' . now()->format('Ymd') . '-' . str_pad(StockInOrder::whereDate('created_at', today())->count() + 1, 4, '0', STR_PAD_LEFT),
|
||||
'status' => 'draft',
|
||||
'note' => $validated['note'] ?? null,
|
||||
'created_by' => Auth::id(),
|
||||
]);
|
||||
|
||||
foreach ($validated['items'] as $item) {
|
||||
$order->items()->create($item);
|
||||
}
|
||||
});
|
||||
|
||||
return redirect()->route('admin.warehouses.inventory', ['tab' => 'stock-in'])
|
||||
->with('success', __('Stock-in order created'));
|
||||
}
|
||||
|
||||
// 機台補貨紀錄(總)
|
||||
public function replenishmentRecordsAll()
|
||||
/**
|
||||
* 確認入庫單(草稿 → 完成,同時增加庫存)
|
||||
*/
|
||||
public function confirmStockIn(StockInOrder $stockInOrder)
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '機台補貨紀錄(總)',
|
||||
'description' => '所有機台的補貨總覽',
|
||||
]);
|
||||
if ($stockInOrder->status !== 'draft') {
|
||||
return back()->with('error', __('Order already processed'));
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($stockInOrder) {
|
||||
$stockInOrder->update([
|
||||
'status' => 'completed',
|
||||
'completed_at' => now(),
|
||||
]);
|
||||
|
||||
foreach ($stockInOrder->items as $item) {
|
||||
// 更新或建立倉庫庫存
|
||||
$stock = WarehouseStock::firstOrCreate(
|
||||
['warehouse_id' => $stockInOrder->warehouse_id, 'product_id' => $item->product_id],
|
||||
['quantity' => 0, 'safety_stock' => 0]
|
||||
);
|
||||
|
||||
$before = $stock->quantity;
|
||||
$stock->increment('quantity', $item->quantity);
|
||||
|
||||
// 記錄異動
|
||||
StockMovement::create([
|
||||
'company_id' => $stockInOrder->company_id,
|
||||
'warehouse_id' => $stockInOrder->warehouse_id,
|
||||
'product_id' => $item->product_id,
|
||||
'type' => 'in',
|
||||
'quantity' => $item->quantity,
|
||||
'before_qty' => $before,
|
||||
'after_qty' => $before + $item->quantity,
|
||||
'reference_type' => StockInOrder::class,
|
||||
'reference_id' => $stockInOrder->id,
|
||||
'note' => __('Stock-in order') . ' #' . $stockInOrder->order_no,
|
||||
'created_by' => Auth::id(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
return back()->with('success', __('Stock-in order confirmed and inventory updated'));
|
||||
}
|
||||
|
||||
// 機台庫存
|
||||
public function machineStock()
|
||||
// ─── 模組 3:調撥單 ───
|
||||
|
||||
public function transfers(Request $request)
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '機台庫存',
|
||||
'description' => '各機台即時庫存查詢',
|
||||
]);
|
||||
$query = TransferOrder::with(['fromWarehouse:id,name', 'toWarehouse:id,name', 'fromMachine:id,name,serial_no', 'creator:id,name', 'items'])
|
||||
->latest();
|
||||
|
||||
if ($request->filled('type')) {
|
||||
$query->where('type', $request->input('type'));
|
||||
}
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->input('status'));
|
||||
}
|
||||
|
||||
$orders = $query->paginate(10)->withQueryString();
|
||||
$warehouses = Warehouse::active()->orderBy('name')->get(['id', 'name', 'type']);
|
||||
$machines = Machine::orderBy('name')->get(['id', 'name', 'serial_no']);
|
||||
$products = Product::orderBy('name')->get(['id', 'name', 'image_url']);
|
||||
|
||||
return view('admin.warehouses.transfers', compact('orders', 'warehouses', 'machines', 'products'));
|
||||
}
|
||||
|
||||
// 人員庫存
|
||||
public function staffStock()
|
||||
/**
|
||||
* 建立調撥單(W2W 或 M2W)
|
||||
*/
|
||||
public function storeTransfer(Request $request)
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '人員庫存',
|
||||
'description' => '補貨人員持有庫存',
|
||||
$validated = $request->validate([
|
||||
'type' => 'required|in:warehouse_to_warehouse,machine_to_warehouse',
|
||||
'from_warehouse_id' => 'required_if:type,warehouse_to_warehouse|nullable|exists:warehouses,id',
|
||||
'from_machine_id' => 'required_if:type,machine_to_warehouse|nullable|exists:machines,id',
|
||||
'to_warehouse_id' => 'required|exists:warehouses,id',
|
||||
'note' => 'nullable|string|max:500',
|
||||
'items' => 'required|array|min:1',
|
||||
'items.*.product_id' => 'required|exists:products,id',
|
||||
'items.*.quantity' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
DB::transaction(function () use ($validated) {
|
||||
$order = TransferOrder::create([
|
||||
'company_id' => Auth::user()->company_id,
|
||||
'order_no' => 'TR-' . now()->format('Ymd') . '-' . str_pad(TransferOrder::whereDate('created_at', today())->count() + 1, 4, '0', STR_PAD_LEFT),
|
||||
'type' => $validated['type'],
|
||||
'from_warehouse_id' => $validated['from_warehouse_id'] ?? null,
|
||||
'from_machine_id' => $validated['from_machine_id'] ?? null,
|
||||
'to_warehouse_id' => $validated['to_warehouse_id'],
|
||||
'status' => TransferOrder::STATUS_DRAFT,
|
||||
'note' => $validated['note'] ?? null,
|
||||
'created_by' => Auth::id(),
|
||||
]);
|
||||
|
||||
foreach ($validated['items'] as $item) {
|
||||
$order->items()->create($item);
|
||||
}
|
||||
});
|
||||
|
||||
return redirect()->route('admin.warehouses.transfers')
|
||||
->with('success', __('Transfer order created'));
|
||||
}
|
||||
|
||||
// 回庫單
|
||||
public function returns()
|
||||
/**
|
||||
* 確認調撥(扣除來源庫存 + 增加目標庫存)
|
||||
*/
|
||||
public function confirmTransfer(TransferOrder $transferOrder)
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '回庫單',
|
||||
'description' => '商品退回倉庫管理',
|
||||
if ($transferOrder->status !== TransferOrder::STATUS_DRAFT) {
|
||||
return back()->with('error', __('Order already processed'));
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($transferOrder) {
|
||||
$transferOrder->loadMissing('items');
|
||||
|
||||
foreach ($transferOrder->items as $item) {
|
||||
// 扣除來源庫存(僅 W2W)
|
||||
if ($transferOrder->type === TransferOrder::TYPE_W2W && $transferOrder->from_warehouse_id) {
|
||||
$fromStock = WarehouseStock::where('warehouse_id', $transferOrder->from_warehouse_id)
|
||||
->where('product_id', $item->product_id)->first();
|
||||
|
||||
if (!$fromStock || $fromStock->quantity < $item->quantity) {
|
||||
throw new \Exception(__('Insufficient stock for transfer'));
|
||||
}
|
||||
|
||||
$beforeFrom = $fromStock->quantity;
|
||||
$fromStock->decrement('quantity', $item->quantity);
|
||||
|
||||
StockMovement::create([
|
||||
'company_id' => $transferOrder->company_id,
|
||||
'warehouse_id' => $transferOrder->from_warehouse_id,
|
||||
'product_id' => $item->product_id,
|
||||
'type' => 'transfer_out',
|
||||
'quantity' => $item->quantity,
|
||||
'before_qty' => $beforeFrom,
|
||||
'after_qty' => $beforeFrom - $item->quantity,
|
||||
'reference_type' => TransferOrder::class,
|
||||
'reference_id' => $transferOrder->id,
|
||||
'note' => __('Transfer out') . ' #' . $transferOrder->order_no,
|
||||
'created_by' => Auth::id(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
// 增加目標倉庫庫存
|
||||
$toStock = WarehouseStock::firstOrCreate(
|
||||
['warehouse_id' => $transferOrder->to_warehouse_id, 'product_id' => $item->product_id],
|
||||
['quantity' => 0, 'safety_stock' => 0]
|
||||
);
|
||||
$beforeTo = $toStock->quantity;
|
||||
$toStock->increment('quantity', $item->quantity);
|
||||
|
||||
StockMovement::create([
|
||||
'company_id' => $transferOrder->company_id,
|
||||
'warehouse_id' => $transferOrder->to_warehouse_id,
|
||||
'product_id' => $item->product_id,
|
||||
'type' => 'transfer_in',
|
||||
'quantity' => $item->quantity,
|
||||
'before_qty' => $beforeTo,
|
||||
'after_qty' => $beforeTo + $item->quantity,
|
||||
'reference_type' => TransferOrder::class,
|
||||
'reference_id' => $transferOrder->id,
|
||||
'note' => __('Transfer in') . ' #' . $transferOrder->order_no,
|
||||
'created_by' => Auth::id(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
$transferOrder->update([
|
||||
'status' => TransferOrder::STATUS_COMPLETED,
|
||||
'completed_at' => now(),
|
||||
]);
|
||||
});
|
||||
|
||||
return back()->with('success', __('Transfer completed'));
|
||||
}
|
||||
|
||||
// ─── 模組 4:機台庫存總覽 ───
|
||||
|
||||
public function machineInventory(Request $request)
|
||||
{
|
||||
$query = Machine::with(['slots' => fn($q) => $q->with('product:id,name,image_url')->orderByRaw('CAST(slot_no AS UNSIGNED) ASC')])
|
||||
->withCount('slots as total_slots')
|
||||
->withSum('slots as total_stock', 'stock');
|
||||
|
||||
if ($search = $request->input('search')) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('serial_no', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
$machines = $query->orderBy('name')
|
||||
->paginate($request->input('per_page', 10))
|
||||
->withQueryString();
|
||||
|
||||
return view('admin.warehouses.machine-inventory', compact('machines'));
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX:取得單台機台貨道詳情
|
||||
*/
|
||||
public function machineSlots(Machine $machine)
|
||||
{
|
||||
$slots = $machine->slots()
|
||||
->with('product:id,name,image_url')
|
||||
->orderByRaw('CAST(slot_no AS UNSIGNED) ASC')
|
||||
->get();
|
||||
|
||||
return response()->json(['success' => true, 'slots' => $slots]);
|
||||
}
|
||||
|
||||
// ─── 模組 5:機台補貨 ───
|
||||
|
||||
public function replenishments(Request $request)
|
||||
{
|
||||
$query = ReplenishmentOrder::with(['warehouse:id,name', 'machine:id,name,serial_no', 'assignee:id,name', 'creator:id,name'])
|
||||
->withCount('items')
|
||||
->latest();
|
||||
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->input('status'));
|
||||
}
|
||||
|
||||
$orders = $query->paginate(10)->withQueryString();
|
||||
$warehouses = Warehouse::active()->orderBy('name')->get(['id', 'name']);
|
||||
$machines = Machine::orderBy('name')->get(['id', 'name', 'serial_no']);
|
||||
|
||||
return view('admin.warehouses.replenishments', compact('orders', 'warehouses', 'machines'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立補貨單
|
||||
*/
|
||||
public function storeReplenishment(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'warehouse_id' => 'required|exists:warehouses,id',
|
||||
'machine_id' => 'required|exists:machines,id',
|
||||
'note' => 'nullable|string|max:500',
|
||||
'items' => 'required|array|min:1',
|
||||
'items.*.product_id' => 'required|exists:products,id',
|
||||
'items.*.slot_no' => 'required|integer',
|
||||
'items.*.quantity' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
DB::transaction(function () use ($validated) {
|
||||
$order = ReplenishmentOrder::create([
|
||||
'company_id' => Auth::user()->company_id,
|
||||
'order_no' => 'RP-' . now()->format('Ymd') . '-' . str_pad(ReplenishmentOrder::whereDate('created_at', today())->count() + 1, 4, '0', STR_PAD_LEFT),
|
||||
'warehouse_id' => $validated['warehouse_id'],
|
||||
'machine_id' => $validated['machine_id'],
|
||||
'status' => ReplenishmentOrder::STATUS_PENDING,
|
||||
'note' => $validated['note'] ?? null,
|
||||
'created_by' => Auth::id(),
|
||||
]);
|
||||
|
||||
foreach ($validated['items'] as $item) {
|
||||
$order->items()->create($item);
|
||||
}
|
||||
});
|
||||
|
||||
return redirect()->route('admin.warehouses.replenishments')
|
||||
->with('success', __('Replenishment order created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 確認補貨完成(扣倉庫庫存 + 增加機台貨道庫存)
|
||||
*/
|
||||
public function confirmReplenishment(ReplenishmentOrder $replenishmentOrder)
|
||||
{
|
||||
if ($replenishmentOrder->status === ReplenishmentOrder::STATUS_COMPLETED) {
|
||||
return back()->with('error', __('Order already completed'));
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($replenishmentOrder) {
|
||||
$replenishmentOrder->loadMissing('items');
|
||||
|
||||
foreach ($replenishmentOrder->items as $item) {
|
||||
// 扣除倉庫庫存
|
||||
$stock = WarehouseStock::where('warehouse_id', $replenishmentOrder->warehouse_id)
|
||||
->where('product_id', $item->product_id)->first();
|
||||
|
||||
if ($stock && $stock->quantity >= $item->quantity) {
|
||||
$before = $stock->quantity;
|
||||
$stock->decrement('quantity', $item->quantity);
|
||||
|
||||
StockMovement::create([
|
||||
'company_id' => $replenishmentOrder->company_id,
|
||||
'warehouse_id' => $replenishmentOrder->warehouse_id,
|
||||
'product_id' => $item->product_id,
|
||||
'type' => 'out',
|
||||
'quantity' => $item->quantity,
|
||||
'before_qty' => $before,
|
||||
'after_qty' => $before - $item->quantity,
|
||||
'reference_type' => ReplenishmentOrder::class,
|
||||
'reference_id' => $replenishmentOrder->id,
|
||||
'note' => __('Replenishment') . ' #' . $replenishmentOrder->order_no,
|
||||
'created_by' => Auth::id(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
// 增加機台貨道庫存
|
||||
MachineSlot::where('machine_id', $replenishmentOrder->machine_id)
|
||||
->where('slot_no', $item->slot_no)
|
||||
->increment('stock', $item->quantity);
|
||||
}
|
||||
|
||||
$replenishmentOrder->update([
|
||||
'status' => ReplenishmentOrder::STATUS_COMPLETED,
|
||||
'completed_at' => now(),
|
||||
]);
|
||||
});
|
||||
|
||||
return back()->with('success', __('Replenishment completed'));
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,10 +31,18 @@ class Machine extends Model
|
||||
});
|
||||
});
|
||||
|
||||
// 當 api_token 發生變動時,自動同步至 Redis (供 MQTT 認證使用)
|
||||
// 當 api_token 或 serial_no 發生變動時,自動同步至 Redis (供 MQTT 認證使用)
|
||||
static::saved(function ($machine) {
|
||||
if ($machine->wasChanged('api_token') || $machine->wasRecentlyCreated) {
|
||||
if ($machine->wasChanged('api_token') || $machine->wasChanged('serial_no') || $machine->wasRecentlyCreated) {
|
||||
app(\App\Services\Machine\MachineService::class)->syncMqttAuth($machine);
|
||||
|
||||
// 如果是修改序號,則刪除舊的 Redis Key 避免殘留
|
||||
if ($machine->wasChanged('serial_no') && !$machine->wasRecentlyCreated) {
|
||||
$oldSerial = $machine->getOriginal('serial_no');
|
||||
if ($oldSerial) {
|
||||
\Illuminate\Support\Facades\Redis::del("machine_auth:{$oldSerial}");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
78
app/Models/Warehouse/ReplenishmentOrder.php
Normal file
78
app/Models/Warehouse/ReplenishmentOrder.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Warehouse;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use App\Traits\TenantScoped;
|
||||
|
||||
class ReplenishmentOrder extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes, TenantScoped;
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'order_no',
|
||||
'warehouse_id',
|
||||
'machine_id',
|
||||
'status',
|
||||
'assigned_to',
|
||||
'note',
|
||||
'created_by',
|
||||
'completed_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'completed_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 狀態常數
|
||||
*/
|
||||
public const STATUS_PENDING = 'pending';
|
||||
public const STATUS_PREPARED = 'prepared';
|
||||
public const STATUS_DELIVERING = 'delivering';
|
||||
public const STATUS_COMPLETED = 'completed';
|
||||
public const STATUS_CANCELLED = 'cancelled';
|
||||
|
||||
/**
|
||||
* 出貨倉庫
|
||||
*/
|
||||
public function warehouse()
|
||||
{
|
||||
return $this->belongsTo(Warehouse::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 目標機台
|
||||
*/
|
||||
public function machine()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Machine\Machine::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 補貨單明細
|
||||
*/
|
||||
public function items()
|
||||
{
|
||||
return $this->hasMany(ReplenishmentOrderItem::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 指派人員
|
||||
*/
|
||||
public function assignee()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\System\User::class, 'assigned_to');
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立人
|
||||
*/
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\System\User::class, 'created_by');
|
||||
}
|
||||
}
|
||||
38
app/Models/Warehouse/ReplenishmentOrderItem.php
Normal file
38
app/Models/Warehouse/ReplenishmentOrderItem.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Warehouse;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ReplenishmentOrderItem extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'replenishment_order_id',
|
||||
'product_id',
|
||||
'slot_no',
|
||||
'quantity',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'quantity' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* 所屬補貨單
|
||||
*/
|
||||
public function replenishmentOrder()
|
||||
{
|
||||
return $this->belongsTo(ReplenishmentOrder::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 對應商品
|
||||
*/
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Product\Product::class);
|
||||
}
|
||||
}
|
||||
51
app/Models/Warehouse/StockInOrder.php
Normal file
51
app/Models/Warehouse/StockInOrder.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Warehouse;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use App\Traits\TenantScoped;
|
||||
|
||||
class StockInOrder extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes, TenantScoped;
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'warehouse_id',
|
||||
'order_no',
|
||||
'status',
|
||||
'note',
|
||||
'created_by',
|
||||
'completed_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'completed_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 所屬倉庫
|
||||
*/
|
||||
public function warehouse()
|
||||
{
|
||||
return $this->belongsTo(Warehouse::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 入庫單明細
|
||||
*/
|
||||
public function items()
|
||||
{
|
||||
return $this->hasMany(StockInOrderItem::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立人
|
||||
*/
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\System\User::class, 'created_by');
|
||||
}
|
||||
}
|
||||
40
app/Models/Warehouse/StockInOrderItem.php
Normal file
40
app/Models/Warehouse/StockInOrderItem.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Warehouse;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class StockInOrderItem extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'stock_in_order_id',
|
||||
'product_id',
|
||||
'quantity',
|
||||
'batch_no',
|
||||
'expiry_date',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'quantity' => 'integer',
|
||||
'expiry_date' => 'date:Y-m-d',
|
||||
];
|
||||
|
||||
/**
|
||||
* 所屬入庫單
|
||||
*/
|
||||
public function stockInOrder()
|
||||
{
|
||||
return $this->belongsTo(StockInOrder::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 對應商品
|
||||
*/
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Product\Product::class);
|
||||
}
|
||||
}
|
||||
82
app/Models/Warehouse/StockMovement.php
Normal file
82
app/Models/Warehouse/StockMovement.php
Normal file
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Warehouse;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Traits\TenantScoped;
|
||||
|
||||
class StockMovement extends Model
|
||||
{
|
||||
use TenantScoped;
|
||||
|
||||
/**
|
||||
* 此表為唯讀日誌,僅有 created_at
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'warehouse_id',
|
||||
'product_id',
|
||||
'type',
|
||||
'quantity',
|
||||
'before_qty',
|
||||
'after_qty',
|
||||
'reference_type',
|
||||
'reference_id',
|
||||
'note',
|
||||
'created_by',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'quantity' => 'integer',
|
||||
'before_qty' => 'integer',
|
||||
'after_qty' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 異動類型對應中文
|
||||
*/
|
||||
public const TYPE_LABELS = [
|
||||
'in' => 'Stock In',
|
||||
'out' => 'Stock Out',
|
||||
'adjust' => 'Adjustment',
|
||||
'damage' => 'Damage',
|
||||
'transfer_in' => 'Transfer In',
|
||||
'transfer_out' => 'Transfer Out',
|
||||
];
|
||||
|
||||
/**
|
||||
* 所屬倉庫
|
||||
*/
|
||||
public function warehouse()
|
||||
{
|
||||
return $this->belongsTo(Warehouse::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 對應商品
|
||||
*/
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Product\Product::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作人
|
||||
*/
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\System\User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 關聯單據(多態)
|
||||
*/
|
||||
public function reference()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
||||
85
app/Models/Warehouse/TransferOrder.php
Normal file
85
app/Models/Warehouse/TransferOrder.php
Normal file
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Warehouse;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use App\Traits\TenantScoped;
|
||||
|
||||
class TransferOrder extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes, TenantScoped;
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'order_no',
|
||||
'type',
|
||||
'from_warehouse_id',
|
||||
'from_machine_id',
|
||||
'to_warehouse_id',
|
||||
'status',
|
||||
'note',
|
||||
'created_by',
|
||||
'completed_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'completed_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 調撥類型常數
|
||||
*/
|
||||
public const TYPE_W2W = 'warehouse_to_warehouse';
|
||||
public const TYPE_M2W = 'machine_to_warehouse';
|
||||
|
||||
/**
|
||||
* 狀態常數
|
||||
*/
|
||||
public const STATUS_DRAFT = 'draft';
|
||||
public const STATUS_PENDING = 'pending';
|
||||
public const STATUS_IN_TRANSIT = 'in_transit';
|
||||
public const STATUS_COMPLETED = 'completed';
|
||||
public const STATUS_CANCELLED = 'cancelled';
|
||||
|
||||
/**
|
||||
* 來源倉庫
|
||||
*/
|
||||
public function fromWarehouse()
|
||||
{
|
||||
return $this->belongsTo(Warehouse::class, 'from_warehouse_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 來源機台(機台退回時)
|
||||
*/
|
||||
public function fromMachine()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Machine\Machine::class, 'from_machine_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 目標倉庫
|
||||
*/
|
||||
public function toWarehouse()
|
||||
{
|
||||
return $this->belongsTo(Warehouse::class, 'to_warehouse_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 調撥單明細
|
||||
*/
|
||||
public function items()
|
||||
{
|
||||
return $this->hasMany(TransferOrderItem::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立人
|
||||
*/
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\System\User::class, 'created_by');
|
||||
}
|
||||
}
|
||||
37
app/Models/Warehouse/TransferOrderItem.php
Normal file
37
app/Models/Warehouse/TransferOrderItem.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Warehouse;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TransferOrderItem extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'transfer_order_id',
|
||||
'product_id',
|
||||
'quantity',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'quantity' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* 所屬調撥單
|
||||
*/
|
||||
public function transferOrder()
|
||||
{
|
||||
return $this->belongsTo(TransferOrder::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 對應商品
|
||||
*/
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Product\Product::class);
|
||||
}
|
||||
}
|
||||
82
app/Models/Warehouse/Warehouse.php
Normal file
82
app/Models/Warehouse/Warehouse.php
Normal file
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Warehouse;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use App\Traits\TenantScoped;
|
||||
|
||||
class Warehouse extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes, TenantScoped;
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'name',
|
||||
'type',
|
||||
'address',
|
||||
'manager_user_id',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Scope:僅篩選啟用的倉庫
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope:僅篩選總倉
|
||||
*/
|
||||
public function scopeMain($query)
|
||||
{
|
||||
return $query->where('type', 'main');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope:僅篩選分倉
|
||||
*/
|
||||
public function scopeBranch($query)
|
||||
{
|
||||
return $query->where('type', 'branch');
|
||||
}
|
||||
|
||||
/**
|
||||
* 倉庫負責人
|
||||
*/
|
||||
public function manager()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\System\User::class, 'manager_user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 倉庫庫存(各商品)
|
||||
*/
|
||||
public function stocks()
|
||||
{
|
||||
return $this->hasMany(WarehouseStock::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 庫存異動紀錄
|
||||
*/
|
||||
public function movements()
|
||||
{
|
||||
return $this->hasMany(StockMovement::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 入庫單
|
||||
*/
|
||||
public function stockInOrders()
|
||||
{
|
||||
return $this->hasMany(StockInOrder::class);
|
||||
}
|
||||
}
|
||||
48
app/Models/Warehouse/WarehouseStock.php
Normal file
48
app/Models/Warehouse/WarehouseStock.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Warehouse;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WarehouseStock extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'warehouse_id',
|
||||
'product_id',
|
||||
'quantity',
|
||||
'safety_stock',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'quantity' => 'integer',
|
||||
'safety_stock' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Scope:低於安全庫存
|
||||
*/
|
||||
public function scopeLowStock($query)
|
||||
{
|
||||
return $query->whereColumn('quantity', '<=', 'safety_stock')
|
||||
->where('safety_stock', '>', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 所屬倉庫
|
||||
*/
|
||||
public function warehouse()
|
||||
{
|
||||
return $this->belongsTo(Warehouse::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 對應商品
|
||||
*/
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Product\Product::class);
|
||||
}
|
||||
}
|
||||
@ -145,7 +145,7 @@ services:
|
||||
MQTT_GATEWAY_CLIENT_ID: '${MQTT_GATEWAY_CLIENT_ID:-star-cloud-gateway}'
|
||||
MQTT_REDIS_ADDR: '${MQTT_REDIS_ADDR:-redis:6379}'
|
||||
MQTT_REDIS_PASSWORD: '${REDIS_PASSWORD:-}'
|
||||
MQTT_REDIS_PREFIX: '${MQTT_REDIS_PREFIX:-starcloud_database_}'
|
||||
MQTT_REDIS_PREFIX: '${MQTT_REDIS_PREFIX:-star_cloud_database_}'
|
||||
MQTT_INCOMING_QUEUE: '${MQTT_INCOMING_QUEUE:-mqtt_incoming_jobs}'
|
||||
MQTT_OUTGOING_QUEUE: '${MQTT_OUTGOING_QUEUE:-mqtt_outgoing_commands}'
|
||||
MQTT_USER: '${MQTT_USER:-star-cloud-gateway}'
|
||||
|
||||
@ -437,6 +437,20 @@ return [
|
||||
'firmware_version' => '2.1.6'
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => '機台連線狀態上報 (Status/LWT)',
|
||||
'slug' => 'mqtt-status',
|
||||
'action' => 'PUB',
|
||||
'topic' => 'machine/{serial_no}/status',
|
||||
'qos' => 0,
|
||||
'description' => '用於機台連線與斷線的即時狀態同步。通常用於 MQTT 的遺囑訊息 (LWT) 設定,或由 Broker 連線事件觸發。',
|
||||
'payload_parameters' => [
|
||||
'status' => ['type' => 'string', 'description' => '連線狀態。接受值:online, offline'],
|
||||
],
|
||||
'payload_example' => [
|
||||
'status' => 'online'
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => '機台異常上報 (Error)',
|
||||
'slug' => 'mqtt-error',
|
||||
@ -446,7 +460,7 @@ return [
|
||||
'description' => '當機台發生硬體故障(如卡貨、通訊中斷)時即時上報。',
|
||||
'payload_parameters' => [
|
||||
'tid' => ['type' => 'integer', 'description' => '貨道編號或任務 ID'],
|
||||
'error_code' => ['type' => 'string', 'description' => '硬體錯誤代碼 (如 0403 代表卡貨)'],
|
||||
'error_code' => ['type' => 'string', 'description' => '硬體錯誤代碼,詳見下方 B013 代碼表'],
|
||||
],
|
||||
'payload_example' => [
|
||||
'tid' => 12,
|
||||
@ -466,7 +480,7 @@ return [
|
||||
'amount' => ['type' => 'number', 'description' => '交易金額 (create/invoice 時使用)'],
|
||||
'slot_no' => ['type' => 'string', 'description' => '出貨貨道號碼'],
|
||||
'invoice_no' => ['type' => 'string', 'description' => '發票號碼 (action 為 invoice 時必填)'],
|
||||
'status' => ['type' => 'string', 'description' => '出貨結果狀態 (action 為 dispense 時必填,如 success, failed)'],
|
||||
'status' => ['type' => 'string', 'description' => '出貨結果狀態 (action 為 dispense 時必填,見狀態定義表)'],
|
||||
],
|
||||
'payload_example' => [
|
||||
'action' => 'create',
|
||||
@ -582,7 +596,7 @@ return [
|
||||
'description' => '機台執行完雲端指令後的主動回報。此回報極為重要,雲端將依此決定交易是否完成或需進行庫存回滾。',
|
||||
'payload_parameters' => [
|
||||
'command_id' => ['type' => 'string', 'description' => '對應下發時的指令 ID'],
|
||||
'result' => ['type' => 'string', 'description' => '執行結果,僅接受: success 或 failed'],
|
||||
'result' => ['type' => 'string', 'description' => '執行結果,見狀態定義表'],
|
||||
'stock' => ['type' => 'integer', 'description' => '執行指令後的最終剩餘庫存 (選填,通常出貨指令需帶上)'],
|
||||
'message' => ['type' => 'string', 'description' => '額外的錯誤訊息 (選填)'],
|
||||
],
|
||||
@ -594,5 +608,39 @@ return [
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
|
||||
'status_definitions' => [
|
||||
[
|
||||
'name' => 'B013: 硬體錯誤代碼 (MQTT Error Topic)',
|
||||
'description' => '當機台透過 machine/{serial_no}/error 上報時,error_code 欄位所使用的代碼。',
|
||||
'codes' => [
|
||||
['code' => '0402', 'label' => '出貨成功', 'level' => 'info', 'description' => '商品成功掉落至取貨口。'],
|
||||
['code' => '0403', 'label' => '貨道卡貨', 'level' => 'error', 'description' => '馬達轉動異常或偵測到卡貨,需人工排除。'],
|
||||
['code' => '0202', 'label' => '貨道缺貨', 'level' => 'warning', 'description' => '偵測到實體貨道已無商品。'],
|
||||
['code' => '0412', 'label' => '昇降機上升異常', 'level' => 'error', 'description' => '昇降馬達無法正確到達指定位置。'],
|
||||
['code' => '0415', 'label' => '取貨門異常', 'level' => 'error', 'description' => '取貨門馬達損壞或感應器失效。'],
|
||||
['code' => '5402', 'label' => '取貨門未關', 'level' => 'warning', 'description' => '取貨逾時或門片未正確閉合。'],
|
||||
['code' => '5403', 'label' => '昇降系統故障', 'level' => 'error', 'description' => '昇降機通訊中斷或驅動板異常。'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => '交易與指令執行狀態 (Transaction & Command)',
|
||||
'description' => '用於 Transaction (dispense) 與 Command ACK 的執行結果判定。',
|
||||
'codes' => [
|
||||
['code' => 'success', 'label' => '成功', 'description' => '動作執行圓滿完成。'],
|
||||
['code' => 'failed', 'label' => '失敗', 'description' => '動作執行失敗,雲端將視情況發起退款或記錄異常。'],
|
||||
['code' => '1', 'label' => '成功 (數字型態)', 'description' => '部分舊型指令或 B602 使用的成功代碼。'],
|
||||
['code' => '0', 'label' => '失敗 (數字型態)', 'description' => '部分舊型指令或 B602 使用的失敗代碼。'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => '機台連線狀態 (Connection Status)',
|
||||
'description' => '用於 machine/{serial_no}/status 主題,反映機台與 Broker 的連線情況。',
|
||||
'codes' => [
|
||||
['code' => 'online', 'label' => '在線', 'description' => '機台已成功連線至 MQTT Broker。'],
|
||||
['code' => 'offline', 'label' => '離線', 'description' => '機台已斷開連線(通常由 LWT 遺囑訊息觸發)。'],
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 建立倉庫主表
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('warehouses', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->nullable()->constrained()->comment('所屬租戶');
|
||||
$table->string('name')->comment('倉庫名稱');
|
||||
$table->enum('type', ['main', 'branch'])->default('branch')->comment('倉庫類型:總倉/分倉');
|
||||
$table->string('address')->nullable()->comment('地址');
|
||||
$table->foreignId('manager_user_id')->nullable()->constrained('users')->nullOnDelete()->comment('負責人');
|
||||
$table->boolean('is_active')->default(true)->comment('啟用/停用');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
// 索引:常用篩選條件
|
||||
$table->index(['company_id', 'type']);
|
||||
$table->index(['company_id', 'is_active']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('warehouses');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 建立倉庫庫存表(每個倉庫各商品的即時庫存)
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('warehouse_stocks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('warehouse_id')->constrained()->cascadeOnDelete()->comment('所屬倉庫');
|
||||
$table->foreignId('product_id')->constrained()->comment('商品');
|
||||
$table->integer('quantity')->default(0)->comment('現有庫存數量');
|
||||
$table->integer('safety_stock')->default(0)->comment('安全庫存量');
|
||||
$table->timestamps();
|
||||
|
||||
// 唯一約束:同一倉庫同一商品只能有一筆記錄
|
||||
$table->unique(['warehouse_id', 'product_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('warehouse_stocks');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 建立入庫單 + 入庫單明細
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('stock_in_orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->nullable()->constrained()->comment('所屬租戶');
|
||||
$table->foreignId('warehouse_id')->constrained()->comment('入庫目標倉庫');
|
||||
$table->string('order_no')->unique()->comment('入庫單號');
|
||||
$table->enum('status', ['draft', 'completed'])->default('draft')->comment('狀態');
|
||||
$table->text('note')->nullable()->comment('備註');
|
||||
$table->foreignId('created_by')->constrained('users')->comment('建立人');
|
||||
$table->timestamp('completed_at')->nullable()->comment('入庫完成時間');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
// 索引
|
||||
$table->index(['company_id', 'status']);
|
||||
$table->index(['company_id', 'created_at']);
|
||||
});
|
||||
|
||||
Schema::create('stock_in_order_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('stock_in_order_id')->constrained()->cascadeOnDelete()->comment('所屬入庫單');
|
||||
$table->foreignId('product_id')->constrained()->comment('商品');
|
||||
$table->integer('quantity')->comment('入庫數量');
|
||||
$table->string('batch_no')->nullable()->comment('批號');
|
||||
$table->date('expiry_date')->nullable()->comment('有效期限');
|
||||
$table->timestamps();
|
||||
|
||||
// 索引
|
||||
$table->index('stock_in_order_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('stock_in_order_items');
|
||||
Schema::dropIfExists('stock_in_orders');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 建立庫存異動紀錄表
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('stock_movements', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->nullable()->constrained()->comment('所屬租戶');
|
||||
$table->foreignId('warehouse_id')->constrained()->comment('異動倉庫');
|
||||
$table->foreignId('product_id')->constrained()->comment('商品');
|
||||
$table->enum('type', [
|
||||
'in', // 入庫
|
||||
'out', // 出庫(補貨扣減)
|
||||
'adjust', // 盤點調整
|
||||
'damage', // 報損
|
||||
'transfer_in', // 調撥入庫
|
||||
'transfer_out', // 調撥出庫
|
||||
])->comment('異動類型');
|
||||
$table->integer('quantity')->comment('異動數量(正數入庫、負數出庫)');
|
||||
$table->integer('before_qty')->comment('異動前數量');
|
||||
$table->integer('after_qty')->comment('異動後數量');
|
||||
$table->string('reference_type')->nullable()->comment('關聯單據類型(多態)');
|
||||
$table->unsignedBigInteger('reference_id')->nullable()->comment('關聯單據 ID');
|
||||
$table->string('note')->nullable()->comment('備註/原因');
|
||||
$table->foreignId('created_by')->constrained('users')->comment('操作人');
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
// 索引:高頻查詢
|
||||
$table->index(['company_id', 'created_at']);
|
||||
$table->index(['warehouse_id', 'product_id']);
|
||||
$table->index(['reference_type', 'reference_id']);
|
||||
$table->index('type');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('stock_movements');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 建立調撥單 + 調撥單明細(含倉庫間調撥 & 機台退回倉庫)
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('transfer_orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->nullable()->constrained()->comment('所屬租戶');
|
||||
$table->string('order_no')->unique()->comment('調撥單號');
|
||||
$table->enum('type', ['warehouse_to_warehouse', 'machine_to_warehouse'])->comment('調撥類型');
|
||||
$table->foreignId('from_warehouse_id')->nullable()->constrained('warehouses')->comment('來源倉庫');
|
||||
$table->unsignedBigInteger('from_machine_id')->nullable()->comment('來源機台(機台退回時)');
|
||||
$table->foreignId('to_warehouse_id')->constrained('warehouses')->comment('目標倉庫');
|
||||
$table->enum('status', ['draft', 'pending', 'in_transit', 'completed', 'cancelled'])->default('draft')->comment('狀態');
|
||||
$table->text('note')->nullable()->comment('備註');
|
||||
$table->foreignId('created_by')->constrained('users')->comment('建立人');
|
||||
$table->timestamp('completed_at')->nullable()->comment('完成時間');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
// 外鍵:from_machine_id 手動建立(因為可能為 null 且跨表)
|
||||
$table->foreign('from_machine_id')->references('id')->on('machines')->nullOnDelete();
|
||||
|
||||
// 索引
|
||||
$table->index(['company_id', 'status']);
|
||||
$table->index(['company_id', 'created_at']);
|
||||
$table->index('type');
|
||||
});
|
||||
|
||||
Schema::create('transfer_order_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('transfer_order_id')->constrained()->cascadeOnDelete()->comment('所屬調撥單');
|
||||
$table->foreignId('product_id')->constrained()->comment('商品');
|
||||
$table->integer('quantity')->comment('調撥數量');
|
||||
$table->timestamps();
|
||||
|
||||
// 索引
|
||||
$table->index('transfer_order_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('transfer_order_items');
|
||||
Schema::dropIfExists('transfer_orders');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 建立補貨單 + 補貨單明細
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('replenishment_orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->nullable()->constrained()->comment('所屬租戶');
|
||||
$table->string('order_no')->unique()->comment('補貨單號');
|
||||
$table->foreignId('warehouse_id')->constrained()->comment('出貨倉庫');
|
||||
$table->unsignedBigInteger('machine_id')->comment('目標機台');
|
||||
$table->enum('status', ['pending', 'prepared', 'delivering', 'completed', 'cancelled'])->default('pending')->comment('狀態');
|
||||
$table->foreignId('assigned_to')->nullable()->constrained('users')->nullOnDelete()->comment('指派補貨人員');
|
||||
$table->text('note')->nullable()->comment('備註');
|
||||
$table->foreignId('created_by')->constrained('users')->comment('建立人');
|
||||
$table->timestamp('completed_at')->nullable()->comment('完成時間');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
// 外鍵
|
||||
$table->foreign('machine_id')->references('id')->on('machines');
|
||||
|
||||
// 索引
|
||||
$table->index(['company_id', 'status']);
|
||||
$table->index(['company_id', 'created_at']);
|
||||
$table->index('machine_id');
|
||||
$table->index('assigned_to');
|
||||
});
|
||||
|
||||
Schema::create('replenishment_order_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('replenishment_order_id')->constrained()->cascadeOnDelete()->comment('所屬補貨單');
|
||||
$table->foreignId('product_id')->constrained()->comment('商品');
|
||||
$table->string('slot_no')->nullable()->comment('貨道編號');
|
||||
$table->integer('quantity')->comment('補貨數量');
|
||||
$table->timestamps();
|
||||
|
||||
// 索引
|
||||
$table->index('replenishment_order_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('replenishment_order_items');
|
||||
Schema::dropIfExists('replenishment_orders');
|
||||
}
|
||||
};
|
||||
@ -10,8 +10,8 @@ class MachineSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
// 建立 50 台機台
|
||||
Machine::factory()->count(50)->create()->each(function ($machine) {
|
||||
// 建立 2 台機台
|
||||
Machine::factory()->count(2)->create()->each(function ($machine) {
|
||||
// 每台機台隨機建立 5-10 筆初始日誌
|
||||
MachineLog::factory()->count(rand(5, 10))->create([
|
||||
'machine_id' => $machine->id,
|
||||
|
||||
160
lang/en.json
160
lang/en.json
@ -702,6 +702,7 @@
|
||||
"Please check the form for errors.": "Please check the form for errors.",
|
||||
"Please confirm the details below": "Please confirm the details below",
|
||||
"Please select a machine first": "Please select a machine first",
|
||||
"Please select a machine model": "Please select a machine model",
|
||||
"Please select a machine to view and manage its advertisements.": "Please select a machine to view and manage its advertisements.",
|
||||
"Please select a machine to view metrics": "請選擇機台以查看數據",
|
||||
"Please select a material": "Please select a material",
|
||||
@ -1188,5 +1189,162 @@
|
||||
"This slot has a pending update. Please wait for the previous command to complete.": "This slot has a pending update. Please wait for the previous command to complete.",
|
||||
"This slot has a pending command. Please wait.": "This slot has a pending command. Please wait.",
|
||||
"Out of stock.": "Out of stock.",
|
||||
"Save error:": "Save error:"
|
||||
"Save error:": "Save error:",
|
||||
"Warehouse Overview": "Warehouse Overview",
|
||||
"Inventory Management": "Inventory Management",
|
||||
"Transfer Orders": "Transfer Orders",
|
||||
"Machine Replenishment": "Machine Replenishment",
|
||||
"Machine Inventory Overview": "Machine Inventory Overview",
|
||||
"Manage your warehouses, including main and branch warehouses": "Manage your warehouses, including main and branch warehouses",
|
||||
"View all warehouses": "View all warehouses",
|
||||
"Create main and branch warehouses": "Create main and branch warehouses",
|
||||
"Assign warehouse managers": "Assign warehouse managers",
|
||||
"Monitor warehouse stock summary": "Monitor warehouse stock summary",
|
||||
"Manage warehouse stock levels, stock-in orders, adjustments, and movement history": "Manage warehouse stock levels, stock-in orders, adjustments, and movement history",
|
||||
"Stock overview by warehouse": "Stock overview by warehouse",
|
||||
"Create stock-in orders": "Create stock-in orders",
|
||||
"Stock adjustments and damage reports": "Stock adjustments and damage reports",
|
||||
"Complete movement history log": "Complete movement history log",
|
||||
"Manage stock transfers between warehouses and machine returns": "Manage stock transfers between warehouses and machine returns",
|
||||
"Warehouse to warehouse transfers": "Warehouse to warehouse transfers",
|
||||
"Machine to warehouse returns": "Machine to warehouse returns",
|
||||
"Transfer order status tracking": "Transfer order status tracking",
|
||||
"Create replenishment orders and manage restocking from warehouse to machines": "Create replenishment orders and manage restocking from warehouse to machines",
|
||||
"Smart replenishment suggestions": "Smart replenishment suggestions",
|
||||
"One-click replenishment order generation": "One-click replenishment order generation",
|
||||
"Assign replenishment staff": "Assign replenishment staff",
|
||||
"Replenishment history": "Replenishment history",
|
||||
"Real-time inventory status across all machines with expiry tracking": "Real-time inventory status across all machines with expiry tracking",
|
||||
"View slot-level inventory for each machine": "View slot-level inventory for each machine",
|
||||
"Low stock and out-of-stock alerts": "Low stock and out-of-stock alerts",
|
||||
"Expiry date tracking and warnings": "Expiry date tracking and warnings",
|
||||
"Quick replenishment from this view": "Quick replenishment from this view",
|
||||
"Cash Module": "Cash Module",
|
||||
"Save Settings": "Save Settings",
|
||||
"System Settings": "System Settings",
|
||||
"Tax System": "Tax System",
|
||||
"Electronic Invoice": "Electronic Invoice",
|
||||
"Card System": "Card System",
|
||||
"Credit Card / Contactless": "Credit Card / Contactless",
|
||||
"Scan Pay": "Scan Pay",
|
||||
"ESUN Scan Pay": "ESUN Scan Pay",
|
||||
"LinePay": "LinePay",
|
||||
"Other Features": "Other Features",
|
||||
"Shopping Cart": "Shopping Cart",
|
||||
"Goods & System Settings": "Goods & System Settings",
|
||||
"Display Material Code": "Display Material Code",
|
||||
"Enable Points Mechanism": "Enable Points Mechanism",
|
||||
"Taxation System": "Taxation System",
|
||||
"Card Terminal System": "Card Terminal System",
|
||||
"QR Scan Payment": "QR Scan Payment",
|
||||
"Shopping Cart Feature": "Shopping Cart Feature",
|
||||
"Welcome Gift Feature": "Welcome Gift Feature",
|
||||
"Cash Module Feature": "Cash Module Feature",
|
||||
"Includes Credit Card/Mobile Pay": "Includes Credit Card/Mobile Pay",
|
||||
"E.SUN QR Pay": "E.SUN QR Pay",
|
||||
"LinePay Payment": "LinePay Payment",
|
||||
"Coin/Bill Acceptor": "Coin/Bill Acceptor",
|
||||
"System settings updated successfully.": "System settings updated successfully.",
|
||||
"Update Settings": "Update Settings",
|
||||
"Material Code": "Material Code",
|
||||
"Points": "Points",
|
||||
"Warehouse Management": "Warehouse Management",
|
||||
"Warehouse Overview": "Warehouse Overview",
|
||||
"Add Warehouse": "Add Warehouse",
|
||||
"Total Warehouses": "Total Warehouses",
|
||||
"Main Warehouses": "Main Warehouses",
|
||||
"Branch Warehouses": "Branch Warehouses",
|
||||
"Low Stock Alerts": "Low Stock Alerts",
|
||||
"Search warehouses...": "Search warehouses...",
|
||||
"Main": "Main",
|
||||
"Branch": "Branch",
|
||||
"Warehouse Info": "Warehouse Info",
|
||||
"Products / Stock": "Products / Stock",
|
||||
"Products": "Products",
|
||||
"Stock": "Stock",
|
||||
"Warehouse created successfully.": "Warehouse created successfully.",
|
||||
"Warehouse updated successfully.": "Warehouse updated successfully.",
|
||||
"Warehouse deleted successfully.": "Warehouse deleted successfully.",
|
||||
"Cannot delete warehouse with existing stock": "Cannot delete warehouse with existing stock",
|
||||
"Inventory Management": "Inventory Management",
|
||||
"Track stock levels, stock-in orders, and movement history": "Track stock levels, stock-in orders, and movement history",
|
||||
"New Stock-In Order": "New Stock-In Order",
|
||||
"Stock Levels": "Stock Levels",
|
||||
"Stock-In Orders": "Stock-In Orders",
|
||||
"Movement History": "Movement History",
|
||||
"Search products...": "Search products...",
|
||||
"All Warehouses": "All Warehouses",
|
||||
"Product": "Product",
|
||||
"Warehouse": "Warehouse",
|
||||
"Quantity": "Quantity",
|
||||
"Safety Stock": "Safety Stock",
|
||||
"Low Stock": "Low Stock",
|
||||
"Out of Stock": "Out of Stock",
|
||||
"Normal": "Normal",
|
||||
"No stock data found": "No stock data found",
|
||||
"Order No.": "Order No.",
|
||||
"Created By": "Created By",
|
||||
"Created At": "Created At",
|
||||
"Draft": "Draft",
|
||||
"Completed": "Completed",
|
||||
"Confirm this stock-in order?": "Confirm this stock-in order?",
|
||||
"Confirm": "Confirm",
|
||||
"No stock-in orders found": "No stock-in orders found",
|
||||
"Time": "Time",
|
||||
"Qty Change": "Qty Change",
|
||||
"No movement records found": "No movement records found",
|
||||
"Target Warehouse": "Target Warehouse",
|
||||
"Select Warehouse": "Select Warehouse",
|
||||
"Add Item": "Add Item",
|
||||
"Select Product": "Select Product",
|
||||
"Qty": "Qty",
|
||||
"Note": "Note",
|
||||
"Optional": "Optional",
|
||||
"Transfer Orders": "Transfer Orders",
|
||||
"Manage stock transfers between warehouses and machine returns": "Manage stock transfers between warehouses and machine returns",
|
||||
"New Transfer": "New Transfer",
|
||||
"All Types": "All Types",
|
||||
"Warehouse to Warehouse": "Warehouse to Warehouse",
|
||||
"Machine to Warehouse": "Machine to Warehouse",
|
||||
"All Statuses": "All Statuses",
|
||||
"Cancelled": "Cancelled",
|
||||
"From": "From",
|
||||
"To": "To",
|
||||
"Confirm this transfer?": "Confirm this transfer?",
|
||||
"No transfer orders found": "No transfer orders found",
|
||||
"Transfer Type": "Transfer Type",
|
||||
"From Warehouse": "From Warehouse",
|
||||
"From Machine": "From Machine",
|
||||
"Select Machine": "Select Machine",
|
||||
"To Warehouse": "To Warehouse",
|
||||
"Note (optional)": "Note (optional)",
|
||||
"Machine Inventory Overview": "Machine Inventory Overview",
|
||||
"Real-time slot-level inventory across all machines": "Real-time slot-level inventory across all machines",
|
||||
"Search machines...": "Search machines...",
|
||||
"Serial No.": "Serial No.",
|
||||
"Total Stock": "Total Stock",
|
||||
"Fill Rate": "Fill Rate",
|
||||
"View Slots": "View Slots",
|
||||
"Loading...": "Loading...",
|
||||
"Empty": "Empty",
|
||||
"Expiry": "Expiry",
|
||||
"No slot data": "No slot data",
|
||||
"Machine Replenishment": "Machine Replenishment",
|
||||
"Create and manage replenishment orders from warehouse to machines": "Create and manage replenishment orders from warehouse to machines",
|
||||
"New Replenishment": "New Replenishment",
|
||||
"Pending": "Pending",
|
||||
"Prepared": "Prepared",
|
||||
"Delivering": "Delivering",
|
||||
"Confirm replenishment completed?": "Confirm replenishment completed?",
|
||||
"No replenishment orders found": "No replenishment orders found",
|
||||
"Source Warehouse": "Source Warehouse",
|
||||
"Target Machine": "Target Machine",
|
||||
"Replenishment Items": "Replenishment Items",
|
||||
"Slot": "Slot",
|
||||
"Stock In": "Stock In",
|
||||
"Stock Out": "Stock Out",
|
||||
"Adjustment": "Adjustment",
|
||||
"Damage": "Damage",
|
||||
"Transfer In": "Transfer In",
|
||||
"Transfer Out": "Transfer Out"
|
||||
}
|
||||
121
lang/ja.json
121
lang/ja.json
@ -1 +1,120 @@
|
||||
null
|
||||
{
|
||||
"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": "設定を更新",
|
||||
"Material Code": "素材コード",
|
||||
"Points": "ポイント",
|
||||
"Please enter configuration name": "設定名を入力してください",
|
||||
"Please select a company": "会社を選択してください",
|
||||
"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 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 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": "転送出"
|
||||
}
|
||||
279
lang/zh_TW.json
279
lang/zh_TW.json
@ -240,6 +240,7 @@
|
||||
"Customer Details": "客戶詳情",
|
||||
"Customer enabled successfully.": "客戶已成功啟用。",
|
||||
"Customer Info": "客戶資訊",
|
||||
"Customer List": "客戶列表",
|
||||
"Customer Management": "客戶管理",
|
||||
"Customer Payment Config": "客戶金流設定",
|
||||
"Customer updated successfully.": "客戶更新成功。",
|
||||
@ -469,6 +470,7 @@
|
||||
"Machine model deleted successfully.": "機台型號已成功刪除。",
|
||||
"Machine Model Settings": "機台型號設定",
|
||||
"Machine model updated successfully.": "機台型號已成功更新。",
|
||||
"Machine permissions updated successfully.": "機台權限已成功更新。",
|
||||
"Machine Name": "機台名稱",
|
||||
"Machine Serial No": "機台序號",
|
||||
"Machine Permissions": "機台權限",
|
||||
@ -704,8 +706,10 @@
|
||||
"Playback Order": "播放順序",
|
||||
"Please check the following errors:": "請檢查以下錯誤:",
|
||||
"Please check the form for errors.": "請檢查欄位內容是否正確。",
|
||||
"Please confirm the details below": "請確認以下指令詳情",
|
||||
"Please enter configuration name": "請輸入設定名稱",
|
||||
"Please select a company": "請選擇公司",
|
||||
"Please select a machine first": "請先選擇機台",
|
||||
"Please select a machine model": "請選擇機台型號",
|
||||
"Please select a machine to view and manage its advertisements.": "請選擇一個機台以查看並管理其廣告。",
|
||||
"Please select a machine to view metrics": "請選擇機台以查看數據",
|
||||
"Please select a material": "請選擇素材",
|
||||
@ -1197,5 +1201,276 @@
|
||||
"This slot has a pending update. Please wait for the previous command to complete.": "此貨道已有更新指令在執行中,請等候前一個指令完成。",
|
||||
"This slot has a pending command. Please wait.": "此貨道已有指令正在執行,請稍後。",
|
||||
"Out of stock.": "庫存不足。",
|
||||
"Save error:": "儲存錯誤:"
|
||||
"Save error:": "儲存錯誤:",
|
||||
"Warehouse Overview": "倉庫總覽",
|
||||
"Inventory Management": "庫存管理",
|
||||
"Transfer Orders": "調撥單",
|
||||
"Machine Replenishment": "機台補貨",
|
||||
"Machine Inventory Overview": "機台庫存總覽",
|
||||
"Manage your warehouses, including main and branch warehouses": "管理您的倉庫,包含總倉與分倉設定",
|
||||
"View all warehouses": "查看所有倉庫",
|
||||
"Create main and branch warehouses": "建立總倉與分倉",
|
||||
"Assign warehouse managers": "指派倉庫負責人",
|
||||
"Monitor warehouse stock summary": "監控倉庫庫存摘要",
|
||||
"Manage warehouse stock levels, stock-in orders, adjustments, and movement history": "管理倉庫庫存水位、入庫單、盤點調整與異動紀錄",
|
||||
"Stock overview by warehouse": "依倉庫查看庫存概況",
|
||||
"Create stock-in orders": "建立入庫單",
|
||||
"Stock adjustments and damage reports": "庫存調整與報損處理",
|
||||
"Complete movement history log": "完整的庫存異動歷程",
|
||||
"Manage stock transfers between warehouses and machine returns": "管理倉庫間調撥與機台退回作業",
|
||||
"Warehouse to warehouse transfers": "倉庫對倉庫調撥",
|
||||
"Machine to warehouse returns": "機台退回倉庫",
|
||||
"Transfer order status tracking": "調撥單狀態追蹤",
|
||||
"Create replenishment orders and manage restocking from warehouse to machines": "建立補貨單,管理從倉庫到機台的補貨流程",
|
||||
"Smart replenishment suggestions": "智能補貨建議",
|
||||
"One-click replenishment order generation": "一鍵產生補貨單",
|
||||
"Assign replenishment staff": "指派補貨人員",
|
||||
"Replenishment history": "補貨歷史紀錄",
|
||||
"Real-time inventory status across all machines with expiry tracking": "即時掌握所有機台庫存狀態與有效期限追蹤",
|
||||
"View slot-level inventory for each machine": "查看各機台貨道庫存",
|
||||
"Low stock and out-of-stock alerts": "低庫存與缺貨警示",
|
||||
"Expiry date tracking and warnings": "有效期限追蹤與預警",
|
||||
"Quick replenishment from this view": "從此畫面快速補貨",
|
||||
"Add Warehouse": "新增倉庫",
|
||||
"Edit Warehouse": "編輯倉庫",
|
||||
"Warehouse Name": "倉庫名稱",
|
||||
"Warehouse Type": "倉庫類型",
|
||||
"Warehouse Info": "倉庫資訊",
|
||||
"Total Warehouses": "倉庫總數",
|
||||
"Main Warehouses": "總倉數量",
|
||||
"Branch Warehouses": "分倉數量",
|
||||
"Low Stock Alerts": "低庫存警示",
|
||||
"Products / Stock": "商品 / 庫存",
|
||||
"Search warehouses...": "搜尋倉庫...",
|
||||
"e.g. Main Warehouse": "如:總倉A",
|
||||
"Warehouse created successfully": "倉庫建立成功",
|
||||
"Warehouse updated successfully": "倉庫更新成功",
|
||||
"Warehouse deleted successfully": "倉庫已刪除",
|
||||
"Cannot delete warehouse with existing stock": "無法刪除仍有庫存的倉庫",
|
||||
"Warehouse :status successfully": "倉庫已:status",
|
||||
"enabled": "啟用",
|
||||
"disabled": "停用",
|
||||
"No warehouses found": "未找到倉庫",
|
||||
"Disable Warehouse": "停用倉庫",
|
||||
"Are you sure to delete this warehouse? This action cannot be undone.": "確定要刪除此倉庫嗎?此操作無法復原。",
|
||||
"Are you sure to disable this warehouse?": "確定要停用此倉庫嗎?",
|
||||
"System Settings": "系統設定",
|
||||
"Tax System": "稅務系統",
|
||||
"Electronic Invoice": "電子發票",
|
||||
"Card System": "刷卡機系統",
|
||||
"Credit Card / Contactless": "支援信用卡 / 感應支付",
|
||||
"Scan Pay": "掃碼支付",
|
||||
"ESUN Scan Pay": "玉山掃碼支付",
|
||||
"LinePay": "LinePay",
|
||||
"Other Features": "其他功能",
|
||||
"Shopping Cart": "購物車功能",
|
||||
"Cash Module": "硬幣機 / 紙鈔機模組",
|
||||
"Save Settings": "儲存設定",
|
||||
"Track stock levels, stock-in orders, and movement history": "追蹤庫存水位、入庫單與異動紀錄",
|
||||
"Stock Levels": "庫存水位",
|
||||
"Stock-In Orders": "入庫單",
|
||||
"Movement History": "異動紀錄",
|
||||
"New Stock-In Order": "新增入庫單",
|
||||
"All Warehouses": "所有倉庫",
|
||||
"Quantity": "數量",
|
||||
"Safety Stock": "安全庫存",
|
||||
"Low Stock": "低庫存",
|
||||
"Out of Stock": "缺貨",
|
||||
"Normal": "正常",
|
||||
"No stock data found": "未找到庫存資料",
|
||||
"Order No.": "單號",
|
||||
"Created By": "建立者",
|
||||
"Confirm": "確認",
|
||||
"Confirm this stock-in order?": "確認此入庫單?",
|
||||
"No stock-in orders found": "未找到入庫單",
|
||||
"All Types": "所有類型",
|
||||
"All Statuses": "所有狀態",
|
||||
"Stock In": "入庫",
|
||||
"Stock Out": "出庫",
|
||||
"Adjustment": "調整",
|
||||
"Damage": "報損",
|
||||
"Transfer In": "調入",
|
||||
"Transfer Out": "調出",
|
||||
"Qty Change": "數量變動",
|
||||
"Operator": "操作人員",
|
||||
"No movement records found": "未找到異動紀錄",
|
||||
"Target Warehouse": "目標倉庫",
|
||||
"Select Warehouse": "選擇倉庫",
|
||||
"Select Product": "選擇商品",
|
||||
"Add Item": "新增項目",
|
||||
"Qty": "數量",
|
||||
"Stock-in order created": "入庫單已建立",
|
||||
"Order already processed": "訂單已處理",
|
||||
"Stock-in order confirmed and inventory updated": "入庫單已確認,庫存已更新",
|
||||
"Stock-in order": "入庫單",
|
||||
"Manage stock transfers between warehouses and machine returns": "管理倉庫間調撥與機台退貨",
|
||||
"New Transfer": "新增調撥",
|
||||
"Transfer Type": "調撥類型",
|
||||
"Warehouse to Warehouse": "倉庫對倉庫",
|
||||
"Machine to Warehouse": "機台退回倉庫",
|
||||
"From Warehouse": "來源倉庫",
|
||||
"From Machine": "來源機台",
|
||||
"To Warehouse": "目標倉庫",
|
||||
"Select Machine": "選擇機台",
|
||||
"Note (optional)": "備註(選填)",
|
||||
"From": "來源",
|
||||
"To": "目標",
|
||||
"No transfer orders found": "未找到調撥單",
|
||||
"Transfer order created": "調撥單已建立",
|
||||
"Confirm this transfer?": "確認此調撥?",
|
||||
"Insufficient stock for transfer": "庫存不足,無法調撥",
|
||||
"Transfer completed": "調撥完成",
|
||||
"Transfer out": "調出",
|
||||
"Transfer in": "調入",
|
||||
"Real-time slot-level inventory across all machines": "即時查看各機台貨道庫存",
|
||||
"Serial No.": "序號",
|
||||
"Slots": "貨道數",
|
||||
"Total Stock": "總庫存",
|
||||
"Fill Rate": "補貨率",
|
||||
"View Slots": "查看貨道",
|
||||
"Search machines...": "搜尋機台...",
|
||||
"Empty": "空置",
|
||||
"Expiry": "有效期限",
|
||||
"No slot data": "無貨道資料",
|
||||
"Create and manage replenishment orders from warehouse to machines": "建立及管理從倉庫到機台的補貨單",
|
||||
"New Replenishment": "新增補貨單",
|
||||
"Source Warehouse": "來源倉庫",
|
||||
"Target Machine": "目標機台",
|
||||
"Replenishment Items": "補貨項目",
|
||||
"Slot": "貨道",
|
||||
"Pending": "待處理",
|
||||
"Prepared": "已備貨",
|
||||
"Delivering": "配送中",
|
||||
"No replenishment orders found": "未找到補貨單",
|
||||
"Replenishment order created": "補貨單已建立",
|
||||
"Order already completed": "訂單已完成",
|
||||
"Replenishment completed": "補貨完成",
|
||||
"Replenishment": "補貨",
|
||||
"Confirm replenishment completed?": "確認補貨已完成?",
|
||||
"Complete": "完成",
|
||||
"Goods & System Settings": "商品與系統設定",
|
||||
"Display Material Code": "顯示物料代碼",
|
||||
"Enable Points Mechanism": "啟用點數機制",
|
||||
"Taxation System": "稅務系統",
|
||||
"Card Terminal System": "刷卡機系統",
|
||||
"QR Scan Payment": "掃碼支付功能",
|
||||
"Shopping Cart Feature": "購物車功能",
|
||||
"Welcome Gift Feature": "來店禮功能",
|
||||
"Cash Module Feature": "現金模組功能",
|
||||
"Includes Credit Card/Mobile Pay": "包括 /信用卡/卡片支付/手機支付",
|
||||
"E.SUN QR Pay": "玉山掃碼支付",
|
||||
"LinePay Payment": "LinePay 支付",
|
||||
"Coin/Bill Acceptor": "硬幣機/紙鈔機",
|
||||
"System settings updated successfully.": "系統設定更新成功。",
|
||||
"Update Settings": "更新設定",
|
||||
"Disable Customer Confirmation": "停用客戶確認",
|
||||
"Disabling this customer will also disable all accounts under this customer. Are you sure you want to continue?": "停用此客戶將會連帶停用該客戶下的所有帳號,確定要繼續嗎?",
|
||||
"Are you sure to delete this customer?": "確定要刪除此客戶嗎?",
|
||||
"Customer List": "客戶列表",
|
||||
"Manage all tenant accounts and validity": "管理所有租戶帳號及其效期",
|
||||
"Add Customer": "新增客戶",
|
||||
"Edit Settings": "編輯設定",
|
||||
"Delete Customer": "刪除客戶",
|
||||
"Customer Details": "客戶詳情",
|
||||
"Close Panel": "關閉面板",
|
||||
"Settings Saved": "設定已儲存",
|
||||
"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 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 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": "轉出"
|
||||
}
|
||||
@ -26,7 +26,7 @@ func LoadConfig() *Config {
|
||||
RedisPassword: getEnv("MQTT_REDIS_PASSWORD", ""),
|
||||
IncomingQueueKey: getEnv("MQTT_INCOMING_QUEUE", "mqtt_incoming_jobs"),
|
||||
OutgoingQueueKey: getEnv("MQTT_OUTGOING_QUEUE", "mqtt_outgoing_commands"),
|
||||
RedisPrefix: getEnv("MQTT_REDIS_PREFIX", "starcloud_database_"),
|
||||
RedisPrefix: getEnv("MQTT_REDIS_PREFIX", "star_cloud_database_"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -32,7 +32,7 @@ $baseRoute = 'admin.data-config.advertisements';
|
||||
</div>
|
||||
<div class="flex items-center gap-3" x-show="activeTab === 'list'">
|
||||
<button @click="openAddModal()" class="btn-luxury-primary">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
<span>{{ __('Add Advertisement') }}</span>
|
||||
@ -86,7 +86,7 @@ $baseRoute = 'admin.data-config.advertisements';
|
||||
<img src="{{ $ad->url }}" class="w-full h-full object-cover">
|
||||
@else
|
||||
<div class="w-full h-full flex items-center justify-center bg-slate-900 group-hover:bg-cyan-900 transition-colors">
|
||||
<svg class="size-4 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5.14v14l11-7-11-7z"/></svg>
|
||||
<svg class="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5.14v14l11-7-11-7z"/></svg>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@ -136,11 +136,11 @@ $baseRoute = 'admin.data-config.advertisements';
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<div class="flex justify-end items-center gap-2">
|
||||
<button @click="openEditModal(@js($ad))" 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">
|
||||
<svg class="size-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 @click="openEditModal(@js($ad))" 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 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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 @click="confirmDelete(@js($ad->id))" 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">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 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 @click="confirmDelete(@js($ad->id))" class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 hover:bg-rose-500/5 transition-all border border-transparent hover:border-rose-500/20" title="{{ __('Delete') }}">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 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>
|
||||
</div>
|
||||
</td>
|
||||
@ -186,11 +186,11 @@ $baseRoute = 'admin.data-config.advertisements';
|
||||
<button x-cloak x-show="machineAds['{{ $pos }}'] && machineAds['{{ $pos }}'].length > 0"
|
||||
@click="startSequencePreview('{{ $pos }}')"
|
||||
class="p-1.5 px-3 text-[10px] font-black bg-slate-800 dark:bg-slate-700 text-white rounded-lg hover:bg-slate-700 transition-colors uppercase flex items-center gap-1">
|
||||
<svg class="size-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
{{ __('Preview') }}
|
||||
</button>
|
||||
<button @click="openAssignModal('{{ $pos }}')" class="p-1.5 px-3 text-[10px] font-black bg-cyan-500 text-white rounded-lg hover:bg-cyan-600 transition-colors uppercase shadow-sm shadow-cyan-500/20 flex items-center gap-1">
|
||||
<svg class="size-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 4.5v15m7.5-7.5h-15" /></svg>
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 4.5v15m7.5-7.5h-15" /></svg>
|
||||
{{ __('Ad Settings') }}
|
||||
</button>
|
||||
</div>
|
||||
@ -199,7 +199,7 @@ $baseRoute = 'admin.data-config.advertisements';
|
||||
<div class="luxury-card p-4 bg-slate-50/50 dark:bg-slate-900/40 border border-slate-100 dark:border-white/5 space-y-3 min-h-[150px]">
|
||||
<template x-if="!machineAds['{{ $pos }}'] || machineAds['{{ $pos }}'].length === 0">
|
||||
<div class="flex flex-col items-center justify-center h-full py-10">
|
||||
<svg class="size-6 text-slate-300 dark:text-slate-600 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v6m-3-3h6m-9-3a9 9 0 1118 0 9 9 0 01-18 0z"/></svg>
|
||||
<svg class="w-6 h-6 text-slate-300 dark:text-slate-600 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v6m-3-3h6m-9-3a9 9 0 1118 0 9 9 0 01-18 0z"/></svg>
|
||||
<span class="text-[10px] font-black uppercase tracking-widest text-slate-400 dark:text-slate-500">{{ __('No assignments') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
@ -209,23 +209,23 @@ $baseRoute = 'admin.data-config.advertisements';
|
||||
<!-- Sort Controls -->
|
||||
<div class="flex flex-col gap-1 shrink-0 bg-slate-50 dark:bg-slate-900 rounded-lg p-1 border border-slate-100 dark:border-white/5">
|
||||
<button @click.prevent="moveUp('{{ $pos }}', index)" :disabled="index === 0" class="p-1 text-slate-400 hover:text-cyan-500 disabled:opacity-30 disabled:hover:text-slate-400 transition-colors">
|
||||
<svg class="size-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M5 15l7-7 7 7" /></svg>
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M5 15l7-7 7 7" /></svg>
|
||||
</button>
|
||||
<button @click.prevent="moveDown('{{ $pos }}', index)" :disabled="index === machineAds['{{ $pos }}'].length - 1" class="p-1 text-slate-400 hover:text-cyan-500 disabled:opacity-30 disabled:hover:text-slate-400 transition-colors">
|
||||
<svg class="size-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" /></svg>
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button @click="openPreview(assign.advertisement)" class="w-12 h-12 rounded-lg bg-slate-100 dark:bg-slate-900 border border-white/5 overflow-hidden shrink-0 hover:border-cyan-500/50 hover:shadow-[0_0_15px_rgba(6,182,212,0.3)] transition-all relative group/thumb">
|
||||
<div class="absolute inset-0 bg-black/40 flex items-center justify-center opacity-0 group-hover/thumb:opacity-100 transition-opacity z-10">
|
||||
<svg class="size-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /></svg>
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /></svg>
|
||||
</div>
|
||||
<template x-if="assign.advertisement.type === 'image'">
|
||||
<img :src="assign.advertisement.url" class="w-full h-full object-cover">
|
||||
</template>
|
||||
<template x-if="assign.advertisement.type === 'video'">
|
||||
<div class="w-full h-full flex items-center justify-center bg-slate-900 group-hover/thumb:bg-cyan-900 transition-colors">
|
||||
<svg class="size-4 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5.14v14l11-7-11-7z"/></svg>
|
||||
<svg class="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5.14v14l11-7-11-7z"/></svg>
|
||||
</div>
|
||||
</template>
|
||||
</button>
|
||||
@ -234,7 +234,7 @@ $baseRoute = 'admin.data-config.advertisements';
|
||||
<p class="text-[11px] font-mono font-bold text-slate-400 uppercase tracking-tight mt-0.5" x-text="assign.advertisement.duration + 's'"></p>
|
||||
</div>
|
||||
<button @click="removeAssignment(assign.id)" class="p-1.5 text-slate-300 hover:text-rose-500 transition-colors">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></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="M6 18L18 6M6 6l12 12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@ -273,7 +273,7 @@ $baseRoute = 'admin.data-config.advertisements';
|
||||
<!-- Close Button -->
|
||||
<button @click="isPreviewOpen = false"
|
||||
class="absolute -top-12 right-0 p-2 text-white/50 hover:text-white transition-colors">
|
||||
<svg class="size-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
</button>
|
||||
|
||||
<!-- Content Area -->
|
||||
@ -313,7 +313,7 @@ $baseRoute = 'admin.data-config.advertisements';
|
||||
<!-- Close Button -->
|
||||
<button @click="stopSequencePreview()"
|
||||
class="absolute -top-12 right-0 p-2 text-white/50 hover:text-white transition-colors">
|
||||
<svg class="size-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
</button>
|
||||
|
||||
<!-- Media Container -->
|
||||
@ -349,15 +349,15 @@ $baseRoute = 'admin.data-config.advertisements';
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click.stop="prevSequenceAd()" class="p-2.5 bg-black/60 backdrop-blur-md rounded-xl border border-white/10 text-white hover:bg-white/20 transition-all group">
|
||||
<svg class="size-5 group-hover:-translate-x-0.5 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M15 19l-7-7 7-7"/></svg>
|
||||
<svg class="w-5 h-5 group-hover:-translate-x-0.5 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M15 19l-7-7 7-7"/></svg>
|
||||
</button>
|
||||
<!-- Play/Pause -->
|
||||
<button @click.stop="toggleSequencePlay()" class="p-3 bg-cyan-500 backdrop-blur-md rounded-xl border border-cyan-400/30 text-white hover:bg-cyan-400 transition-all shadow-[0_0_15px_rgba(6,182,212,0.4)]">
|
||||
<svg x-show="!isSequencePaused" class="size-5" fill="currentColor" viewBox="0 0 24 24"><path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z"/></svg>
|
||||
<svg x-show="isSequencePaused" class="size-5 ml-0.5" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
||||
<svg x-show="!isSequencePaused" class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z"/></svg>
|
||||
<svg x-show="isSequencePaused" class="w-5 h-5 ml-0.5" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
||||
</button>
|
||||
<button @click.stop="nextSequenceAd()" class="p-2.5 bg-black/60 backdrop-blur-md rounded-xl border border-white/10 text-white hover:bg-white/20 transition-all group">
|
||||
<svg class="size-5 group-hover:translate-x-0.5 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 5l7 7-7 7"/></svg>
|
||||
<svg class="w-5 h-5 group-hover:translate-x-0.5 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 5l7 7-7 7"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">{{ __('Manage your ad material details') }}</p>
|
||||
</div>
|
||||
<button @click="isAdModalOpen = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
||||
<svg class="size-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>
|
||||
<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>
|
||||
|
||||
@ -112,7 +112,7 @@
|
||||
class="w-full h-12 bg-slate-50 dark:bg-slate-800/50 border-none rounded-xl px-4 pr-10 text-sm font-bold text-slate-800 dark:text-white focus:ring-2 focus:ring-cyan-500/20 transition-all placeholder:text-slate-400"
|
||||
placeholder="YYYY/MM/DD HH:MM">
|
||||
<div class="absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 group-hover/input:text-cyan-500 transition-colors pointer-events-none">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -133,7 +133,7 @@
|
||||
class="w-full h-12 bg-slate-50 dark:bg-slate-800/50 border-none rounded-xl px-4 pr-10 text-sm font-bold text-slate-800 dark:text-white focus:ring-2 focus:ring-cyan-500/20 transition-all placeholder:text-slate-400"
|
||||
placeholder="YYYY/MM/DD HH:MM">
|
||||
<div class="absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 group-hover/input:text-cyan-500 transition-colors pointer-events-none">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -152,7 +152,7 @@
|
||||
<template x-if="!mediaPreview">
|
||||
<div @click="$refs.fileInput.click()" class="aspect-video rounded-3xl border-2 border-dashed border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 flex flex-col items-center justify-center gap-4 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800/80 hover:border-cyan-500/50 transition-all duration-300 group">
|
||||
<div class="p-4 rounded-2xl bg-white dark:bg-slate-800 shadow-sm border border-slate-100 dark:border-slate-700 group-hover:scale-110 group-hover:text-cyan-500 transition-all duration-300">
|
||||
<svg class="size-8 text-slate-400 dark:text-slate-500 group-hover:text-cyan-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /></svg>
|
||||
<svg class="w-8 h-8 text-slate-400 dark:text-slate-500 group-hover:text-cyan-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /></svg>
|
||||
</div>
|
||||
<div class="text-center px-4">
|
||||
<p class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-tighter">{{ __('Click to upload') }}</p>
|
||||
@ -171,7 +171,7 @@
|
||||
<video :src="mediaPreview" class="max-w-full max-h-full object-contain" muted></video>
|
||||
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<div class="p-4 rounded-full bg-block/20 backdrop-blur-sm border border-white/10">
|
||||
<svg class="size-8 text-white/50" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5.14v14l11-7-11-7z"/></svg>
|
||||
<svg class="w-8 h-8 text-white/50" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5.14v14l11-7-11-7z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -179,13 +179,13 @@
|
||||
|
||||
<div class="absolute inset-0 bg-slate-950/40 opacity-0 group-hover/media:opacity-100 transition-opacity duration-300 flex items-center justify-center gap-3">
|
||||
<button type="button" @click="$refs.fileInput.click()" class="p-3 rounded-2xl bg-white text-slate-800 hover:bg-cyan-500 hover:text-white transition-all duration-300 shadow-lg">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" /></svg>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" /></svg>
|
||||
</button>
|
||||
<button type="button" @click="openPreview(adFormMode === 'edit' ? (fileName ? {url: mediaPreview, type: adForm.type, name: fileName} : adForm) : {url: mediaPreview, type: adForm.type, name: fileName})" class="p-3 rounded-2xl bg-white text-slate-800 hover:bg-emerald-500 hover:text-white transition-all duration-300 shadow-lg">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /></svg>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /></svg>
|
||||
</button>
|
||||
<button type="button" @click="removeMedia" class="p-3 rounded-2xl bg-white text-slate-800 hover:bg-rose-500 hover:text-white transition-all duration-300 shadow-lg" x-show="mediaPreview">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 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-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 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>
|
||||
</div>
|
||||
</div>
|
||||
@ -212,7 +212,7 @@
|
||||
{{ __('Cancel') }}
|
||||
</button>
|
||||
<button type="submit" class="btn-luxury-primary px-10 py-3">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.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="m4.5 12.75 6 6 9-13.5" /></svg>
|
||||
<span>{{ __('Save Material') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">{{ __('Select a material to play on this machine') }}</p>
|
||||
</div>
|
||||
<button @click="isAssignModalOpen = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
||||
<svg class="size-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>
|
||||
<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>
|
||||
|
||||
|
||||
@ -55,8 +55,29 @@
|
||||
this.imagePreviews[index] = null;
|
||||
this.removeImages[index] = true;
|
||||
if (this.$refs['imageInput_' + index]) this.$refs['imageInput_' + index].value = '';
|
||||
},
|
||||
submitForm() {
|
||||
if (!this.$el.name.value.trim()) {
|
||||
window.dispatchEvent(new CustomEvent('toast', {
|
||||
detail: { message: '{{ __("Enter machine name") }}', type: 'error' }
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (!this.$el.serial_no.value.trim()) {
|
||||
window.dispatchEvent(new CustomEvent('toast', {
|
||||
detail: { message: '{{ __("Enter serial number") }}', type: 'error' }
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (!this.$el.machine_model_id.value.trim()) {
|
||||
window.dispatchEvent(new CustomEvent('toast', {
|
||||
detail: { message: '{{ __("Please select a machine model") }}', type: 'error' }
|
||||
}));
|
||||
return;
|
||||
}
|
||||
this.$el.submit();
|
||||
}
|
||||
}" action="{{ route('admin.basic-settings.machines.update', $machine) }}" method="POST" enctype="multipart/form-data" class="space-y-6">
|
||||
}" @submit.prevent="submitForm" action="{{ route('admin.basic-settings.machines.update', $machine) }}" method="POST" enctype="multipart/form-data" class="space-y-6">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
@ -99,22 +120,21 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6">
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-400 uppercase tracking-[0.15em] mb-2">{{ __('Machine Name') }} <span class="text-rose-500">*</span></label>
|
||||
<input type="text" name="name" value="{{ old('name', $machine->name) }}" class="luxury-input w-full" required>
|
||||
<input type="text" name="name" value="{{ old('name', $machine->name) }}" class="luxury-input w-full">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-400 uppercase tracking-[0.15em] mb-2">{{ __('Machine Serial No') }} <span class="text-rose-500">*</span></label>
|
||||
<input type="text" name="serial_no" value="{{ old('serial_no', $machine->serial_no) }}" class="luxury-input w-full" required>
|
||||
<input type="text" name="serial_no" value="{{ old('serial_no', $machine->serial_no) }}" class="luxury-input w-full">
|
||||
</div>
|
||||
<div>
|
||||
<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') }}</label>
|
||||
<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
|
||||
name="machine_model_id"
|
||||
:selected="old('machine_model_id', $machine->machine_model_id)"
|
||||
required
|
||||
>
|
||||
@foreach($models as $model)
|
||||
<option value="{{ $model->id }}" {{ old('machine_model_id', $machine->machine_model_id) == $model->id ? 'selected' : '' }}>{{ $model->name }}</option>
|
||||
@ -241,7 +261,7 @@
|
||||
<span>{{ __('Photo Slot') }} {{ $i + 1 }}</span>
|
||||
<template x-if="imagePreviews[{{ $i }}]">
|
||||
<span class="text-emerald-500 flex items-center gap-1 lowercase tracking-normal font-bold">
|
||||
<svg class="size-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="m4.5 12.75 6 6 9-13.5"/></svg>
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="m4.5 12.75 6 6 9-13.5"/></svg>
|
||||
{{ __('set') }}
|
||||
</span>
|
||||
</template>
|
||||
@ -253,7 +273,7 @@
|
||||
<template x-if="!imagePreviews[{{ $i }}]">
|
||||
<div @click="$refs.imageInput_{{ $i }}.click()" class="aspect-video rounded-2xl border-2 border-dashed border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 flex flex-col items-center justify-center gap-3 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800/80 hover:border-cyan-500/50 transition-all duration-300 group/upload">
|
||||
<div class="p-2.5 rounded-xl bg-white dark:bg-slate-800 shadow-sm border border-slate-100 dark:border-slate-700 group-hover/upload:scale-110 group-hover/upload:text-cyan-500 transition-all duration-300 text-slate-400">
|
||||
<svg class="size-5" 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-5 h-5" 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>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<p class="text-[10px] font-black text-slate-500 dark:text-slate-400 uppercase tracking-tighter">{{ __('Click to upload') }}</p>
|
||||
@ -266,10 +286,10 @@
|
||||
<img :src="imagePreviews[{{ $i }}]" class="w-full h-full object-cover">
|
||||
<div class="absolute inset-0 bg-slate-950/40 opacity-0 group-hover/image:opacity-100 transition-opacity duration-300 flex items-center justify-center gap-3">
|
||||
<button type="button" @click="$refs.imageInput_{{ $i }}.click()" class="p-2.5 rounded-xl bg-white text-slate-800 hover:bg-cyan-500 hover:text-white transition-all duration-300 shadow-lg">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" /></svg>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" /></svg>
|
||||
</button>
|
||||
<button type="button" @click="removeImage({{ $i }})" class="p-2.5 rounded-xl bg-white text-slate-800 hover:bg-rose-500 hover:text-white transition-all duration-300 shadow-lg">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 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="3"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -184,10 +184,8 @@
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message: data.message, type: 'success' } }));
|
||||
this.showPermissionModal = false;
|
||||
// SPA 模式:重新載入 permissions Tab 內容
|
||||
setTimeout(() => this.searchInTab('permissions'), 300);
|
||||
window.location.href = '{{ route('admin.basic-settings.machines.index', ['tab' => 'permissions']) }}';
|
||||
} else {
|
||||
throw new Error(data.error || 'Update failed');
|
||||
}
|
||||
@ -419,7 +417,13 @@
|
||||
</button>
|
||||
</div>
|
||||
<form action="{{ route('admin.basic-settings.machines.store') }}" method="POST"
|
||||
enctype="multipart/form-data">
|
||||
enctype="multipart/form-data"
|
||||
@submit.prevent="
|
||||
if(!$el.name.value.trim()){ window.dispatchEvent(new CustomEvent('toast', { detail: { message: '{{ __('Enter machine name') }}', type: 'error' } })); return; }
|
||||
if(!$el.serial_no.value.trim()){ window.dispatchEvent(new CustomEvent('toast', { detail: { message: '{{ __('Enter serial number') }}', type: 'error' } })); return; }
|
||||
if(!$el.machine_model_id.value.trim()){ window.dispatchEvent(new CustomEvent('toast', { detail: { message: '{{ __('Please select a machine model') }}', type: 'error' } })); return; }
|
||||
$el.submit()
|
||||
">
|
||||
@csrf
|
||||
<div class="px-8 py-8 space-y-6">
|
||||
<div>
|
||||
@ -427,7 +431,7 @@
|
||||
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">
|
||||
{{ __('Machine Name') }} <span class="text-rose-500">*</span>
|
||||
</label>
|
||||
<input type="text" name="name" required class="luxury-input w-full"
|
||||
<input type="text" name="name" class="luxury-input w-full"
|
||||
placeholder="{{ __('Enter machine name') }}">
|
||||
</div>
|
||||
<div>
|
||||
@ -435,7 +439,7 @@
|
||||
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">
|
||||
{{ __('Serial No') }} <span class="text-rose-500">*</span>
|
||||
</label>
|
||||
<input type="text" name="serial_no" required class="luxury-input w-full"
|
||||
<input type="text" name="serial_no" class="luxury-input w-full"
|
||||
placeholder="{{ __('Enter serial number') }}">
|
||||
</div>
|
||||
<div class="relative z-20">
|
||||
@ -464,7 +468,7 @@
|
||||
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">
|
||||
{{ __('Model') }} <span class="text-rose-500">*</span>
|
||||
</label>
|
||||
<x-searchable-select name="machine_model_id" required :placeholder="__('Select Model')">
|
||||
<x-searchable-select name="machine_model_id" :placeholder="__('Select Model')">
|
||||
@foreach($models as $model)
|
||||
<option value="{{ $model->id }}">{{ $model->name }}</option>
|
||||
@endforeach
|
||||
@ -1023,7 +1027,7 @@
|
||||
</div>
|
||||
<button @click='showPermissionModal = false'
|
||||
class='text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors bg-slate-50 dark:bg-slate-800 p-2 rounded-xl'>
|
||||
<svg class='size-6' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
|
||||
<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>
|
||||
@ -1034,7 +1038,7 @@
|
||||
<div class='mb-6 flex flex-col md:flex-row gap-4 items-center'>
|
||||
<div class='flex-1 relative group w-full text-left'>
|
||||
<span class='absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10'>
|
||||
<svg class='size-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors'
|
||||
<svg class='w-4 h-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>
|
||||
@ -1047,7 +1051,7 @@
|
||||
</div>
|
||||
<button @click="toggleSelectAll()"
|
||||
class="shrink-0 flex items-center gap-2 px-6 py-3 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-cyan-500 hover:text-white transition-all duration-300 border border-slate-200 dark:border-slate-700 font-black text-xs uppercase tracking-widest shadow-sm">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
</svg>
|
||||
@ -1078,7 +1082,7 @@
|
||||
class='p-4 rounded-2xl border-2 cursor-pointer transition-all duration-300 group relative overflow-hidden'>
|
||||
<div class='flex flex-col relative z-10 text-left'>
|
||||
<div class='flex items-center gap-2'>
|
||||
<div class='size-2 rounded-full'
|
||||
<div class='w-2 h-2 rounded-full'
|
||||
:class='permissions[machine.id] ? "bg-cyan-500 animate-pulse" : "bg-slate-300 dark:bg-slate-700"'>
|
||||
</div>
|
||||
<span class='text-sm font-extrabold truncate drop-shadow-sm'
|
||||
@ -1091,7 +1095,7 @@
|
||||
</div>
|
||||
<div
|
||||
class='absolute -right-2 -bottom-2 opacity-[0.03] text-slate-900 dark:text-white pointer-events-none group-hover:scale-110 transition-transform duration-700'>
|
||||
<svg class='size-20' fill='currentColor' viewBox='0 0 24 24'>
|
||||
<svg class='w-20 h-20' fill='currentColor' viewBox='0 0 24 24'>
|
||||
<path
|
||||
d='M5 2h14c1.1 0 2 .9 2 2v16c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2zm0 2v16h14V4H5zm3 3h8v6H8V7zm0 8h3v2H8v-2zm5 0h3v2h-3v-2z' />
|
||||
</svg>
|
||||
@ -1099,8 +1103,8 @@
|
||||
<div class='absolute top-4 right-4 animate-luxury-in'
|
||||
x-show='permissions[machine.id]'>
|
||||
<div
|
||||
class='size-5 rounded-full bg-cyan-500 flex items-center justify-center shadow-lg shadow-cyan-500/30'>
|
||||
<svg class='size-3 text-white' fill='none' stroke='currentColor'
|
||||
class='w-5 h-5 rounded-full bg-cyan-500 flex items-center justify-center shadow-lg shadow-cyan-500/30'>
|
||||
<svg class='w-3 h-3 text-white' fill='none' stroke='currentColor'
|
||||
viewBox='0 0 24 24'>
|
||||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='3'
|
||||
d='M5 13l4 4L19 7' />
|
||||
@ -1119,8 +1123,8 @@
|
||||
<template x-for='i in Math.min(3, Object.values(permissions).filter(v => v).length)'
|
||||
:key='i'>
|
||||
<div
|
||||
class='size-6 rounded-full border-2 border-white dark:border-slate-900 bg-cyan-500 flex items-center justify-center shadow-sm'>
|
||||
<svg class='size-3 text-white' fill='currentColor' viewBox='0 0 24 24'>
|
||||
class='w-6 h-6 rounded-full border-2 border-white dark:border-slate-900 bg-cyan-500 flex items-center justify-center shadow-sm'>
|
||||
<svg class='w-3 h-3 text-white' fill='currentColor' viewBox='0 0 24 24'>
|
||||
<path
|
||||
d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14.5v-9l6 4.5-6 4.5z' />
|
||||
</svg>
|
||||
|
||||
@ -54,7 +54,7 @@
|
||||
<div class="flex items-center gap-4 text-left">
|
||||
<div
|
||||
class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white group-hover:border-cyan-500 shadow-sm group-hover:shadow-cyan-500/50 transition-all duration-300">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
||||
</svg>
|
||||
@ -80,11 +80,9 @@
|
||||
class="flex flex-wrap gap-2 justify-center lg:justify-start max-w-[420px] mx-auto lg:mx-0 max-h-[140px] overflow-y-auto pr-2 custom-scrollbar py-1 text-left">
|
||||
@forelse($user->machines as $m)
|
||||
<div
|
||||
class="flex flex-col px-3 py-1.5 rounded-xl bg-slate-50 dark:bg-slate-800/40 border border-slate-100 dark:border-white/5 hover:border-cyan-500/30 transition-all duration-300 text-left">
|
||||
<span class="text-[11px] font-black text-slate-700 dark:text-slate-200 leading-tight">{{
|
||||
class="px-3 py-1.5 rounded-xl bg-slate-50 dark:bg-slate-800/40 border border-slate-100 dark:border-white/5 hover:border-cyan-500/30 transition-all duration-300 text-left">
|
||||
<span class="text-xs font-black text-slate-700 dark:text-slate-200 leading-tight">{{
|
||||
$m->name }}</span>
|
||||
<span class="text-[9px] font-mono font-bold text-cyan-500 tracking-tighter mt-0.5 opacity-80">{{
|
||||
$m->serial_no }}</span>
|
||||
</div>
|
||||
@empty
|
||||
<div class="w-full text-center lg:text-left">
|
||||
@ -111,7 +109,7 @@
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-24 text-center">
|
||||
<div class="flex flex-col items-center gap-3 opacity-20">
|
||||
<svg class="size-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-16 h-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2m16-10a4 4 0 11-8 0 4 4 0 018 0zM23 21v-2a4 4 0 00-3-3.87m-4-12a4 4 0 010 7.75" />
|
||||
</svg>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="{{ route('admin.basic-settings.payment-configs.index') }}" class="p-2.5 rounded-xl bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 text-slate-500 hover:text-cyan-500 transition-all shadow-sm">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18"/></svg>
|
||||
<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="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18"/></svg>
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="text-2xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Create Payment Config') }}</h1>
|
||||
@ -15,19 +15,41 @@
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button type="submit" form="create-form" class="btn-luxury-primary px-8 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="m4.5 12.75 6 6 9-13.5"/></svg>
|
||||
<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="m4.5 12.75 6 6 9-13.5"/></svg>
|
||||
<span>{{ __('Save Config') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="create-form" action="{{ route('admin.basic-settings.payment-configs.store') }}" method="POST" class="space-y-6">
|
||||
<form id="create-form"
|
||||
x-data="{
|
||||
submitForm() {
|
||||
if (!this.$el.name.value.trim()) {
|
||||
window.dispatchEvent(new CustomEvent('toast', {
|
||||
detail: { message: '{{ __("Please enter configuration name") }}', type: 'error' }
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (!this.$el.company_id.value.trim()) {
|
||||
window.dispatchEvent(new CustomEvent('toast', {
|
||||
detail: { message: '{{ __("Please select a company") }}', type: 'error' }
|
||||
}));
|
||||
return;
|
||||
}
|
||||
this.$el.submit();
|
||||
}
|
||||
}"
|
||||
@submit.prevent="submitForm"
|
||||
action="{{ route('admin.basic-settings.payment-configs.store') }}"
|
||||
method="POST"
|
||||
class="space-y-6"
|
||||
novalidate>
|
||||
@csrf
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="p-5 bg-rose-500/10 border border-rose-500/20 text-rose-600 dark:text-rose-400 rounded-2xl flex items-start gap-4 animate-luxury-in font-bold">
|
||||
<div class="size-8 bg-rose-500/20 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
|
||||
<div class="w-8 h-8 bg-rose-500/20 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm tracking-wide">{{ __('Please check the following errors:') }}</p>
|
||||
@ -58,7 +80,11 @@
|
||||
:placeholder="__('Select Company')"
|
||||
>
|
||||
@foreach($companies as $company)
|
||||
<option value="{{ $company->id }}" data-title="{{ $company->name }}{{ $company->code ? ' ('.$company->code.')' : '' }}">{{ $company->name }}{{ $company->code ? ' ('.$company->code.')' : '' }}</option>
|
||||
<option value="{{ $company->id }}"
|
||||
{{ old('company_id') == $company->id ? 'selected' : '' }}
|
||||
data-title="{{ $company->name }}{{ $company->code ? ' ('.$company->code.')' : '' }}">
|
||||
{{ $company->name }}{{ $company->code ? ' ('.$company->code.')' : '' }}
|
||||
</option>
|
||||
@endforeach
|
||||
</x-searchable-select>
|
||||
</div>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="{{ route('admin.basic-settings.payment-configs.index') }}" class="p-2.5 rounded-xl bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 text-slate-500 hover:text-cyan-500 transition-all shadow-sm">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18"/></svg>
|
||||
<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="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18"/></svg>
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="text-2xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Edit Payment Config') }}</h1>
|
||||
@ -15,20 +15,42 @@
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button type="submit" form="edit-form" class="btn-luxury-primary px-8 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="m4.5 12.75 6 6 9-13.5"/></svg>
|
||||
<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="m4.5 12.75 6 6 9-13.5"/></svg>
|
||||
<span>{{ __('Save Changes') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="edit-form" action="{{ route('admin.basic-settings.payment-configs.update', $paymentConfig) }}" method="POST" class="space-y-6">
|
||||
<form id="edit-form"
|
||||
x-data="{
|
||||
submitForm() {
|
||||
if (!this.$el.name.value.trim()) {
|
||||
window.dispatchEvent(new CustomEvent('toast', {
|
||||
detail: { message: '{{ __("Please enter configuration name") }}', type: 'error' }
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (!this.$el.company_id.value.trim()) {
|
||||
window.dispatchEvent(new CustomEvent('toast', {
|
||||
detail: { message: '{{ __("Please select a company") }}', type: 'error' }
|
||||
}));
|
||||
return;
|
||||
}
|
||||
this.$el.submit();
|
||||
}
|
||||
}"
|
||||
@submit.prevent="submitForm"
|
||||
action="{{ route('admin.basic-settings.payment-configs.update', $paymentConfig) }}"
|
||||
method="POST"
|
||||
class="space-y-6"
|
||||
novalidate>
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="p-5 bg-rose-500/10 border border-rose-500/20 text-rose-600 dark:text-rose-400 rounded-2xl flex items-start gap-4 animate-luxury-in font-bold">
|
||||
<div class="size-8 bg-rose-500/20 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
|
||||
<div class="w-8 h-8 bg-rose-500/20 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm tracking-wide">{{ __('Please check the following errors:') }}</p>
|
||||
@ -59,7 +81,11 @@
|
||||
:placeholder="__('Select Company')"
|
||||
>
|
||||
@foreach($companies as $company)
|
||||
<option value="{{ $company->id }}" data-title="{{ $company->name }}{{ $company->code ? ' ('.$company->code.')' : '' }}">{{ $company->name }}{{ $company->code ? ' ('.$company->code.')' : '' }}</option>
|
||||
<option value="{{ $company->id }}"
|
||||
{{ old('company_id', $paymentConfig->company_id) == $company->id ? 'selected' : '' }}
|
||||
data-title="{{ $company->name }}{{ $company->code ? ' ('.$company->code.')' : '' }}">
|
||||
{{ $company->name }}{{ $company->code ? ' ('.$company->code.')' : '' }}
|
||||
</option>
|
||||
@endforeach
|
||||
</x-searchable-select>
|
||||
</div>
|
||||
|
||||
@ -7,6 +7,44 @@
|
||||
confirmDelete(action) {
|
||||
this.deleteFormAction = action;
|
||||
this.isDeleteConfirmOpen = true;
|
||||
},
|
||||
isLoadingTable: false,
|
||||
async fetchPage(url) {
|
||||
if (!url || this.isLoadingTable) return;
|
||||
|
||||
this.isLoadingTable = true;
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
if (newContent) {
|
||||
document.querySelector('#ajax-content-container').innerHTML = newContent.innerHTML;
|
||||
history.pushState(null, '', url);
|
||||
if (window.HSStaticMethods && window.HSStaticMethods.autoInit) {
|
||||
window.HSStaticMethods.autoInit();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
window.location.href = url;
|
||||
} finally {
|
||||
this.isLoadingTable = false;
|
||||
}
|
||||
},
|
||||
init() {
|
||||
window.addEventListener('popstate', () => {
|
||||
this.fetchPage(window.location.href);
|
||||
});
|
||||
}
|
||||
}">
|
||||
<!-- Header -->
|
||||
@ -17,31 +55,65 @@
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{ route('admin.basic-settings.payment-configs.create') }}" 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"/></svg>
|
||||
<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="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18"/></svg>
|
||||
<span>{{ __('Create Config') }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Card -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
<!-- Toolbar & Filters -->
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<form method="GET" action="{{ route('admin.basic-settings.payment-configs.index') }}" 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" 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>
|
||||
<!-- Content Container with Loading State -->
|
||||
<div id="ajax-content-container"
|
||||
class="relative transition-all duration-500 min-h-[400px]"
|
||||
:class="isLoadingTable ? 'opacity-40 blur-[2px] pointer-events-none' : 'opacity-100 blur-0'"
|
||||
@ajax:navigate.prevent.stop="fetchPage($event.detail.url)"
|
||||
@click="if ($event.target.closest('a') && $event.target.closest('a').href && ($event.target.closest('a').href.includes('page=') || $event.target.closest('a').href.includes('per_page='))) { $event.preventDefault(); fetchPage($event.target.closest('a').href); }">
|
||||
|
||||
<!-- Loading Spinner Overlay -->
|
||||
<div x-show="isLoadingTable"
|
||||
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-300"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="absolute inset-0 z-50 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 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" 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>
|
||||
</span>
|
||||
<input type="text" name="search" value="{{ request('search') }}"
|
||||
placeholder="{{ __('Search configurations...') }}"
|
||||
class="luxury-input py-2.5 pl-12 pr-6 block w-64">
|
||||
</form>
|
||||
</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>
|
||||
|
||||
<!-- Main Card -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
<!-- Toolbar & Filters -->
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<form method="GET" action="{{ route('admin.basic-settings.payment-configs.index') }}"
|
||||
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 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 configurations...') }}"
|
||||
class="luxury-input py-2.5 pl-12 pr-6 block w-64">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-separate border-spacing-y-0">
|
||||
<thead>
|
||||
@ -58,7 +130,7 @@
|
||||
<td class="px-6 py-6 font-extrabold text-slate-800 dark:text-slate-100">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300">
|
||||
<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="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/></svg>
|
||||
<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="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div 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 whitespace-nowrap">{{ $config->name }}</div>
|
||||
@ -79,7 +151,7 @@
|
||||
<td class="px-6 py-6 text-right space-x-2">
|
||||
<a href="{{ route('admin.basic-settings.payment-configs.edit', $config) }}"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 border border-transparent hover:border-cyan-500/20 transition-all inline-flex" 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 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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>
|
||||
</a>
|
||||
<form action="{{ route('admin.basic-settings.payment-configs.destroy', $config) }}" method="POST" class="inline-block" @submit.prevent="confirmDelete($el.getAttribute('action'))">
|
||||
@csrf
|
||||
@ -87,7 +159,7 @@
|
||||
<button type="submit"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 hover:bg-rose-500/5 border border-transparent hover:border-rose-500/20 transition-all"
|
||||
title="{{ __('Delete') }}">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 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>
|
||||
</form>
|
||||
</td>
|
||||
@ -110,6 +182,7 @@
|
||||
{{ $paymentConfigs->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Global Delete Confirm Modal -->
|
||||
<x-delete-confirm-modal :message="__('Are you sure you want to delete this configuration? This action cannot be undone.')" />
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-6" x-data="{
|
||||
|
||||
<div class="space-y-2" x-data="{
|
||||
showModal: false,
|
||||
showHistoryModal: false,
|
||||
showSettingsModal: false,
|
||||
editing: false,
|
||||
sidebarView: 'detail',
|
||||
activeTab: '{{ request('tab', 'list') }}',
|
||||
currentCompany: {
|
||||
id: '',
|
||||
name: '',
|
||||
@ -114,6 +115,23 @@
|
||||
};
|
||||
this.showHistoryModal = true;
|
||||
},
|
||||
openSettingsModal(company) {
|
||||
this.currentCompany = {
|
||||
...company,
|
||||
settings: {
|
||||
enable_material_code: company.settings?.enable_material_code === true || company.settings?.enable_material_code === 1 || company.settings?.enable_material_code === '1',
|
||||
enable_points: company.settings?.enable_points === true || company.settings?.enable_points === 1 || company.settings?.enable_points === '1',
|
||||
tax_invoice: company.settings?.tax_invoice === true || company.settings?.tax_invoice === 1 || company.settings?.tax_invoice === '1',
|
||||
card_terminal: company.settings?.card_terminal === true || company.settings?.card_terminal === 1 || company.settings?.card_terminal === '1',
|
||||
scan_pay_esun: company.settings?.scan_pay_esun === true || company.settings?.scan_pay_esun === 1 || company.settings?.scan_pay_esun === '1',
|
||||
scan_pay_linepay: company.settings?.scan_pay_linepay === true || company.settings?.scan_pay_linepay === 1 || company.settings?.scan_pay_linepay === '1',
|
||||
shopping_cart: company.settings?.shopping_cart === true || company.settings?.shopping_cart === 1 || company.settings?.shopping_cart === '1',
|
||||
welcome_gift: company.settings?.welcome_gift === true || company.settings?.welcome_gift === 1 || company.settings?.welcome_gift === '1',
|
||||
cash_module: company.settings?.cash_module === true || company.settings?.cash_module === 1 || company.settings?.cash_module === '1',
|
||||
}
|
||||
};
|
||||
this.showSettingsModal = true;
|
||||
},
|
||||
submitConfirmedForm() {
|
||||
if (this.statusToggleSource === 'list') {
|
||||
this.$refs.statusToggleForm.submit();
|
||||
@ -121,22 +139,107 @@
|
||||
this.$refs.companyForm.submit();
|
||||
}
|
||||
},
|
||||
init() {
|
||||
this.$watch('activeTab', function(val) {
|
||||
history.pushState({}, '', '{{ route('admin.permission.companies.index') }}?tab=' + val);
|
||||
});
|
||||
|
||||
// Listen to popstate to handle browser back/forward correctly
|
||||
window.addEventListener('popstate', () => {
|
||||
if (window.location.search) {
|
||||
this.fetchPage(window.location.href, false);
|
||||
}
|
||||
});
|
||||
},
|
||||
isLoadingTable: false,
|
||||
async fetchPage(url, pushState = true) {
|
||||
this.isLoadingTable = true;
|
||||
try {
|
||||
const response = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||
if (response.ok) {
|
||||
const html = await response.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const newContainer = doc.querySelector('#ajax-content-container');
|
||||
if (newContainer) {
|
||||
document.querySelector('#ajax-content-container').innerHTML = newContainer.innerHTML;
|
||||
if (pushState) {
|
||||
history.pushState({}, '', url);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('AJAX fetch error:', error);
|
||||
window.location.href = url;
|
||||
} finally {
|
||||
this.isLoadingTable = false;
|
||||
}
|
||||
}
|
||||
}">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||
<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">{{ __('Customer Management') }}</h1>
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Manage all tenant accounts and validity') }}</p>
|
||||
<h1 class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display transition-all duration-300">
|
||||
{{ __('Customer List') }}
|
||||
</h1>
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">
|
||||
{{ __('Manage all tenant accounts and validity') }}
|
||||
</p>
|
||||
</div>
|
||||
<button @click="openCreateModal()" class="btn-luxury-primary">
|
||||
<svg class="size-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 class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14M5 12h14" />
|
||||
</svg>
|
||||
<span>{{ __('Add Customer') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<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">
|
||||
<button @click="activeTab = 'list'"
|
||||
:class="activeTab === 'list' ? '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">
|
||||
{{ __('Customer List') }}
|
||||
</button>
|
||||
<button @click="activeTab = 'settings'"
|
||||
:class="activeTab === 'settings' ? '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">
|
||||
{{ __('System Settings') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<!-- Loading Spinner Overlay -->
|
||||
<div x-show="isLoadingTable"
|
||||
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-300"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="absolute inset-0 z-50 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" 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="ajax-content-container"
|
||||
:class="{ 'opacity-30 pointer-events-none transition-opacity duration-300': isLoadingTable }"
|
||||
@ajax:navigate.prevent.stop="fetchPage($event.detail.url)"
|
||||
@click="if ($event.target.closest('a') && $event.target.closest('a').href && ($event.target.closest('a').href.includes('page=') || $event.target.closest('a').href.includes('tab='))) { $event.preventDefault(); fetchPage($event.target.closest('a').href); }">
|
||||
<!-- Customer List Tab -->
|
||||
<div x-show="activeTab === 'list'" x-cloak class="space-y-3">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="luxury-card p-6 rounded-2xl animate-luxury-in">
|
||||
<p class="text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Total Customers') }}</p>
|
||||
<p class="text-3xl font-black text-slate-800 dark:text-white mt-2">{{ $companies->total() }}</p>
|
||||
@ -156,10 +259,10 @@
|
||||
<!-- Table Section -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 300ms">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-6">
|
||||
<form action="{{ route('admin.permission.companies.index') }}" method="GET" class="relative group">
|
||||
<form action="{{ route('admin.permission.companies.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"
|
||||
<svg class="w-4 h-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>
|
||||
@ -174,15 +277,15 @@
|
||||
|
||||
<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.permission.companies.index') }}"
|
||||
<a href="{{ route('admin.permission.companies.index') }}" @click.prevent="fetchPage($event.currentTarget.href)"
|
||||
class="px-5 py-2 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ !request()->filled('status') ? 'bg-white dark:bg-slate-800 text-cyan-600 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.permission.companies.index', ['status' => 1]) }}"
|
||||
<a href="{{ route('admin.permission.companies.index', ['status' => 1]) }}" @click.prevent="fetchPage($event.currentTarget.href)"
|
||||
class="px-5 py-2 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ request('status') === '1' ? 'bg-white dark:bg-emerald-500/10 text-emerald-500 shadow-lg shadow-emerald-500/10' : 'text-slate-400 dark:text-slate-500 hover:text-emerald-500/80' }}">
|
||||
{{ __('Active') }}
|
||||
</a>
|
||||
<a href="{{ route('admin.permission.companies.index', ['status' => 0]) }}"
|
||||
<a href="{{ route('admin.permission.companies.index', ['status' => 0]) }}" @click.prevent="fetchPage($event.currentTarget.href)"
|
||||
class="px-5 py-2 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ request('status') === '0' ? 'bg-white dark:bg-rose-500/10 text-rose-500 shadow-lg shadow-rose-500/10' : 'text-slate-400 dark:text-slate-500 hover:text-rose-500/80' }}">
|
||||
{{ __('Disabled') }}
|
||||
</a>
|
||||
@ -220,8 +323,7 @@
|
||||
<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="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
stroke-width="2.5">
|
||||
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
@ -334,7 +436,7 @@
|
||||
@click="toggleFormAction = '{{ route('admin.permission.companies.status.toggle', $company->id) }}'; statusToggleSource = 'list'; isStatusConfirmOpen = true"
|
||||
class="p-2 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="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<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="M15.75 5.25v13.5m-7.5-13.5v13.5" />
|
||||
</svg>
|
||||
</button>
|
||||
@ -343,32 +445,30 @@
|
||||
@click="toggleFormAction = '{{ route('admin.permission.companies.status.toggle', $company->id) }}'; $nextTick(() => $refs.statusToggleForm.submit())"
|
||||
class="p-2 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="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<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.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="statusToggleSource = 'edit'; openEditModal({{ json_encode($company) }})"
|
||||
class="p-2 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">
|
||||
<svg class="size-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" />
|
||||
<button @click='statusToggleSource = "edit"; openEditModal({{ json_encode($company) }})'
|
||||
class="p-2 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 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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.permission.companies.destroy', $company->id) }}'; isDeleteConfirmOpen = true"
|
||||
class="p-2 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">
|
||||
<svg class="size-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" />
|
||||
class="p-2 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 Customer') }}">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 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="openDetailSidebar({{ json_encode($company) }})"
|
||||
<button type="button" @click='openDetailSidebar({{ json_encode($company) }})'
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-indigo-500 hover:bg-indigo-500/5 transition-all border border-transparent hover:border-indigo-500/20"
|
||||
title="{{ __('View Details') }}">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<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-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
@ -382,7 +482,7 @@
|
||||
<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="size-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
@ -399,6 +499,123 @@
|
||||
<div class="mt-8 border-t border-slate-100 dark:border-slate-800 pt-6">
|
||||
{{ $companies->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Settings Tab -->
|
||||
<div x-show="activeTab === 'settings'" x-cloak class="space-y-3">
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-6">
|
||||
<form action="{{ route('admin.permission.companies.index') }}" method="GET" class="relative group" @submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())">
|
||||
<input type="hidden" name="tab" value="settings">
|
||||
<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') }}" class="py-2.5 pl-12 pr-6 block w-64 luxury-input" placeholder="{{ __('Search customers...') }}">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-separate border-spacing-y-0">
|
||||
<thead>
|
||||
<tr class="bg-slate-50/50 dark:bg-slate-900/30">
|
||||
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">{{ __('Customer Info') }}</th>
|
||||
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">{{ __('Enabled Features') }}</th>
|
||||
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest 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/50">
|
||||
@forelse($companies as $company)
|
||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||
<td class="px-6 py-6 w-1/3">
|
||||
<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 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
<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">{{ $company->name }}</span>
|
||||
</div>
|
||||
<span class="text-xs font-mono font-bold text-slate-500 dark:text-slate-400 mt-0.5 tracking-widest uppercase">{{ $company->code }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@php $settings = $company->settings ?? []; @endphp
|
||||
|
||||
@if(empty($settings) || !in_array(true, array_map(fn($v) => (bool)$v, is_array($settings) ? $settings : ($settings->toArray() ?? [])), true))
|
||||
<span class="px-2.5 py-1 rounded-md bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 text-[10px] font-black uppercase tracking-wider">{{ __('None') }}</span>
|
||||
@else
|
||||
@if($settings['enable_material_code'] ?? false)
|
||||
<span class="px-2.5 py-1 rounded-md bg-cyan-500/10 text-cyan-600 border border-cyan-500/20 text-[10px] font-black uppercase tracking-wider">{{ __('Material Code') }}</span>
|
||||
@endif
|
||||
@if($settings['enable_points'] ?? false)
|
||||
<span class="px-2.5 py-1 rounded-md bg-cyan-500/10 text-cyan-600 border border-cyan-500/20 text-[10px] font-black uppercase tracking-wider">{{ __('Points') }}</span>
|
||||
@endif
|
||||
@if($settings['tax_invoice'] ?? false)
|
||||
<span class="px-2.5 py-1 rounded-md bg-emerald-500/10 text-emerald-600 border border-emerald-500/20 text-[10px] font-black uppercase tracking-wider">{{ __('Electronic Invoice') }}</span>
|
||||
@endif
|
||||
@if($settings['card_terminal'] ?? false)
|
||||
<span class="px-2.5 py-1 rounded-md bg-blue-500/10 text-blue-600 border border-blue-500/20 text-[10px] font-black uppercase tracking-wider">{{ __('Card System') }}</span>
|
||||
@endif
|
||||
@if($settings['scan_pay_esun'] ?? false)
|
||||
<span class="px-2.5 py-1 rounded-md bg-green-500/10 text-green-600 border border-green-500/20 text-[10px] font-black uppercase tracking-wider">{{ __('ESUN') }}</span>
|
||||
@endif
|
||||
@if($settings['scan_pay_linepay'] ?? false)
|
||||
<span class="px-2.5 py-1 rounded-md bg-green-500/10 text-green-600 border border-green-500/20 text-[10px] font-black uppercase tracking-wider">{{ __('LinePay') }}</span>
|
||||
@endif
|
||||
@if($settings['shopping_cart'] ?? false)
|
||||
<span class="px-2.5 py-1 rounded-md bg-purple-500/10 text-purple-600 border border-purple-500/20 text-[10px] font-black uppercase tracking-wider">{{ __('Shopping Cart') }}</span>
|
||||
@endif
|
||||
@if($settings['welcome_gift'] ?? false)
|
||||
<span class="px-2.5 py-1 rounded-md bg-rose-500/10 text-rose-600 border border-rose-500/20 text-[10px] font-black uppercase tracking-wider">{{ __('Welcome Gift') }}</span>
|
||||
@endif
|
||||
@if($settings['cash_module'] ?? false)
|
||||
<span class="px-2.5 py-1 rounded-md bg-amber-500/10 text-amber-600 border border-amber-500/20 text-[10px] font-black uppercase tracking-wider">{{ __('Cash Module') }}</span>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-right">
|
||||
<button type="button" @click='openSettingsModal({{ json_encode($company) }})'
|
||||
class="p-2 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 Settings') }}">
|
||||
<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="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75" />
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="3" class="px-6 py-24 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 d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-slate-400 font-bold">{{ __('No customers found') }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 border-t border-slate-100 dark:border-slate-800 pt-6">
|
||||
{{ $companies->appends(['tab' => 'settings', 'search' => request('search')])->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create/Edit Modal (Simplified with standard Blade form for reliability) -->
|
||||
@ -425,8 +642,8 @@
|
||||
x-text="editing ? '{{ __('Edit Customer') }}' : '{{ __('Add Customer') }}'"></h3>
|
||||
<button @click="showModal = false"
|
||||
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
||||
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
@ -597,7 +814,7 @@
|
||||
placeholder="{{ __('Min 8 characters') }}">
|
||||
<button type="button" @click="show = !show"
|
||||
class="absolute inset-y-0 end-0 flex items-center z-20 px-4 cursor-pointer text-slate-400 hover:text-cyan-500 transition-colors">
|
||||
<svg x-show="!show" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<svg x-show="!show" 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-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
@ -675,41 +892,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Feature Toggles Section -->
|
||||
<div class="space-y-6 pt-6 border-t border-slate-100 dark:border-slate-800 relative z-[5]">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="h-6 w-1 bg-cyan-500 rounded-full"></div>
|
||||
<h4 class="text-xs font-black text-slate-800 dark:text-white uppercase tracking-widest">{{ __('Feature Settings') }}</h4>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Material Code Toggle -->
|
||||
<div class="flex items-center justify-between p-4 rounded-2xl bg-slate-50/50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-700/50 transition-all hover:bg-slate-50 dark:hover:bg-slate-800">
|
||||
<div class="space-y-0.5">
|
||||
<label class="text-sm font-black text-slate-700 dark:text-slate-200 uppercase tracking-wide">{{ __('Enable Material Code') }}</label>
|
||||
<p class="text-xs text-slate-400 font-medium">{{ __('Show material code field in products') }}</p>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="hidden" name="settings[enable_material_code]" value="0">
|
||||
<input type="checkbox" name="settings[enable_material_code]" value="1" x-model="currentCompany.settings.enable_material_code" class="peer sr-only">
|
||||
<div class="w-11 h-6 bg-slate-200 peer-focus:outline-none rounded-full peer dark:bg-slate-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-cyan-500"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Points Toggle -->
|
||||
<div class="flex items-center justify-between p-4 rounded-2xl bg-slate-50/50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-700/50 transition-all hover:bg-slate-50 dark:hover:bg-slate-800">
|
||||
<div class="space-y-0.5">
|
||||
<label class="text-sm font-black text-slate-700 dark:text-slate-200 uppercase tracking-wide">{{ __('Enable Points') }}</label>
|
||||
<p class="text-xs text-slate-400 font-medium">{{ __('Show points rules in products') }}</p>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="hidden" name="settings[enable_points]" value="0">
|
||||
<input type="checkbox" name="settings[enable_points]" value="1" x-model="currentCompany.settings.enable_points" class="peer sr-only">
|
||||
<div class="w-11 h-6 bg-slate-200 peer-focus:outline-none rounded-full peer dark:bg-slate-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-cyan-500"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex justify-end gap-x-4 pt-8">
|
||||
@ -723,8 +905,193 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Settings Edit Modal -->
|
||||
<div x-show="showSettingsModal" class="fixed inset-0 z-[100] flex items-center justify-center" x-cloak>
|
||||
<div x-show="showSettingsModal"
|
||||
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/40 backdrop-blur-sm" @click="showSettingsModal = false"></div>
|
||||
|
||||
<div x-show="showSettingsModal"
|
||||
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 bg-white dark:bg-slate-900 rounded-3xl shadow-2xl border border-slate-100 dark:border-slate-800 w-full max-w-3xl mx-4 overflow-hidden z-[101] flex flex-col max-h-[90vh]">
|
||||
|
||||
<div class="px-8 py-6 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center bg-slate-50/50 dark:bg-slate-900/50">
|
||||
<div>
|
||||
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight">{{ __('Edit System Settings') }}</h3>
|
||||
<p class="text-xs font-bold text-slate-400 mt-1" x-text="currentCompany.name"></p>
|
||||
</div>
|
||||
<button type="button" @click="showSettingsModal = false" class="text-slate-400 hover:text-slate-500 bg-white dark:bg-slate-800 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-xl p-2 transition-colors">
|
||||
<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>
|
||||
|
||||
<div class="p-8 overflow-y-auto custom-scrollbar flex-1">
|
||||
<form :action="`{{ route('admin.permission.companies.index') }}/${currentCompany.id}/settings`" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- 商品與系統設定 (Additional Features) -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 items-center p-4 rounded-xl bg-slate-50/50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-700/50 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors">
|
||||
<div class="md:col-span-1">
|
||||
<h4 class="text-sm font-black text-slate-700 dark:text-slate-200">{{ __('Goods & System Settings') }}</h4>
|
||||
</div>
|
||||
<div class="md:col-span-3 flex flex-wrap gap-x-8 gap-y-4">
|
||||
<label class="flex items-center gap-3 cursor-pointer group">
|
||||
<div class="relative inline-flex items-center">
|
||||
<input type="hidden" name="settings[enable_material_code]" value="0">
|
||||
<input type="checkbox" name="settings[enable_material_code]" value="1" x-model="currentCompany.settings.enable_material_code" class="peer sr-only">
|
||||
<div class="w-9 h-5 bg-slate-200 peer-focus:outline-none rounded-full peer dark:bg-slate-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-cyan-500"></div>
|
||||
</div>
|
||||
<span class="text-sm font-bold text-slate-600 dark:text-slate-300 group-hover:text-slate-900 dark:group-hover:text-white transition-colors">{{ __('Display Material Code') }}</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-3 cursor-pointer group">
|
||||
<div class="relative inline-flex items-center">
|
||||
<input type="hidden" name="settings[enable_points]" value="0">
|
||||
<input type="checkbox" name="settings[enable_points]" value="1" x-model="currentCompany.settings.enable_points" class="peer sr-only">
|
||||
<div class="w-9 h-5 bg-slate-200 peer-focus:outline-none rounded-full peer dark:bg-slate-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-cyan-500"></div>
|
||||
</div>
|
||||
<span class="text-sm font-bold text-slate-600 dark:text-slate-300 group-hover:text-slate-900 dark:group-hover:text-white transition-colors">{{ __('Enable Points Mechanism') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 稅務系統 (Taxation System) -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 items-center p-4 rounded-xl bg-slate-50/50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-700/50 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors">
|
||||
<div class="md:col-span-1">
|
||||
<h4 class="text-sm font-black text-slate-700 dark:text-slate-200">{{ __('Taxation System') }}</h4>
|
||||
</div>
|
||||
<div class="md:col-span-3 flex flex-wrap gap-x-8 gap-y-4">
|
||||
<label class="flex items-center gap-3 cursor-pointer group">
|
||||
<div class="relative inline-flex items-center">
|
||||
<input type="hidden" name="settings[tax_invoice]" value="0">
|
||||
<input type="checkbox" name="settings[tax_invoice]" value="1" x-model="currentCompany.settings.tax_invoice" class="peer sr-only">
|
||||
<div class="w-9 h-5 bg-slate-200 peer-focus:outline-none rounded-full peer dark:bg-slate-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-cyan-500"></div>
|
||||
</div>
|
||||
<span class="text-sm font-bold text-slate-600 dark:text-slate-300 group-hover:text-slate-900 dark:group-hover:text-white transition-colors">{{ __('Electronic Invoice') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 刷卡機系統 (Card Terminal System) -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 items-center p-4 rounded-xl bg-slate-50/50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-700/50 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors">
|
||||
<div class="md:col-span-1">
|
||||
<h4 class="text-sm font-black text-slate-700 dark:text-slate-200">{{ __('Card Terminal System') }}</h4>
|
||||
</div>
|
||||
<div class="md:col-span-3 flex flex-wrap gap-x-8 gap-y-4">
|
||||
<label class="flex items-center gap-3 cursor-pointer group">
|
||||
<div class="relative inline-flex items-center">
|
||||
<input type="hidden" name="settings[card_terminal]" value="0">
|
||||
<input type="checkbox" name="settings[card_terminal]" value="1" x-model="currentCompany.settings.card_terminal" class="peer sr-only">
|
||||
<div class="w-9 h-5 bg-slate-200 peer-focus:outline-none rounded-full peer dark:bg-slate-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-cyan-500"></div>
|
||||
</div>
|
||||
<span class="text-sm font-bold text-slate-600 dark:text-slate-300 group-hover:text-slate-900 dark:group-hover:text-white transition-colors">{{ __('Includes Credit Card/Mobile Pay') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 掃碼支付功能 (QR Scan Payment) -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 items-center p-4 rounded-xl bg-slate-50/50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-700/50 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors">
|
||||
<div class="md:col-span-1">
|
||||
<h4 class="text-sm font-black text-slate-700 dark:text-slate-200">{{ __('QR Scan Payment') }}</h4>
|
||||
</div>
|
||||
<div class="md:col-span-3 flex flex-wrap gap-x-8 gap-y-4">
|
||||
<label class="flex items-center gap-3 cursor-pointer group">
|
||||
<div class="relative inline-flex items-center">
|
||||
<input type="hidden" name="settings[scan_pay_esun]" value="0">
|
||||
<input type="checkbox" name="settings[scan_pay_esun]" value="1" x-model="currentCompany.settings.scan_pay_esun" class="peer sr-only">
|
||||
<div class="w-9 h-5 bg-slate-200 peer-focus:outline-none rounded-full peer dark:bg-slate-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-cyan-500"></div>
|
||||
</div>
|
||||
<span class="text-sm font-bold text-slate-600 dark:text-slate-300 group-hover:text-slate-900 dark:group-hover:text-white transition-colors">{{ __('E.SUN QR Pay') }}</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-3 cursor-pointer group">
|
||||
<div class="relative inline-flex items-center">
|
||||
<input type="hidden" name="settings[scan_pay_linepay]" value="0">
|
||||
<input type="checkbox" name="settings[scan_pay_linepay]" value="1" x-model="currentCompany.settings.scan_pay_linepay" class="peer sr-only">
|
||||
<div class="w-9 h-5 bg-slate-200 peer-focus:outline-none rounded-full peer dark:bg-slate-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-cyan-500"></div>
|
||||
</div>
|
||||
<span class="text-sm font-bold text-slate-600 dark:text-slate-300 group-hover:text-slate-900 dark:group-hover:text-white transition-colors">{{ __('LinePay Payment') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 購物車功能 (Shopping Cart Feature) -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 items-center p-4 rounded-xl bg-slate-50/50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-700/50 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors">
|
||||
<div class="md:col-span-1">
|
||||
<h4 class="text-sm font-black text-slate-700 dark:text-slate-200">{{ __('Shopping Cart Feature') }}</h4>
|
||||
</div>
|
||||
<div class="md:col-span-3 flex flex-wrap gap-x-8 gap-y-4">
|
||||
<label class="flex items-center gap-3 cursor-pointer group">
|
||||
<div class="relative inline-flex items-center">
|
||||
<input type="hidden" name="settings[shopping_cart]" value="0">
|
||||
<input type="checkbox" name="settings[shopping_cart]" value="1" x-model="currentCompany.settings.shopping_cart" class="peer sr-only">
|
||||
<div class="w-9 h-5 bg-slate-200 peer-focus:outline-none rounded-full peer dark:bg-slate-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-cyan-500"></div>
|
||||
</div>
|
||||
<span class="text-sm font-bold text-slate-600 dark:text-slate-300 group-hover:text-slate-900 dark:group-hover:text-white transition-colors">{{ __('Shopping Cart') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 來店禮功能 (Welcome Gift Feature) -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 items-center p-4 rounded-xl bg-slate-50/50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-700/50 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors">
|
||||
<div class="md:col-span-1">
|
||||
<h4 class="text-sm font-black text-slate-700 dark:text-slate-200">{{ __('Welcome Gift Feature') }}</h4>
|
||||
</div>
|
||||
<div class="md:col-span-3 flex flex-wrap gap-x-8 gap-y-4">
|
||||
<label class="flex items-center gap-3 cursor-pointer group">
|
||||
<div class="relative inline-flex items-center">
|
||||
<input type="hidden" name="settings[welcome_gift]" value="0">
|
||||
<input type="checkbox" name="settings[welcome_gift]" value="1" x-model="currentCompany.settings.welcome_gift" class="peer sr-only">
|
||||
<div class="w-9 h-5 bg-slate-200 peer-focus:outline-none rounded-full peer dark:bg-slate-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-cyan-500"></div>
|
||||
</div>
|
||||
<span class="text-sm font-bold text-slate-600 dark:text-slate-300 group-hover:text-slate-900 dark:group-hover:text-white transition-colors">{{ __('Welcome Gift') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 現金模組功能 (Cash Module Feature) -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 items-center p-4 rounded-xl bg-slate-50/50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-700/50 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors">
|
||||
<div class="md:col-span-1">
|
||||
<h4 class="text-sm font-black text-slate-700 dark:text-slate-200">{{ __('Cash Module Feature') }}</h4>
|
||||
</div>
|
||||
<div class="md:col-span-3 flex flex-wrap gap-x-8 gap-y-4">
|
||||
<label class="flex items-center gap-3 cursor-pointer group">
|
||||
<div class="relative inline-flex items-center">
|
||||
<input type="hidden" name="settings[cash_module]" value="0">
|
||||
<input type="checkbox" name="settings[cash_module]" value="1" x-model="currentCompany.settings.cash_module" class="peer sr-only">
|
||||
<div class="w-9 h-5 bg-slate-200 peer-focus:outline-none rounded-full peer dark:bg-slate-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-cyan-500"></div>
|
||||
</div>
|
||||
<span class="text-sm font-bold text-slate-600 dark:text-slate-300 group-hover:text-slate-900 dark:group-hover:text-white transition-colors">{{ __('Coin/Bill Acceptor') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-x-4 pt-8 mt-6 border-t border-slate-100 dark:border-slate-800">
|
||||
<button type="button" @click="showSettingsModal = false" class="btn-luxury-ghost px-8">{{ __('Cancel') }}</button>
|
||||
<button type="submit" class="btn-luxury-primary px-12">
|
||||
<span>{{ __('Update Settings') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-delete-confirm-modal :message="__('Are you sure to delete this customer?')" />
|
||||
<x-status-confirm-modal :title="__('停用客戶確認')" :message="__('停用此客戶將會連帶停用該客戶下的所有帳號,確定要繼續嗎?')" />
|
||||
<x-status-confirm-modal :title="__('Disable Customer Confirmation')" :message="__('Disabling this customer will also disable all accounts under this customer. Are you sure you want to continue?')" />
|
||||
|
||||
<form x-ref="statusToggleForm" :action="toggleFormAction" method="POST" class="hidden">
|
||||
@csrf
|
||||
@ -767,7 +1134,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<button @click="showDetail = false" class="p-2 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors text-slate-400">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<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>
|
||||
@ -950,7 +1317,7 @@
|
||||
<!-- Card Header -->
|
||||
<div class="px-5 py-3 bg-white dark:bg-slate-800/50 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="size-7 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center text-[9px] font-black text-slate-500" x-text="'#' + (detailCompany.contracts.length - index)"></div>
|
||||
<div class="w-7 h-7 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center text-[9px] font-black text-slate-500" x-text="'#' + (detailCompany.contracts.length - index)"></div>
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-slate-700 dark:text-slate-200" x-text="new Date(contract.created_at).toLocaleString()"></p>
|
||||
<div class="flex items-center gap-1.5 mt-0.5">
|
||||
@ -1011,8 +1378,8 @@
|
||||
</template>
|
||||
|
||||
<div x-show="!(detailCompany.contracts || []).length" class="py-12 text-center">
|
||||
<div class="size-14 rounded-2xl bg-slate-50 dark:bg-slate-800/50 flex items-center justify-center mx-auto mb-3 border border-slate-100 dark:border-slate-800">
|
||||
<svg class="size-7 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18c-2.305 0-4.408.867-6 2.292m0-14.25v14.25" /></svg>
|
||||
<div class="w-14 h-14 rounded-2xl bg-slate-50 dark:bg-slate-800/50 flex items-center justify-center mx-auto mb-3 border border-slate-100 dark:border-slate-800">
|
||||
<svg class="w-7 h-7 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18c-2.305 0-4.408.867-6 2.292m0-14.25v14.25" /></svg>
|
||||
</div>
|
||||
<p class="text-xs font-black text-slate-400 uppercase tracking-[0.2em]">{{ __('No history records') }}</p>
|
||||
</div>
|
||||
|
||||
@ -48,14 +48,14 @@ $roleSelectConfig = [
|
||||
<div class="flex items-center gap-3">
|
||||
@if($tab === 'roles')
|
||||
<a href="{{ route('admin.data-config.sub-account-roles.create') }}" class="btn-luxury-primary">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<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 Role') }}</span>
|
||||
</a>
|
||||
@else
|
||||
<button @click="openCreateModal()" class="btn-luxury-primary">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<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 Account') }}</span>
|
||||
@ -82,35 +82,68 @@ $roleSelectConfig = [
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Accounts Content (Integrated Card) -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in mt-6">
|
||||
@if($tab === 'roles')
|
||||
<!-- Role Filters & Search -->
|
||||
<form action="{{ route($baseRoute) }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 mb-10">
|
||||
<input type="hidden" name="tab" value="roles">
|
||||
<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" 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-full md:w-80 luxury-input"
|
||||
placeholder="{{ __('Search roles...') }}"
|
||||
@keydown.enter="$el.form.submit()">
|
||||
</div>
|
||||
<!-- Content Container with Loading State -->
|
||||
<div id="ajax-content-container"
|
||||
class="relative transition-all duration-500 min-h-[400px]"
|
||||
:class="isLoadingTable ? 'opacity-40 blur-[2px] pointer-events-none' : 'opacity-100 blur-0'"
|
||||
@ajax:navigate.prevent.stop="fetchPage($event.detail.url)"
|
||||
@click="if ($event.target.closest('a') && $event.target.closest('a').href && ($event.target.closest('a').href.includes('page=') || $event.target.closest('a').href.includes('per_page='))) { $event.preventDefault(); fetchPage($event.target.closest('a').href); }">
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="relative">
|
||||
<x-searchable-select name="company_id" :options="$companies" :selected="request('company_id')" placeholder="{{ __('All Affiliations') }}" class="w-full md:w-auto min-w-[280px]" onchange="this.form.submit()">
|
||||
<option value="system" {{ request('company_id') === 'system' ? 'selected' : '' }} data-title="{{ __('System Level') }}">{{ __('System Level') }}</option>
|
||||
</x-searchable-select>
|
||||
<!-- Loading Spinner Overlay -->
|
||||
<div x-show="isLoadingTable"
|
||||
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-300"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="absolute inset-0 z-50 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>
|
||||
@endif
|
||||
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
||||
<button type="submit" class="hidden"></button>
|
||||
</form>
|
||||
</div>
|
||||
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p>
|
||||
</div>
|
||||
|
||||
<!-- Accounts Content (Integrated Card) -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in mt-6">
|
||||
@if($tab === 'roles')
|
||||
<!-- Role Filters & Search -->
|
||||
<form action="{{ route($baseRoute) }}" method="GET"
|
||||
class="flex flex-col md:flex-row md:items-center gap-4 mb-10"
|
||||
@submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())">
|
||||
<input type="hidden" name="tab" value="roles">
|
||||
<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" 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-full md:w-80 luxury-input"
|
||||
placeholder="{{ __('Search roles...') }}">
|
||||
</div>
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="relative">
|
||||
<x-searchable-select name="company_id" :options="$companies" :selected="request('company_id')" placeholder="{{ __('All Affiliations') }}" class="w-full md:w-auto min-w-[280px]" onchange="this.form.dispatchEvent(new Event('submit'))">
|
||||
<option value="system" {{ request('company_id') === 'system' ? 'selected' : '' }} data-title="{{ __('System Level') }}">{{ __('System Level') }}</option>
|
||||
</x-searchable-select>
|
||||
</div>
|
||||
@endif
|
||||
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
||||
<button type="submit" class="hidden"></button>
|
||||
</form>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-separate border-spacing-y-0">
|
||||
@ -200,35 +233,36 @@ $roleSelectConfig = [
|
||||
{{ $paginated_roles->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
|
||||
@else
|
||||
<!-- Accounts Filters & Search -->
|
||||
<form action="{{ route($baseRoute) }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 mb-10">
|
||||
<input type="hidden" name="tab" value="accounts">
|
||||
<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" 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-full md:w-80 luxury-input"
|
||||
placeholder="{{ __('Search users...') }}"
|
||||
@keydown.enter="$el.form.submit()">
|
||||
</div>
|
||||
@else
|
||||
<!-- Accounts Filters & Search -->
|
||||
<form action="{{ route($baseRoute) }}" method="GET"
|
||||
class="flex flex-col md:flex-row md:items-center gap-4 mb-10"
|
||||
@submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())">
|
||||
<input type="hidden" name="tab" value="accounts">
|
||||
<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" 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-full md:w-80 luxury-input"
|
||||
placeholder="{{ __('Search users...') }}">
|
||||
</div>
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="relative">
|
||||
<x-searchable-select name="company_id" :options="$companies" :selected="request('company_id')"
|
||||
:placeholder="__('All Affiliations')" class="w-full md:w-auto min-w-[280px]"
|
||||
onchange="this.form.submit()" />
|
||||
</div>
|
||||
@endif
|
||||
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
||||
<button type="submit" class="hidden"></button>
|
||||
</form>
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="relative">
|
||||
<x-searchable-select name="company_id" :options="$companies" :selected="request('company_id')"
|
||||
:placeholder="__('All Affiliations')" class="w-full md:w-auto min-w-[280px]"
|
||||
onchange="this.form.dispatchEvent(new Event('submit'))" />
|
||||
</div>
|
||||
@endif
|
||||
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
||||
<button type="submit" class="hidden"></button>
|
||||
</form>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-separate border-spacing-y-0">
|
||||
@ -259,7 +293,7 @@ $roleSelectConfig = [
|
||||
@if($user->avatar)
|
||||
<img src="{{ Storage::url($user->avatar) }}" class="w-full h-full object-cover">
|
||||
@else
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
||||
@ -315,7 +349,7 @@ $roleSelectConfig = [
|
||||
@click="toggleFormAction = '{{ route($baseRoute . '.status.toggle', $user->id) }}'; statusToggleSource = 'list'; isStatusConfirmOpen = true"
|
||||
class="p-2 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="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<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>
|
||||
@ -324,7 +358,7 @@ $roleSelectConfig = [
|
||||
@click="toggleFormAction = '{{ route($baseRoute . '.status.toggle', $user->id) }}'; $nextTick(() => $refs.statusToggleForm.submit())"
|
||||
class="p-2 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="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<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 010 1.971l-11.54 6.347c-.75.412-1.667-.13-1.667-.986V5.653z" />
|
||||
</svg>
|
||||
</button>
|
||||
@ -332,7 +366,7 @@ $roleSelectConfig = [
|
||||
<button @click="openEditModal(@js($user))"
|
||||
class="p-2 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="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
<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" />
|
||||
@ -363,7 +397,7 @@ $roleSelectConfig = [
|
||||
<tr>
|
||||
<td colspan="{{ auth()->user()->isSystemAdmin() ? 6 : 5 }}" class="px-6 py-24 text-center">
|
||||
<div class="flex flex-col items-center gap-3 opacity-20">
|
||||
<svg class="size-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-16 h-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2m16-10a4 4 0 11-8 0 4 4 0 018 0zM23 21v-2a4 4 0 00-3-3.87m-4-12a4 4 0 010 7.75" />
|
||||
</svg>
|
||||
@ -379,8 +413,10 @@ $roleSelectConfig = [
|
||||
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
|
||||
{{ $users->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Modal -->
|
||||
<div x-show="showModal" class="fixed inset-0 z-[100] overflow-y-auto" x-cloak>
|
||||
@ -406,7 +442,7 @@ $roleSelectConfig = [
|
||||
x-text="editing ? '{{ __('Edit Account') }}' : '{{ __('Add Account') }}'"></h3>
|
||||
<button @click="showModal = false"
|
||||
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
||||
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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>
|
||||
@ -616,8 +652,51 @@ $roleSelectConfig = [
|
||||
switchTab(newTab) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('tab', newTab);
|
||||
// Clear search and filters when switching tabs if desired, but here we just go to the URL
|
||||
window.location.href = url.toString();
|
||||
url.searchParams.delete('page'); // Reset page when switching tabs
|
||||
url.searchParams.delete('search'); // Clear search when switching tabs
|
||||
|
||||
this.tab = newTab;
|
||||
this.fetchPage(url.toString());
|
||||
},
|
||||
|
||||
isLoadingTable: false,
|
||||
|
||||
async fetchPage(url) {
|
||||
if (!url || this.isLoadingTable) return;
|
||||
|
||||
this.isLoadingTable = true;
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
if (newContent) {
|
||||
document.querySelector('#ajax-content-container').innerHTML = newContent.innerHTML;
|
||||
|
||||
// Sync URL
|
||||
history.pushState(null, '', url);
|
||||
|
||||
// Re-init tooltips or other UI components if necessary
|
||||
if (window.HSStaticMethods && window.HSStaticMethods.autoInit) {
|
||||
window.HSStaticMethods.autoInit();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
// Fallback to full page reload if AJAX fails
|
||||
window.location.href = url;
|
||||
} finally {
|
||||
this.isLoadingTable = false;
|
||||
}
|
||||
},
|
||||
|
||||
confirmDelete(action) {
|
||||
@ -802,6 +881,11 @@ $roleSelectConfig = [
|
||||
this.syncSelect('modal-account-status', value);
|
||||
});
|
||||
|
||||
// Handle back/forward buttons
|
||||
window.addEventListener('popstate', () => {
|
||||
this.fetchPage(window.location.href);
|
||||
});
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.updateRoleSelect();
|
||||
});
|
||||
|
||||
@ -12,6 +12,26 @@
|
||||
editMachineName: '',
|
||||
activeTab: 'status',
|
||||
currentMachineId: '',
|
||||
isLoadingTable: false,
|
||||
async fetchPage(url) {
|
||||
if (!url || this.isLoadingTable) return;
|
||||
this.isLoadingTable = true;
|
||||
try {
|
||||
const res = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||
const html = await res.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const newContent = doc.querySelector('#ajax-content-container');
|
||||
if (newContent) {
|
||||
document.querySelector('#ajax-content-container').innerHTML = newContent.innerHTML;
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('AJAX Load Failed:', e);
|
||||
} finally {
|
||||
this.isLoadingTable = false;
|
||||
}
|
||||
},
|
||||
currentMachineSn: '',
|
||||
currentMachineName: '',
|
||||
logs: [],
|
||||
@ -131,7 +151,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="space-y-4 pb-20 mt-4" x-data="machineApp()"
|
||||
<div class="space-y-2 pb-20" x-data="machineApp()"
|
||||
@keydown.escape.window="showLogPanel = false; showInventoryPanel = false">
|
||||
<!-- Top Header & Actions -->
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
@ -149,10 +169,39 @@
|
||||
</div>
|
||||
|
||||
<!-- Main Card (Machine List) -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in overflow-hidden mt-6">
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in relative overflow-hidden">
|
||||
<!-- Loading Spinner Overlay -->
|
||||
<div x-show="isLoadingTable"
|
||||
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-300"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="absolute inset-0 z-50 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-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">Syncing Data...</p>
|
||||
</div>
|
||||
|
||||
<div id="ajax-content-container"
|
||||
:class="{ 'opacity-30 pointer-events-none transition-opacity duration-300': isLoadingTable }"
|
||||
@ajax:navigate.prevent.stop="fetchPage($event.detail.url)"
|
||||
@click="if ($event.target.closest('a') && $event.target.closest('a').href && ($event.target.closest('a').href.includes('page=') || $event.target.closest('a').href.includes('search='))) { $event.preventDefault(); fetchPage($event.target.closest('a').href); }">
|
||||
|
||||
<!-- Filters Area -->
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<form method="GET" action="{{ route('admin.machines.index') }}" class="relative group">
|
||||
<form method="GET" action="{{ route('admin.machines.index') }}" class="relative group"
|
||||
@submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())">
|
||||
|
||||
<input type="hidden" name="tab" value="list">
|
||||
<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"
|
||||
@ -341,8 +390,10 @@
|
||||
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
|
||||
{{ $machines->appends(request()->query())->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Offcanvas Log Panel -->
|
||||
<div x-show="showLogPanel" class="fixed inset-0 z-[100] overflow-hidden" style="display: none;"
|
||||
aria-labelledby="slide-over-title" role="dialog" aria-modal="true">
|
||||
@ -485,14 +536,27 @@
|
||||
<div class="relative min-h-[400px]">
|
||||
<!-- Loading State -->
|
||||
<div x-show="loading"
|
||||
class="absolute inset-0 bg-white/50 dark:bg-slate-900/50 backdrop-blur-[1px] flex items-center justify-center z-20">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<div
|
||||
class="w-8 h-8 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin">
|
||||
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-300"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="absolute inset-0 z-50 flex flex-col items-center justify-center bg-white/40 dark:bg-slate-900/40 backdrop-blur-[1px] rounded-2xl" 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>
|
||||
<span class="text-xs font-black text-slate-400 uppercase tracking-widest">{{
|
||||
__('Loading...') }}</span>
|
||||
</div>
|
||||
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p>
|
||||
</div>
|
||||
|
||||
<!-- Logs Container -->
|
||||
@ -777,14 +841,27 @@
|
||||
<div class="relative min-h-[400px]">
|
||||
<!-- Loading State -->
|
||||
<div x-show="inventoryLoading"
|
||||
class="absolute inset-0 bg-white/50 dark:bg-slate-900/50 backdrop-blur-[1px] flex items-center justify-center z-20">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<div
|
||||
class="w-8 h-8 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin">
|
||||
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-300"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="absolute inset-0 z-50 flex flex-col items-center justify-center bg-white/40 dark:bg-slate-900/40 backdrop-blur-[1px] rounded-2xl" 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>
|
||||
<span class="text-xs font-black text-slate-400 uppercase tracking-widest">{{
|
||||
__('Loading...') }}</span>
|
||||
</div>
|
||||
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p>
|
||||
</div>
|
||||
|
||||
<!-- Status Legend -->
|
||||
|
||||
@ -1,10 +1,30 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-6 pb-20" x-data="{
|
||||
<div class="space-y-2 pb-20" x-data="{
|
||||
permissionSearchQuery: '',
|
||||
showPermissionModal: false,
|
||||
isPermissionsLoading: false,
|
||||
isLoadingTable: false,
|
||||
async fetchPage(url) {
|
||||
if (!url || this.isLoadingTable) return;
|
||||
this.isLoadingTable = true;
|
||||
try {
|
||||
const res = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||
const html = await res.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const newContent = doc.querySelector('#ajax-content-container');
|
||||
if (newContent) {
|
||||
document.querySelector('#ajax-content-container').innerHTML = newContent.innerHTML;
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('AJAX Load Failed:', e);
|
||||
} finally {
|
||||
this.isLoadingTable = false;
|
||||
}
|
||||
},
|
||||
targetUserId: null,
|
||||
targetUserName: '',
|
||||
allMachines: [],
|
||||
@ -67,8 +87,9 @@
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message: data.message, type: 'success' } }));
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
this.showPermissionModal = false;
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message: '{{ __('Permissions updated successfully') }}', type: 'success' } }));
|
||||
this.fetchPage(window.location.href);
|
||||
} else {
|
||||
throw new Error(data.error || 'Update failed');
|
||||
}
|
||||
@ -88,11 +109,38 @@
|
||||
</div>
|
||||
|
||||
<!-- 2. Main Content Card -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in relative overflow-hidden">
|
||||
<!-- Loading Spinner Overlay -->
|
||||
<div x-show="isLoadingTable"
|
||||
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-300"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="absolute inset-0 z-50 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-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">Syncing Data...</p>
|
||||
</div>
|
||||
|
||||
<div id="ajax-content-container"
|
||||
:class="{ 'opacity-30 pointer-events-none transition-opacity duration-300': isLoadingTable }">
|
||||
|
||||
<!-- Toolbar & Filters -->
|
||||
<div class="flex flex-col md:flex-row items-center justify-between mb-8 gap-4">
|
||||
<form method="GET" action="{{ route('admin.machines.permissions') }}"
|
||||
class="flex flex-wrap items-center gap-4 w-full md:w-auto">
|
||||
class="flex flex-wrap items-center gap-4 w-full md:w-auto"
|
||||
@submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())">
|
||||
|
||||
<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"
|
||||
@ -110,7 +158,7 @@
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="w-72">
|
||||
<x-searchable-select name="company_id" :options="$companies" :selected="request('company_id')"
|
||||
:placeholder="__('All Companies')" onchange="this.form.submit()" />
|
||||
:placeholder="__('All Companies')" onchange="this.dispatchEvent(new CustomEvent('ajax:navigate', { detail: { url: this.form.action + '?' + new URLSearchParams(new FormData(this.form)).toString() }, bubbles: true }))" />
|
||||
</div>
|
||||
@endif
|
||||
</form>
|
||||
@ -141,7 +189,7 @@
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300 shadow-sm overflow-hidden">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
||||
</svg>
|
||||
@ -167,11 +215,9 @@
|
||||
class="flex flex-wrap gap-2 justify-center lg:justify-start max-w-[420px] mx-auto lg:mx-0 max-h-[140px] overflow-y-auto pr-2 custom-scrollbar py-1">
|
||||
@forelse($user->machines as $m)
|
||||
<div
|
||||
class="flex flex-col px-3 py-1.5 rounded-xl bg-slate-50 dark:bg-slate-800/40 border border-slate-100 dark:border-white/5 hover:border-cyan-500/30 transition-all duration-300">
|
||||
<span class="text-[11px] font-black text-slate-700 dark:text-slate-200 leading-tight">{{
|
||||
class="px-3 py-1.5 rounded-xl bg-slate-50 dark:bg-slate-800/40 border border-slate-100 dark:border-white/5 hover:border-cyan-500/30 transition-all duration-300">
|
||||
<span class="text-xs font-black text-slate-700 dark:text-slate-200 leading-tight">{{
|
||||
$m->name }}</span>
|
||||
<span class="text-[9px] font-mono font-bold text-cyan-500 tracking-tighter mt-0.5 opacity-80">{{
|
||||
$m->serial_no }}</span>
|
||||
</div>
|
||||
@empty
|
||||
<div class="w-full text-center lg:text-left">
|
||||
@ -198,7 +244,7 @@
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-24 text-center">
|
||||
<div class="flex flex-col items-center gap-3 opacity-20">
|
||||
<svg class="size-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-16 h-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2m16-10a4 4 0 11-8 0 4 4 0 018 0zM23 21v-2a4 4 0 00-3-3.87m-4-12a4 4 0 010 7.75" />
|
||||
</svg>
|
||||
@ -214,8 +260,10 @@
|
||||
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
|
||||
{{ $users_list->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Machine Permissions Modal -->
|
||||
<template x-teleport='body'>
|
||||
<div x-show='showPermissionModal' class='fixed inset-0 z-[160] overflow-y-auto' x-cloak>
|
||||
@ -249,7 +297,7 @@
|
||||
</div>
|
||||
<button @click='showPermissionModal = false'
|
||||
class='text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors bg-slate-50 dark:bg-slate-800 p-2 rounded-xl'>
|
||||
<svg class='size-6' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
|
||||
<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>
|
||||
@ -260,7 +308,7 @@
|
||||
<div class='mb-6 flex flex-col md:flex-row gap-4 items-center'>
|
||||
<div class='flex-1 relative group w-full'>
|
||||
<span class='absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10'>
|
||||
<svg class='size-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors'
|
||||
<svg class='w-4 h-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>
|
||||
@ -273,7 +321,7 @@
|
||||
</div>
|
||||
<button @click="toggleSelectAll()"
|
||||
class="shrink-0 flex items-center gap-2 px-6 py-3 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-cyan-500 hover:text-white transition-all duration-300 border border-slate-200 dark:border-slate-700 font-black text-xs uppercase tracking-widest">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
</svg>
|
||||
@ -306,7 +354,7 @@
|
||||
class='p-4 rounded-2xl border-2 cursor-pointer transition-all duration-300 group relative overflow-hidden shadow-sm hover:shadow-md'>
|
||||
<div class='flex flex-col relative z-10'>
|
||||
<div class='flex items-center gap-2'>
|
||||
<div class='size-2 rounded-full'
|
||||
<div class='w-2 h-2 rounded-full'
|
||||
:class='permissions[machine.id] ? "bg-cyan-500" : "bg-slate-300 dark:bg-slate-700"'>
|
||||
</div>
|
||||
<span class='text-sm font-extrabold truncate'
|
||||
@ -319,7 +367,7 @@
|
||||
</div>
|
||||
<div
|
||||
class='absolute -right-2 -bottom-2 opacity-[0.03] text-slate-900 dark:text-white pointer-events-none group-hover:scale-110 transition-transform duration-700'>
|
||||
<svg class='size-20' fill='currentColor' viewBox='0 0 24 24'>
|
||||
<svg class='w-20 h-20' fill='currentColor' viewBox='0 0 24 24'>
|
||||
<path
|
||||
d='M5 2h14c1.1 0 2 .9 2 2v16c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2zm0 2v16h14V4H5zm3 3h8v6H8V7zm0 8h3v2H8v-2zm5 0h3v2h-3v-2z' />
|
||||
</svg>
|
||||
@ -327,8 +375,8 @@
|
||||
<div class='absolute top-4 right-4 animate-luxury-in'
|
||||
x-show='permissions[machine.id]'>
|
||||
<div
|
||||
class='size-5 rounded-full bg-cyan-500 flex items-center justify-center shadow-lg shadow-cyan-500/30'>
|
||||
<svg class='size-3 text-white' fill='none' stroke='currentColor'
|
||||
class='w-5 h-5 rounded-full bg-cyan-500 flex items-center justify-center shadow-lg shadow-cyan-500/30'>
|
||||
<svg class='w-3 h-3 text-white' fill='none' stroke='currentColor'
|
||||
viewBox='0 0 24 24'>
|
||||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='3'
|
||||
d='M5 13l4 4L19 7' />
|
||||
@ -347,8 +395,8 @@
|
||||
<template x-for='i in Math.min(3, Object.values(permissions).filter(v => v).length)'
|
||||
:key='i'>
|
||||
<div
|
||||
class='size-6 rounded-full border-2 border-white dark:border-slate-900 bg-cyan-500 flex items-center justify-center'>
|
||||
<svg class='size-3 text-white' fill='currentColor' viewBox='0 0 24 24'>
|
||||
class='w-6 h-6 rounded-full border-2 border-white dark:border-slate-900 bg-cyan-500 flex items-center justify-center'>
|
||||
<svg class='w-3 h-3 text-white' fill='currentColor' viewBox='0 0 24 24'>
|
||||
<path
|
||||
d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14.5v-9l6 4.5-6 4.5z' />
|
||||
</svg>
|
||||
|
||||
@ -176,9 +176,9 @@ window.utilizationDashboard = function(initialMachines, initialStats) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-6" x-data="utilizationDashboard(@js($compactMachines), @js($fleetStats))">
|
||||
<div class="space-y-2" x-data="utilizationDashboard(@js($compactMachines), @js($fleetStats))">
|
||||
<!-- Page Header -->
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||
<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 tracking-tight">{{ __('Machine Utilization') }}</h1>
|
||||
<p class="text-sm font-bold text-slate-400 uppercase tracking-widest mt-1">{{ __('Real-time fleet efficiency and OEE metrics') }}</p>
|
||||
@ -190,7 +190,7 @@ window.utilizationDashboard = function(initialMachines, initialStats) {
|
||||
</div>
|
||||
|
||||
<!-- Top Stats Bar -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mt-6">
|
||||
<!-- OEE Metric Card -->
|
||||
<div class="luxury-card rounded-3xl p-6 relative overflow-hidden group">
|
||||
<div class="absolute -right-4 -top-4 w-24 h-24 bg-cyan-500/5 rounded-full blur-2xl group-hover:bg-cyan-500/10 transition-all duration-500"></div>
|
||||
@ -241,7 +241,7 @@ window.utilizationDashboard = function(initialMachines, initialStats) {
|
||||
<h3 class="text-3xl font-black text-slate-800 dark:text-white tracking-tight" x-text="'$' + (fleetStats.totalSales || 0).toLocaleString()"></h3>
|
||||
</div>
|
||||
<div class="w-12 h-12 rounded-2xl bg-amber-500/10 flex items-center justify-center text-amber-500">
|
||||
<svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
|
||||
<svg class="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">{{ __('Total Gross Value') }}</p>
|
||||
@ -260,9 +260,30 @@ window.utilizationDashboard = function(initialMachines, initialStats) {
|
||||
</div>
|
||||
|
||||
<!-- Master-Detail Interaction Area -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
|
||||
<!-- Master: Machine List -->
|
||||
<div class="lg:col-span-4 space-y-4">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start relative">
|
||||
<!-- Premium Loading Overlay -->
|
||||
<div x-show="loading"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
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-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">Syncing Data...</p>
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-4 space-y-4" :class="{ 'opacity-40 pointer-events-none transition-opacity duration-500': loading }">
|
||||
|
||||
<div class="flex items-center justify-between mb-2 px-1">
|
||||
<h4 class="text-sm font-black text-slate-800 dark:text-slate-200 uppercase tracking-widest">{{ __('Machine Registry') }}</h4>
|
||||
<span class="text-[10px] font-bold px-2 py-0.5 rounded-md bg-slate-100 dark:bg-slate-800 text-slate-500 uppercase" x-text="(filteredMachines.length) + ' {{ __('Units') }}'"></span>
|
||||
@ -297,7 +318,7 @@ window.utilizationDashboard = function(initialMachines, initialStats) {
|
||||
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-0.5" x-text="machine.serial_no"></p>
|
||||
</div>
|
||||
<svg :class="selectedMachineId === machine.id ? 'text-cyan-500 translate-x-0 opacity-100' : 'text-slate-300 -translate-x-2 opacity-0'"
|
||||
class="size-5 transition-all duration-300" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="m9 18 6-6-6-6"/></svg>
|
||||
class="w-5 h-5 transition-all duration-300" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="m9 18 6-6-6-6"/></svg>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
@ -305,8 +326,8 @@ window.utilizationDashboard = function(initialMachines, initialStats) {
|
||||
<!-- Empty State -->
|
||||
<template x-if="filteredMachines.length === 0">
|
||||
<div class="py-12 text-center">
|
||||
<div class="size-16 mx-auto bg-slate-100 dark:bg-slate-800 rounded-full flex items-center justify-center text-slate-300 mb-4">
|
||||
<svg class="size-8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M15 15l6 6m-6-6l-6-6m6 6l6-6m-6 6l-6 6"/></svg>
|
||||
<div class="w-16 h-16 mx-auto bg-slate-100 dark:bg-slate-800 rounded-full flex items-center justify-center text-slate-300 mb-4">
|
||||
<svg class="w-8 h-8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M15 15l6 6m-6-6l-6-6m6 6l6-6m-6 6l-6 6"/></svg>
|
||||
</div>
|
||||
<p class="text-xs font-bold text-slate-500 uppercase tracking-widest">{{ __('No matching machines') }}</p>
|
||||
</div>
|
||||
@ -328,8 +349,8 @@ window.utilizationDashboard = function(initialMachines, initialStats) {
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="size-20 rounded-3xl bg-luxury-gradient flex items-center justify-center text-white shadow-lg shadow-cyan-500/20">
|
||||
<svg class="size-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
|
||||
<div class="w-20 h-20 rounded-3xl bg-luxury-gradient flex items-center justify-center text-white shadow-lg shadow-cyan-500/20">
|
||||
<svg class="w-10 h-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-3xl font-black text-slate-800 dark:text-white tracking-tight" x-text="selectedMachine.name"></h2>
|
||||
@ -366,7 +387,7 @@ window.utilizationDashboard = function(initialMachines, initialStats) {
|
||||
<p class="text-xs font-bold text-slate-500 uppercase tracking-widest mt-1">{{ __('Real-time performance analytics') }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="size-3 rounded-full bg-cyan-500"></span>
|
||||
<span class="w-3 h-3 rounded-full bg-cyan-500"></span>
|
||||
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{{ __('Cycle Efficiency') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -378,8 +399,8 @@ window.utilizationDashboard = function(initialMachines, initialStats) {
|
||||
<!-- Detail Placeholder -->
|
||||
<template x-if="!selectedMachine">
|
||||
<div class="h-full min-h-[500px] flex flex-col items-center justify-center luxury-card rounded-[2.5rem] border-dashed border-2 border-slate-200 dark:border-slate-800 bg-slate-50/20">
|
||||
<div class="size-24 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-300 mb-6 scale-animation">
|
||||
<svg class="size-12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.5l7 7V19a2 2 0 01-2 2z"/></svg>
|
||||
<div class="w-24 h-24 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-300 mb-6 scale-animation">
|
||||
<svg class="w-12 h-12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.5l7 7V19a2 2 0 01-2 2z"/></svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-black text-slate-800 dark:text-slate-200 tracking-tight">{{ __('No Machine Selected') }}</h3>
|
||||
<p class="text-sm font-bold text-slate-400 uppercase tracking-widest mt-2">{{ __('Select an asset from the left to start analysis') }}</p>
|
||||
|
||||
@ -1,15 +1,35 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-4 pb-20" x-data="{
|
||||
<div class="space-y-2 pb-20" x-data="{
|
||||
showDetailDrawer: false,
|
||||
currentRecord: null,
|
||||
isLoadingTable: false,
|
||||
translations: {
|
||||
'Repair': '{{ __('Repair') }}',
|
||||
'Installation': '{{ __('Installation') }}',
|
||||
'Removal': '{{ __('Removal') }}',
|
||||
'Maintenance': '{{ __('Maintenance') }}'
|
||||
},
|
||||
async fetchPage(url) {
|
||||
if (!url || this.isLoadingTable) return;
|
||||
this.isLoadingTable = true;
|
||||
try {
|
||||
const res = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||
const html = await res.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const newContent = doc.querySelector('#ajax-content-container');
|
||||
if (newContent) {
|
||||
document.querySelector('#ajax-content-container').innerHTML = newContent.innerHTML;
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('AJAX Load Failed:', e);
|
||||
} finally {
|
||||
this.isLoadingTable = false;
|
||||
}
|
||||
},
|
||||
openDetail(record) {
|
||||
this.currentRecord = record;
|
||||
this.showDetailDrawer = true;
|
||||
@ -33,12 +53,41 @@
|
||||
</div>
|
||||
|
||||
<!-- 2. Main Content Card -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in mt-6">
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in relative overflow-hidden">
|
||||
<!-- Loading Spinner Overlay -->
|
||||
<div x-show="isLoadingTable"
|
||||
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-300"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="absolute inset-0 z-50 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-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p>
|
||||
</div>
|
||||
|
||||
<div id="ajax-content-container"
|
||||
:class="{ 'opacity-30 pointer-events-none transition-opacity duration-300': isLoadingTable }"
|
||||
@ajax:navigate.prevent.stop="fetchPage($event.detail.url)"
|
||||
@click="if ($event.target.closest('a') && $event.target.closest('a').href && ($event.target.closest('a').href.includes('page=') || $event.target.closest('a').href.includes('search=') || $event.target.closest('a').href.includes('category='))) { $event.preventDefault(); fetchPage($event.target.closest('a').href); }">
|
||||
|
||||
<!-- Toolbar & Filters -->
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center gap-6">
|
||||
<!-- Search -->
|
||||
<form method="GET" action="{{ route('admin.maintenance.index') }}" class="flex items-center gap-4">
|
||||
<form method="GET" action="{{ route('admin.maintenance.index') }}" class="flex items-center gap-4"
|
||||
@submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())">
|
||||
|
||||
<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"
|
||||
@ -56,7 +105,8 @@
|
||||
<!-- Category Filter (Custom Select UI) -->
|
||||
<div class="flex items-center gap-3 w-48">
|
||||
<x-searchable-select name="category" :placeholder="__('All Categories')" :selected="request('category')"
|
||||
onchange="this.form.submit()" :hasSearch="false" class="luxury-select-sm">
|
||||
onchange="this.dispatchEvent(new CustomEvent('ajax:navigate', { detail: { url: this.form.action + '?' + new URLSearchParams(new FormData(this.form)).toString() }, bubbles: true }))" :hasSearch="false" class="luxury-select-sm">
|
||||
|
||||
@foreach(['Repair', 'Installation', 'Removal', 'Maintenance'] as $cat)
|
||||
<option value="{{ $cat }}" {{ request('category') == $cat ? 'selected' : '' }}>
|
||||
{{ __($cat) }}
|
||||
@ -148,7 +198,10 @@
|
||||
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
|
||||
{{ $records->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 1. Maintenance Detail Drawer -->
|
||||
<template x-teleport="body">
|
||||
|
||||
@ -35,6 +35,44 @@
|
||||
confirmDelete(action) {
|
||||
this.deleteFormAction = action;
|
||||
this.isDeleteConfirmOpen = true;
|
||||
},
|
||||
isLoadingTable: false,
|
||||
async fetchPage(url) {
|
||||
if (!url || this.isLoadingTable) return;
|
||||
|
||||
this.isLoadingTable = true;
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
if (newContent) {
|
||||
document.querySelector('#ajax-content-container').innerHTML = newContent.innerHTML;
|
||||
history.pushState(null, '', url);
|
||||
if (window.HSStaticMethods && window.HSStaticMethods.autoInit) {
|
||||
window.HSStaticMethods.autoInit();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
window.location.href = url;
|
||||
} finally {
|
||||
this.isLoadingTable = false;
|
||||
}
|
||||
},
|
||||
init() {
|
||||
window.addEventListener('popstate', () => {
|
||||
this.fetchPage(window.location.href);
|
||||
});
|
||||
}
|
||||
}">
|
||||
<!-- Header -->
|
||||
@ -49,43 +87,76 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Roles Content (Integrated Card) -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
<!-- Toolbar -->
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-10">
|
||||
<form action="{{ route($baseRoute) }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4">
|
||||
<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" 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-full md:w-80 luxury-input"
|
||||
placeholder="{{ __('Search roles...') }}"
|
||||
@keydown.enter="$el.form.submit()">
|
||||
</div>
|
||||
<!-- Content Container with Loading State -->
|
||||
<div id="ajax-content-container"
|
||||
class="relative transition-all duration-500 min-h-[400px]"
|
||||
:class="isLoadingTable ? 'opacity-40 blur-[2px] pointer-events-none' : 'opacity-100 blur-0'"
|
||||
@ajax:navigate.prevent.stop="fetchPage($event.detail.url)"
|
||||
@click="if ($event.target.closest('a') && $event.target.closest('a').href && ($event.target.closest('a').href.includes('page=') || $event.target.closest('a').href.includes('per_page='))) { $event.preventDefault(); fetchPage($event.target.closest('a').href); }">
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="relative">
|
||||
<x-searchable-select
|
||||
name="company_id"
|
||||
:options="$companies"
|
||||
:selected="request('company_id')"
|
||||
placeholder="{{ __('All Affiliations') }}"
|
||||
class="w-full md:w-auto min-w-[280px]"
|
||||
onchange="this.form.submit()"
|
||||
>
|
||||
<option value="system" {{ request('company_id') === 'system' ? 'selected' : '' }} data-title="{{ __('System Level') }}">{{ __('System Level') }}</option>
|
||||
</x-searchable-select>
|
||||
<!-- Loading Spinner Overlay -->
|
||||
<div x-show="isLoadingTable"
|
||||
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-300"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="absolute inset-0 z-50 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>
|
||||
@endif
|
||||
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
||||
<button type="submit" class="hidden"></button>
|
||||
</form>
|
||||
</div>
|
||||
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p>
|
||||
</div>
|
||||
|
||||
<!-- Roles Content (Integrated Card) -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
<!-- Toolbar -->
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-10">
|
||||
<form action="{{ route($baseRoute) }}" method="GET"
|
||||
class="flex flex-col md:flex-row md:items-center gap-4"
|
||||
@submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())">
|
||||
<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" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" x2="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-full md:w-80 luxury-input"
|
||||
placeholder="{{ __('Search roles...') }}">
|
||||
</div>
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="relative">
|
||||
<x-searchable-select
|
||||
name="company_id"
|
||||
:options="$companies"
|
||||
:selected="request('company_id')"
|
||||
placeholder="{{ __('All Affiliations') }}"
|
||||
class="w-full md:w-auto min-w-[280px]"
|
||||
onchange="this.form.dispatchEvent(new Event('submit'))"
|
||||
>
|
||||
<option value="system" {{ request('company_id') === 'system' ? 'selected' : '' }} data-title="{{ __('System Level') }}">{{ __('System Level') }}</option>
|
||||
</x-searchable-select>
|
||||
</div>
|
||||
@endif
|
||||
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
||||
<button type="submit" class="hidden"></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-separate border-spacing-y-0">
|
||||
<thead>
|
||||
@ -174,6 +245,7 @@
|
||||
{{ $roles->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Global Delete Warning Modal -->
|
||||
<div x-show="isWarningModalOpen" class="fixed inset-0 z-[200] overflow-y-auto" x-cloak>
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
<div class="flex items-center justify-between gap-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="{{ route('admin.data-config.products.index') }}" class="p-2.5 rounded-xl bg-white dark:bg-slate-900 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors border border-slate-200/50 dark:border-slate-700/50">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" /></svg>
|
||||
<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="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" /></svg>
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Create Product') }}</h1>
|
||||
@ -19,7 +19,7 @@
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="submit" form="product-form" class="btn-luxury-primary px-8 py-3 h-12">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /></svg>
|
||||
<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.5 12.75 6 6 9-13.5" /></svg>
|
||||
<span>{{ __('Create Product') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
@ -28,7 +28,7 @@
|
||||
@if ($errors->any())
|
||||
<div class="luxury-card p-4 rounded-xl border-rose-500/20 bg-rose-500/5 sm:max-w-xl animate-luxury-in">
|
||||
<div class="flex gap-3">
|
||||
<svg class="size-5 text-rose-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" /></svg>
|
||||
<svg class="w-5 h-5 text-rose-500 shrink-0 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" /></svg>
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm font-black text-rose-500 uppercase tracking-widest">{{ __('Validation Error') }}</p>
|
||||
<ul class="text-xs font-bold text-rose-500/80 list-disc list-inside space-y-0.5">
|
||||
@ -55,7 +55,7 @@
|
||||
<template x-if="!imagePreview">
|
||||
<div @click="$refs.imageInput.click()" class="aspect-square rounded-3xl border-2 border-dashed border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 flex flex-col items-center justify-center gap-4 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800/80 hover:border-cyan-500/50 transition-all duration-300 group">
|
||||
<div class="p-4 rounded-2xl bg-white dark:bg-slate-800 shadow-sm border border-slate-100 dark:border-slate-700 group-hover:scale-110 group-hover:text-cyan-500 transition-all duration-300">
|
||||
<svg class="size-8 text-slate-400 dark:text-slate-500 group-hover:text-cyan-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" /></svg>
|
||||
<svg class="w-8 h-8 text-slate-400 dark:text-slate-500 group-hover:text-cyan-500 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" /></svg>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<p class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-tighter">{{ __('Click to upload') }}</p>
|
||||
@ -68,10 +68,10 @@
|
||||
<img :src="imagePreview" class="w-full h-full object-cover">
|
||||
<div class="absolute inset-0 bg-slate-950/40 opacity-0 group-hover/image:opacity-100 transition-opacity duration-300 flex items-center justify-center gap-3">
|
||||
<button type="button" @click="$refs.imageInput.click()" class="p-3 rounded-2xl bg-white text-slate-800 hover:bg-cyan-500 hover:text-white transition-all duration-300 shadow-lg">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" /></svg>
|
||||
<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="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" /></svg>
|
||||
</button>
|
||||
<button type="button" @click="removeImage" class="p-3 rounded-2xl bg-white text-slate-800 hover:bg-rose-500 hover:text-white transition-all duration-300 shadow-lg">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 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-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 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>
|
||||
</div>
|
||||
</div>
|
||||
@ -189,13 +189,13 @@
|
||||
<label class="text-xs font-black text-emerald-500 uppercase tracking-widest pl-1">{{ __('Sale Price') }} <span class="text-rose-500">*</span></label>
|
||||
<div class="flex items-center h-14 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 group focus-within:ring-2 focus-within:ring-emerald-500/20 transition-all overflow-hidden">
|
||||
<button type="button" @click="formData.price = Math.max(0, parseInt(formData.price || 0) - 1)" class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
<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="M20 12H4"/></svg>
|
||||
</button>
|
||||
<div class="flex-1 min-w-[72px]">
|
||||
<input type="number" name="price" x-model="formData.price" required class="w-full bg-transparent border-none text-center font-black text-slate-800 dark:text-white focus:ring-0 text-lg [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
|
||||
</div>
|
||||
<button type="button" @click="formData.price = parseInt(formData.price || 0) + 1" class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
<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="M12 4v16m8-8H4"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -203,13 +203,13 @@
|
||||
<label class="text-xs font-black text-cyan-500 dark:text-cyan-400 uppercase tracking-widest pl-1">{{ __('Member Price') }} <span class="text-rose-500">*</span></label>
|
||||
<div class="flex items-center h-14 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 group focus-within:ring-2 focus-within:ring-cyan-500/20 transition-all overflow-hidden">
|
||||
<button type="button" @click="formData.member_price = Math.max(0, parseInt(formData.member_price || 0) - 1)" class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
<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="M20 12H4"/></svg>
|
||||
</button>
|
||||
<div class="flex-1 min-w-[72px]">
|
||||
<input type="number" name="member_price" x-model="formData.member_price" required class="w-full bg-transparent border-none text-center font-black text-slate-800 dark:text-white focus:ring-0 text-lg [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
|
||||
</div>
|
||||
<button type="button" @click="formData.member_price = parseInt(formData.member_price || 0) + 1" class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
<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="M12 4v16m8-8H4"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -217,13 +217,13 @@
|
||||
<label class="text-xs font-black text-slate-400 uppercase tracking-widest pl-1">{{ __('Cost') }} <span class="text-rose-500">*</span></label>
|
||||
<div class="flex items-center h-14 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 group focus-within:ring-2 focus-within:ring-slate-500/20 transition-all overflow-hidden">
|
||||
<button type="button" @click="formData.cost = Math.max(0, parseInt(formData.cost || 0) - 1)" class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 hover:bg-slate-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
<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="M20 12H4"/></svg>
|
||||
</button>
|
||||
<div class="flex-1 min-w-[72px]">
|
||||
<input type="number" name="cost" x-model="formData.cost" required class="w-full bg-transparent border-none text-center font-black text-slate-800 dark:text-white focus:ring-0 text-lg [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
|
||||
</div>
|
||||
<button type="button" @click="formData.cost = parseInt(formData.cost || 0) + 1" class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 hover:bg-slate-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
<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="M12 4v16m8-8H4"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -241,13 +241,13 @@
|
||||
<label class="text-xs font-black text-indigo-500 uppercase tracking-widest pl-1">{{ __('Track Channel Limit') }} <span class="text-rose-500">*</span></label>
|
||||
<div class="flex items-center h-14 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 group focus-within:ring-2 focus-within:ring-indigo-500/20 transition-all overflow-hidden">
|
||||
<button type="button" @click="formData.track_limit = Math.max(0, parseInt(formData.track_limit || 0) - 1)" class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-indigo-500 hover:bg-indigo-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
</button>
|
||||
<div class="flex-1 min-w-[72px]">
|
||||
<input type="number" name="track_limit" x-model="formData.track_limit" required class="w-full bg-transparent border-none text-center font-black text-slate-800 dark:text-white focus:ring-0 text-lg [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
|
||||
</div>
|
||||
<button type="button" @click="formData.track_limit = parseInt(formData.track_limit || 0) + 1" class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-indigo-500 hover:bg-indigo-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -255,13 +255,13 @@
|
||||
<label class="text-xs font-black text-amber-500 uppercase tracking-widest pl-1">{{ __('Spring Channel Limit') }} <span class="text-rose-500">*</span></label>
|
||||
<div class="flex items-center h-14 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 group focus-within:ring-2 focus-within:ring-amber-500/20 transition-all overflow-hidden">
|
||||
<button type="button" @click="formData.spring_limit = Math.max(0, parseInt(formData.spring_limit || 0) - 1)" class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-amber-500 hover:bg-amber-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
</button>
|
||||
<div class="flex-1 min-w-[72px]">
|
||||
<input type="number" name="spring_limit" x-model="formData.spring_limit" required class="w-full bg-transparent border-none text-center font-black text-slate-800 dark:text-white focus:ring-0 text-lg [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
|
||||
</div>
|
||||
<button type="button" @click="formData.spring_limit = parseInt(formData.spring_limit || 0) + 1" class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-amber-500 hover:bg-amber-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -293,13 +293,13 @@
|
||||
<label class="text-xs font-black text-cyan-500 uppercase tracking-widest pl-1">{{ __('Full Points') }}</label>
|
||||
<div class="flex items-center h-14 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 group focus-within:ring-2 focus-within:ring-cyan-500/20 transition-all overflow-hidden">
|
||||
<button type="button" @click="formData.metadata.points_full = Math.max(0, parseInt(formData.metadata.points_full || 0) - 1)" class="shrink-0 w-10 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
</button>
|
||||
<div class="flex-1 min-w-[60px]">
|
||||
<input type="number" name="metadata[points_full]" x-model="formData.metadata.points_full" class="w-full bg-transparent border-none text-center font-black text-slate-800 dark:text-white focus:ring-0 text-lg [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
|
||||
</div>
|
||||
<button type="button" @click="formData.metadata.points_full = parseInt(formData.metadata.points_full || 0) + 1" class="shrink-0 w-10 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -307,13 +307,13 @@
|
||||
<label class="text-xs font-black text-cyan-500 uppercase tracking-widest pl-1">{{ __('Half Points') }}</label>
|
||||
<div class="flex items-center h-14 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 group focus-within:ring-2 focus-within:ring-cyan-500/20 transition-all overflow-hidden">
|
||||
<button type="button" @click="formData.metadata.points_half = Math.max(0, parseInt(formData.metadata.points_half || 0) - 1)" class="shrink-0 w-10 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
</button>
|
||||
<div class="flex-1 min-w-[60px]">
|
||||
<input type="number" name="metadata[points_half]" x-model="formData.metadata.points_half" class="w-full bg-transparent border-none text-center font-black text-slate-800 dark:text-white focus:ring-0 text-lg [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
|
||||
</div>
|
||||
<button type="button" @click="formData.metadata.points_half = parseInt(formData.metadata.points_half || 0) + 1" class="shrink-0 w-10 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -321,13 +321,13 @@
|
||||
<label class="text-xs font-black text-cyan-500 uppercase tracking-widest pl-1">{{ __('Half Points Amount') }}</label>
|
||||
<div class="flex items-center h-14 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 group focus-within:ring-2 focus-within:ring-cyan-500/20 transition-all overflow-hidden">
|
||||
<button type="button" @click="formData.metadata.points_half_amount = Math.max(0, parseInt(formData.metadata.points_half_amount || 0) - 1)" class="shrink-0 w-10 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
</button>
|
||||
<div class="flex-1 min-w-[60px]">
|
||||
<input type="number" name="metadata[points_half_amount]" x-model="formData.metadata.points_half_amount" class="w-full bg-transparent border-none text-center font-black text-slate-800 dark:text-white focus:ring-0 text-lg [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
|
||||
</div>
|
||||
<button type="button" @click="formData.metadata.points_half_amount = parseInt(formData.metadata.points_half_amount || 0) + 1" class="shrink-0 w-10 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -340,7 +340,7 @@
|
||||
<div class="pt-8 flex items-center justify-end gap-4 border-t border-slate-100 dark:border-slate-800 animate-luxury-in" style="animation-delay: 500ms">
|
||||
<a href="{{ route('admin.data-config.products.index') }}" class="btn-luxury-ghost px-8 h-12">{{ __('Cancel') }}</a>
|
||||
<button type="submit" form="product-form" class="btn-luxury-primary px-12 h-14 shadow-xl shadow-cyan-500/20">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /></svg>
|
||||
<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.5 12.75 6 6 9-13.5" /></svg>
|
||||
<span>{{ __('Create Product') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
<div class="flex items-center justify-between gap-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="{{ route('admin.data-config.products.index') }}" class="p-2.5 rounded-xl bg-white dark:bg-slate-900 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors border border-slate-200/50 dark:border-slate-700/50">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" /></svg>
|
||||
<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="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" /></svg>
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Edit Product') }}</h1>
|
||||
@ -32,7 +32,7 @@
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="submit" form="product-form" class="btn-luxury-primary px-8 py-3 h-12">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /></svg>
|
||||
<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.5 12.75 6 6 9-13.5" /></svg>
|
||||
<span>{{ __('Save Changes') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
@ -41,7 +41,7 @@
|
||||
@if ($errors->any())
|
||||
<div class="luxury-card p-4 rounded-xl border-rose-500/20 bg-rose-500/5 sm:max-w-xl animate-luxury-in">
|
||||
<div class="flex gap-3">
|
||||
<svg class="size-5 text-rose-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" /></svg>
|
||||
<svg class="w-5 h-5 text-rose-500 shrink-0 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" /></svg>
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm font-black text-rose-500 uppercase tracking-widest">{{ __('Validation Error') }}</p>
|
||||
<ul class="text-xs font-bold text-rose-500/80 list-disc list-inside space-y-0.5">
|
||||
@ -71,7 +71,7 @@
|
||||
<template x-if="!imagePreview">
|
||||
<div @click="$refs.imageInput.click()" class="aspect-square rounded-3xl border-2 border-dashed border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 flex flex-col items-center justify-center gap-4 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800/80 hover:border-cyan-500/50 transition-all duration-300 group">
|
||||
<div class="p-4 rounded-2xl bg-white dark:bg-slate-800 shadow-sm border border-slate-100 dark:border-slate-700 group-hover:scale-110 group-hover:text-cyan-500 transition-all duration-300">
|
||||
<svg class="size-8 text-slate-400 dark:text-slate-500 group-hover:text-cyan-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" /></svg>
|
||||
<svg class="w-8 h-8 text-slate-400 dark:text-slate-500 group-hover:text-cyan-500 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" /></svg>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<p class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-tighter">{{ __('Click to upload') }}</p>
|
||||
@ -84,10 +84,10 @@
|
||||
<img :src="imagePreview" class="w-full h-full object-cover">
|
||||
<div class="absolute inset-0 bg-slate-950/40 opacity-0 group-hover/image:opacity-100 transition-opacity duration-300 flex items-center justify-center gap-3">
|
||||
<button type="button" @click="$refs.imageInput.click()" class="p-3 rounded-2xl bg-white text-slate-800 hover:bg-cyan-500 hover:text-white transition-all duration-300 shadow-lg">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" /></svg>
|
||||
<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="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" /></svg>
|
||||
</button>
|
||||
<button type="button" @click="removeImage" class="p-3 rounded-2xl bg-white text-slate-800 hover:bg-rose-500 hover:text-white transition-all duration-300 shadow-lg">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 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-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 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>
|
||||
</div>
|
||||
</div>
|
||||
@ -115,10 +115,11 @@
|
||||
<div class="h-px bg-slate-100 dark:bg-slate-800"></div>
|
||||
<div class="space-y-3">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Company') }}</label>
|
||||
<div class="px-4 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-sm font-black text-slate-700 dark:text-slate-200 border border-slate-100 dark:border-slate-800">
|
||||
{{ $product->company->name ?? 'N/A' }}
|
||||
<input type="hidden" name="company_id" value="{{ $product->company_id }}">
|
||||
</div>
|
||||
<x-searchable-select id="product-company" name="company_id" x-model="formData.company_id" @change="updateCompanySettings($event.target.value)" :placeholder="__('Select Company')">
|
||||
@foreach($companies as $company)
|
||||
<option value="{{ $company->id }}" {{ $product->company_id == $company->id ? 'selected' : '' }} data-title="{{ $company->name }}">{{ $company->name }}</option>
|
||||
@endforeach
|
||||
</x-searchable-select>
|
||||
</div>
|
||||
@else
|
||||
<input type="hidden" name="company_id" value="{{ $product->company_id }}">
|
||||
@ -204,13 +205,13 @@
|
||||
<label class="text-xs font-black text-emerald-500 uppercase tracking-widest pl-1">{{ __('Sale Price') }} <span class="text-rose-500">*</span></label>
|
||||
<div class="flex items-center h-14 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 group focus-within:ring-2 focus-within:ring-emerald-500/20 transition-all overflow-hidden">
|
||||
<button type="button" @click="formData.price = Math.max(0, parseInt(formData.price || 0) - 1)" class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
<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="M20 12H4"/></svg>
|
||||
</button>
|
||||
<div class="flex-1 min-w-[72px]">
|
||||
<input type="number" name="price" x-model="formData.price" required class="w-full bg-transparent border-none text-center font-black text-slate-800 dark:text-white focus:ring-0 text-lg [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
|
||||
</div>
|
||||
<button type="button" @click="formData.price = parseInt(formData.price || 0) + 1" class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
<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="M12 4v16m8-8H4"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -218,13 +219,13 @@
|
||||
<label class="text-xs font-black text-cyan-500 dark:text-cyan-400 uppercase tracking-widest pl-1">{{ __('Member Price') }} <span class="text-rose-500">*</span></label>
|
||||
<div class="flex items-center h-14 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 group focus-within:ring-2 focus-within:ring-cyan-500/20 transition-all overflow-hidden">
|
||||
<button type="button" @click="formData.member_price = Math.max(0, parseInt(formData.member_price || 0) - 1)" class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
<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="M20 12H4"/></svg>
|
||||
</button>
|
||||
<div class="flex-1 min-w-[72px]">
|
||||
<input type="number" name="member_price" x-model="formData.member_price" required class="w-full bg-transparent border-none text-center font-black text-slate-800 dark:text-white focus:ring-0 text-lg [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
|
||||
</div>
|
||||
<button type="button" @click="formData.member_price = parseInt(formData.member_price || 0) + 1" class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
<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="M12 4v16m8-8H4"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -232,13 +233,13 @@
|
||||
<label class="text-xs font-black text-slate-400 uppercase tracking-widest pl-1">{{ __('Cost') }} <span class="text-rose-500">*</span></label>
|
||||
<div class="flex items-center h-14 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 group focus-within:ring-2 focus-within:ring-slate-500/20 transition-all overflow-hidden">
|
||||
<button type="button" @click="formData.cost = Math.max(0, parseInt(formData.cost || 0) - 1)" class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 hover:bg-slate-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
<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="M20 12H4"/></svg>
|
||||
</button>
|
||||
<div class="flex-1 min-w-[72px]">
|
||||
<input type="number" name="cost" x-model="formData.cost" required class="w-full bg-transparent border-none text-center font-black text-slate-800 dark:text-white focus:ring-0 text-lg [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
|
||||
</div>
|
||||
<button type="button" @click="formData.cost = parseInt(formData.cost || 0) + 1" class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 hover:bg-slate-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
<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="M12 4v16m8-8H4"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -256,13 +257,13 @@
|
||||
<label class="text-xs font-black text-indigo-500 uppercase tracking-widest pl-1">{{ __('Track Channel Limit') }} <span class="text-rose-500">*</span></label>
|
||||
<div class="flex items-center h-14 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 group focus-within:ring-2 focus-within:ring-indigo-500/20 transition-all overflow-hidden">
|
||||
<button type="button" @click="formData.track_limit = Math.max(0, parseInt(formData.track_limit || 0) - 1)" class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-indigo-500 hover:bg-indigo-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
<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="M20 12H4"/></svg>
|
||||
</button>
|
||||
<div class="flex-1 min-w-[72px]">
|
||||
<input type="number" name="track_limit" x-model="formData.track_limit" required class="w-full bg-transparent border-none text-center font-black text-slate-800 dark:text-white focus:ring-0 text-lg [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
|
||||
</div>
|
||||
<button type="button" @click="formData.track_limit = parseInt(formData.track_limit || 0) + 1" class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-indigo-500 hover:bg-indigo-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
<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="M12 4v16m8-8H4"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -270,13 +271,13 @@
|
||||
<label class="text-xs font-black text-amber-500 uppercase tracking-widest pl-1">{{ __('Spring Channel Limit') }} <span class="text-rose-500">*</span></label>
|
||||
<div class="flex items-center h-14 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 group focus-within:ring-2 focus-within:ring-amber-500/20 transition-all overflow-hidden">
|
||||
<button type="button" @click="formData.spring_limit = Math.max(0, parseInt(formData.spring_limit || 0) - 1)" class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-amber-500 hover:bg-amber-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
<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="M20 12H4"/></svg>
|
||||
</button>
|
||||
<div class="flex-1 min-w-[72px]">
|
||||
<input type="number" name="spring_limit" x-model="formData.spring_limit" required class="w-full bg-transparent border-none text-center font-black text-slate-800 dark:text-white focus:ring-0 text-lg [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
|
||||
</div>
|
||||
<button type="button" @click="formData.spring_limit = parseInt(formData.spring_limit || 0) + 1" class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-amber-500 hover:bg-amber-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
<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="M12 4v16m8-8H4"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -308,13 +309,13 @@
|
||||
<label class="text-xs font-black text-cyan-500 uppercase tracking-widest pl-1">{{ __('Full Points') }}</label>
|
||||
<div class="flex items-center h-14 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 group focus-within:ring-2 focus-within:ring-cyan-500/20 transition-all overflow-hidden">
|
||||
<button type="button" @click="formData.metadata.points_full = Math.max(0, parseInt(formData.metadata.points_full || 0) - 1)" class="shrink-0 w-10 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
<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="M20 12H4"/></svg>
|
||||
</button>
|
||||
<div class="flex-1 min-w-[60px]">
|
||||
<input type="number" name="metadata[points_full]" x-model="formData.metadata.points_full" class="w-full bg-transparent border-none text-center font-black text-slate-800 dark:text-white focus:ring-0 text-lg [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
|
||||
</div>
|
||||
<button type="button" @click="formData.metadata.points_full = parseInt(formData.metadata.points_full || 0) + 1" class="shrink-0 w-10 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
<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="M12 4v16m8-8H4"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -322,13 +323,13 @@
|
||||
<label class="text-xs font-black text-cyan-500 uppercase tracking-widest pl-1">{{ __('Half Points') }}</label>
|
||||
<div class="flex items-center h-14 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 group focus-within:ring-2 focus-within:ring-cyan-500/20 transition-all overflow-hidden">
|
||||
<button type="button" @click="formData.metadata.points_half = Math.max(0, parseInt(formData.metadata.points_half || 0) - 1)" class="shrink-0 w-10 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
<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="M20 12H4"/></svg>
|
||||
</button>
|
||||
<div class="flex-1 min-w-[60px]">
|
||||
<input type="number" name="metadata[points_half]" x-model="formData.metadata.points_half" class="w-full bg-transparent border-none text-center font-black text-slate-800 dark:text-white focus:ring-0 text-lg [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
|
||||
</div>
|
||||
<button type="button" @click="formData.metadata.points_half = parseInt(formData.metadata.points_half || 0) + 1" class="shrink-0 w-10 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
<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="M12 4v16m8-8H4"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -336,13 +337,13 @@
|
||||
<label class="text-xs font-black text-cyan-500 uppercase tracking-widest pl-1">{{ __('Half Points Amount') }}</label>
|
||||
<div class="flex items-center h-14 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 group focus-within:ring-2 focus-within:ring-cyan-500/20 transition-all overflow-hidden">
|
||||
<button type="button" @click="formData.metadata.points_half_amount = Math.max(0, parseInt(formData.metadata.points_half_amount || 0) - 1)" class="shrink-0 w-10 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/></svg>
|
||||
<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="M20 12H4"/></svg>
|
||||
</button>
|
||||
<div class="flex-1 min-w-[60px]">
|
||||
<input type="number" name="metadata[points_half_amount]" x-model="formData.metadata.points_half_amount" class="w-full bg-transparent border-none text-center font-black text-slate-800 dark:text-white focus:ring-0 text-lg [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
|
||||
</div>
|
||||
<button type="button" @click="formData.metadata.points_half_amount = parseInt(formData.metadata.points_half_amount || 0) + 1" class="shrink-0 w-10 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
<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="M12 4v16m8-8H4"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -355,7 +356,7 @@
|
||||
<div class="pt-8 flex items-center justify-end gap-4 border-t border-slate-100 dark:border-slate-800 animate-luxury-in" style="animation-delay: 500ms">
|
||||
<a href="{{ route('admin.data-config.products.index') }}" class="btn-luxury-ghost px-8 h-12">{{ __('Cancel') }}</a>
|
||||
<button type="submit" form="product-form" class="btn-luxury-primary px-12 h-14 shadow-xl shadow-cyan-500/20">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /></svg>
|
||||
<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.5 12.75 6 6 9-13.5" /></svg>
|
||||
<span>{{ __('Save Changes') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
@ -419,6 +420,20 @@
|
||||
this.imagePreview = null;
|
||||
this.formData.remove_image = true;
|
||||
this.$refs.imageInput.value = '';
|
||||
},
|
||||
|
||||
updateCompanySettings(companyId) {
|
||||
if (!companyId) {
|
||||
this.companySettings = { enable_material_code: false, enable_points: false };
|
||||
return;
|
||||
}
|
||||
const company = this.companies.find(c => c.id == companyId);
|
||||
if (company) {
|
||||
this.companySettings = {
|
||||
enable_material_code: company.settings?.enable_material_code || false,
|
||||
enable_points: company.settings?.enable_points || false
|
||||
};
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
@ -34,7 +34,7 @@ $roleSelectConfig = [
|
||||
<div class="flex items-center gap-3">
|
||||
<template x-if="activeTab === 'products'">
|
||||
<a href="{{ route($baseRoute . '.create') }}" class="btn-luxury-primary transition-all duration-300">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<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 Product') }}</span>
|
||||
@ -42,7 +42,7 @@ $roleSelectConfig = [
|
||||
</template>
|
||||
<template x-if="activeTab === 'categories'">
|
||||
<button @click="openCategoryModal()" type="button" class="btn-luxury-primary transition-all duration-300 bg-emerald-600 hover:bg-emerald-700 shadow-emerald-500/20">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<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 Category') }}</span>
|
||||
@ -147,7 +147,7 @@ $roleSelectConfig = [
|
||||
<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="categoryModalMode === 'create' ? '{{ __('Add Category') }}' : '{{ __('Edit Category') }}'"></h3>
|
||||
<button @click="isCategoryModalOpen = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
||||
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -280,7 +280,7 @@ $roleSelectConfig = [
|
||||
<img :src="selectedProduct.image_url" class="absolute inset-0 w-full h-full object-cover group-hover:scale-105 transition-transform duration-1000">
|
||||
<div class="absolute inset-0 bg-slate-950/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
|
||||
<div class="p-3 rounded-full bg-white/20 backdrop-blur-md text-white border border-white/30 scale-50 group-hover:scale-100 transition-all duration-500 shadow-2xl">
|
||||
<svg class="size-6 shadow-glow" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM10.5 7.5v6m3-3h-6" /></svg>
|
||||
<svg class="w-6 h-6 shadow-glow" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM10.5 7.5v6m3-3h-6" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -412,7 +412,7 @@ $roleSelectConfig = [
|
||||
x-cloak>
|
||||
|
||||
<button @click="isImageZoomed = false" class="absolute top-6 right-6 p-3 rounded-full bg-white/10 text-white hover:bg-white/20 transition-all border border-white/10 active:scale-95">
|
||||
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
</button>
|
||||
|
||||
<div class="relative max-w-5xl w-full aspect-square md:aspect-auto md:max-h-[90vh] flex items-center justify-center" @click.away="isImageZoomed = false">
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<!-- Search Input -->
|
||||
<div class="relative group">
|
||||
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-slate-400 group-focus-within:text-cyan-500 transition-colors">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
||||
<svg 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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
||||
</div>
|
||||
<input type="text" name="category_search" value="{{ request('category_search', request('tab') === 'categories' ? request('category_search') : '') }}"
|
||||
class="bg-slate-50 dark:bg-slate-900/50 border-none rounded-xl py-3 pl-10 pr-4 text-sm font-bold text-slate-700 dark:text-slate-200 focus:ring-2 focus:ring-cyan-500/20 w-64 transition-all shadow-sm"
|
||||
@ -40,7 +40,7 @@
|
||||
<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 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white group-hover:border-cyan-500 shadow-sm group-hover:shadow-cyan-500/50 transition-all duration-300">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<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="M9.568 3H5.25A2.25 2.25 0 003 5.25v13.5A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V9.432a2.25 2.25 0 00-.659-1.591l-4.182-4.182A2.25 2.25 0 0014.568 3h-5z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12h-6m6 4h-6m6-8h-6" />
|
||||
</svg>
|
||||
@ -58,10 +58,10 @@
|
||||
<td class="px-6 py-5 text-right">
|
||||
<div class="flex justify-end items-center gap-2">
|
||||
<button type="button" @click="openCategoryModal(@js($category))" 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="size-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 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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="confirmDelete('{{ route('admin.data-config.product-categories.destroy', $category->id) }}')" 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="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 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 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 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>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<form @submit.prevent="handleFilterSubmit('products')" id="products-filter-form" class="flex flex-col md:flex-row md:items-center gap-4 mb-6">
|
||||
<div class="relative group">
|
||||
<button type="submit" class="absolute inset-y-0 left-0 flex items-center pl-4 z-10 text-slate-400 group-focus-within:text-cyan-500 transition-colors">
|
||||
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg class="w-4 h-4 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>
|
||||
</button>
|
||||
@ -50,7 +50,7 @@
|
||||
@if($product->image_url)
|
||||
<img src="{{ $product->image_url }}" class="w-full h-full object-cover">
|
||||
@else
|
||||
<svg class="size-6" 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>
|
||||
<svg class="w-6 h-6 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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">
|
||||
@ -103,24 +103,24 @@
|
||||
@click="toggleStatus('{{ route($baseRoute . '.status.toggle', $product->id) }}')"
|
||||
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="size-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 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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="toggleStatus('{{ route($baseRoute . '.status.toggle', $product->id) }}')"
|
||||
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 transition-all border border-transparent hover:border-emerald-500/20"
|
||||
title="{{ __('Enable') }}">
|
||||
<svg class="size-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 010 1.971l-11.54 6.347c-.75.412-1.667-.13-1.667-.986V5.653z" /></svg>
|
||||
<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.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347c-.75.412-1.667-.13-1.667-.986V5.653z" /></svg>
|
||||
</button>
|
||||
@endif
|
||||
<a href="{{ route($baseRoute . '.edit', $product->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="{{ __('Edit') }}">
|
||||
<svg class="size-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 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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>
|
||||
</a>
|
||||
<button type="button" @click="confirmDelete('{{ route($baseRoute . '.destroy', $product->id) }}')" 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="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 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 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 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="viewProductDetail({{ json_encode($product) }})" class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-indigo-500 hover:bg-indigo-500/5 transition-all border border-transparent hover:border-indigo-500/20" title="{{ __('View') }}">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<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-.644C3.67 8.5 7.652 6 12 6c4.348 0 8.33 2.5 9.964 5.678.134.263.134.561 0 .824C20.33 15.5 16.348 18 12 18c-4.348 0-8.33-2.5-9.964-5.678Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
|
||||
@ -512,7 +512,7 @@
|
||||
<template x-if="viewMode === 'control'">
|
||||
<button @click="backToList()"
|
||||
class="p-2.5 rounded-xl bg-white dark:bg-slate-900 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-all border border-slate-200/50 dark:border-slate-700/50 shadow-sm hover:shadow-md active:scale-95">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@ -132,7 +132,7 @@
|
||||
<button @click="selectMachine({{ Js::from($machine) }})"
|
||||
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="{{ __('Manage') }}">
|
||||
<svg class="size-4" fill="none" stroke="currentColor"
|
||||
<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" />
|
||||
|
||||
@ -94,7 +94,7 @@
|
||||
<button @click="selectMachine({{ Js::from($machine) }})"
|
||||
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="{{ __('Manage') }}">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<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>
|
||||
|
||||
@ -356,7 +356,7 @@
|
||||
<template x-if="viewMode === 'detail'">
|
||||
<button @click="backToList()"
|
||||
class="p-2.5 rounded-xl bg-white dark:bg-slate-900 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-all border border-slate-200/50 dark:border-slate-700/50 shadow-sm hover:shadow-md active:scale-95">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
|
||||
</svg>
|
||||
</button>
|
||||
@ -708,7 +708,7 @@
|
||||
</div>
|
||||
<button @click="showEditModal = false"
|
||||
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors p-2 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800">
|
||||
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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>
|
||||
|
||||
272
resources/views/admin/warehouses/index.blade.php
Normal file
272
resources/views/admin/warehouses/index.blade.php
Normal file
@ -0,0 +1,272 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="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 },
|
||||
openCreateModal() {
|
||||
this.editing = false;
|
||||
this.currentWarehouse = { id: '', name: '', type: 'branch', address: '', is_active: 1 };
|
||||
this.showModal = true;
|
||||
},
|
||||
openEditModal(wh) {
|
||||
this.editing = true;
|
||||
this.currentWarehouse = { ...wh };
|
||||
this.showModal = true;
|
||||
},
|
||||
submitConfirmedForm() {
|
||||
this.$refs.statusToggleForm.submit();
|
||||
}
|
||||
}">
|
||||
{{-- 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">{{ __('Warehouse Overview') }}</h1>
|
||||
<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>
|
||||
<span>{{ __('Add Warehouse') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Stats Cards --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="luxury-card p-6 rounded-2xl animate-luxury-in">
|
||||
<p class="text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Total Warehouses') }}</p>
|
||||
<p class="text-3xl font-black text-slate-800 dark:text-white mt-2">{{ $stats['total'] }}</p>
|
||||
</div>
|
||||
<div class="luxury-card p-6 rounded-2xl animate-luxury-in" style="animation-delay: 100ms">
|
||||
<p class="text-sm font-black text-cyan-500 uppercase tracking-widest">{{ __('Main Warehouses') }}</p>
|
||||
<p class="text-3xl font-black text-slate-800 dark:text-white mt-2">{{ $stats['main'] }}</p>
|
||||
</div>
|
||||
<div class="luxury-card p-6 rounded-2xl animate-luxury-in" style="animation-delay: 200ms">
|
||||
<p class="text-sm font-black text-indigo-500 uppercase tracking-widest">{{ __('Branch Warehouses') }}</p>
|
||||
<p class="text-3xl font-black text-slate-800 dark:text-white mt-2">{{ $stats['branch'] }}</p>
|
||||
</div>
|
||||
<div class="luxury-card p-6 rounded-2xl animate-luxury-in" style="animation-delay: 300ms">
|
||||
<p class="text-sm font-black text-rose-500 uppercase tracking-widest">{{ __('Low Stock Alerts') }}</p>
|
||||
<p class="text-3xl font-black text-slate-800 dark:text-white mt-2">{{ $stats['low_stock'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Table Card --}}
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 400ms">
|
||||
{{-- 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">
|
||||
<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>
|
||||
</span>
|
||||
<input type="text" name="search" value="{{ request('search') }}" class="py-2.5 pl-12 pr-6 block w-64 luxury-input" placeholder="{{ __('Search warehouses...') }}">
|
||||
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
||||
</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') }}"
|
||||
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']) }}"
|
||||
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']) }}"
|
||||
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>
|
||||
</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">{{ __('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>
|
||||
</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">
|
||||
<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>
|
||||
<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>
|
||||
@if($warehouse->address)
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
@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>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-5 text-right">
|
||||
<div class="flex justify-end items-center gap-2">
|
||||
@if($warehouse->is_active)
|
||||
<button type="button"
|
||||
@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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="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>
|
||||
<p class="text-slate-400 font-bold">{{ __('No warehouses found') }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- Pagination --}}
|
||||
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">
|
||||
{{ $warehouses->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 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>
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
||||
<div x-show="showModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100" x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" class="inline-block px-8 py-10 overflow-visible text-left align-bottom transition-all transform luxury-card rounded-3xl dark:bg-slate-900 border-slate-200/50 dark:border-slate-700/50 shadow-2xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||
<div 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>
|
||||
</button>
|
||||
</div>
|
||||
<form x-ref="warehouseForm" :action="editing ? '{{ url('admin/warehouses') }}/' + currentWarehouse.id : '{{ route('admin.warehouses.store') }}'" method="POST" 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') }}">
|
||||
</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">
|
||||
<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">
|
||||
{{ __('Main') }}
|
||||
</button>
|
||||
<button type="button" @click="currentWarehouse.type = 'branch'"
|
||||
:class="currentWarehouse.type === 'branch' ? 'bg-indigo-500 text-white shadow-lg shadow-indigo-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">
|
||||
{{ __('Branch') }}
|
||||
</button>
|
||||
</div>
|
||||
<input type="hidden" name="type" :value="currentWarehouse.type">
|
||||
</div>
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Confirm Modals --}}
|
||||
<x-delete-confirm-modal :message="__('Are you sure to delete this warehouse? This action cannot be undone.')" />
|
||||
<x-status-confirm-modal :title="__('Disable Warehouse')" :message="__('Are you sure to disable this warehouse?')" />
|
||||
<form x-ref="statusToggleForm" :action="toggleFormAction" method="POST" class="hidden">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
275
resources/views/admin/warehouses/inventory.blade.php
Normal file
275
resources/views/admin/warehouses/inventory.blade.php
Normal file
@ -0,0 +1,275 @@
|
||||
@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 --}}
|
||||
<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>
|
||||
</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>
|
||||
</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' }}">
|
||||
{{ __('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' }}">
|
||||
{{ __('Movement History') }}
|
||||
</a>
|
||||
</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...') }}">
|
||||
</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>
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</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>
|
||||
@endsection
|
||||
139
resources/views/admin/warehouses/machine-inventory.blade.php
Normal file
139
resources/views/admin/warehouses/machine-inventory.blade.php
Normal file
@ -0,0 +1,139 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-3 pb-20" x-data="{
|
||||
showSlotDrawer: false,
|
||||
selectedMachine: null,
|
||||
slots: [],
|
||||
loading: false,
|
||||
async viewSlots(machine) {
|
||||
this.selectedMachine = machine;
|
||||
this.showSlotDrawer = true;
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await fetch(`/admin/warehouses/machine-inventory/${machine.id}/slots`);
|
||||
const data = await res.json();
|
||||
this.slots = data.slots || [];
|
||||
} catch(e) { this.slots = []; }
|
||||
this.loading = false;
|
||||
}
|
||||
}">
|
||||
{{-- 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 Inventory Overview') }}</h1>
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Real-time slot-level inventory across all machines') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Table --}}
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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">{{ __('Machine') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Serial No.') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Slots') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Total Stock') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Fill Rate') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||
@forelse($machines as $machine)
|
||||
@php
|
||||
$maxStock = $machine->slots->sum('max_stock') ?: 1;
|
||||
$currentStock = (int)($machine->total_stock ?? 0);
|
||||
$fillRate = round(($currentStock / $maxStock) * 100);
|
||||
$fillColor = $fillRate >= 50 ? 'emerald' : ($fillRate >= 20 ? 'amber' : 'rose');
|
||||
@endphp
|
||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||
<td class="px-6 py-5">
|
||||
<span class="font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ $machine->name }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-5">
|
||||
<span class="font-mono font-bold text-slate-500">{{ $machine->serial_no }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-5 text-center">
|
||||
<span class="font-black text-slate-800 dark:text-white">{{ $machine->total_slots ?? 0 }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-5 text-center">
|
||||
<span class="font-black text-slate-800 dark:text-white">{{ $currentStock }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-5 text-center">
|
||||
<div class="flex items-center justify-center gap-3">
|
||||
<div class="w-24 h-2 bg-slate-100 dark:bg-slate-800 rounded-full overflow-hidden">
|
||||
<div class="h-full bg-{{ $fillColor }}-500 rounded-full transition-all" style="width: {{ $fillRate }}%"></div>
|
||||
</div>
|
||||
<span class="text-xs font-black text-{{ $fillColor }}-500">{{ $fillRate }}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-5 text-right">
|
||||
<button @click="viewSlots({{ json_encode($machine->only(['id', 'name', 'serial_no'])) }})"
|
||||
class="px-4 py-2 rounded-xl text-xs font-black bg-cyan-500/10 text-cyan-500 border border-cyan-500/20 hover:bg-cyan-500 hover:text-white transition-all">
|
||||
{{ __('View Slots') }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="6" class="px-6 py-20 text-center text-slate-400 font-bold">{{ __('No machines found') }}</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">{{ $machines->links('vendor.pagination.luxury') }}</div>
|
||||
</div>
|
||||
|
||||
{{-- Slot Drawer --}}
|
||||
<div x-show="showSlotDrawer" class="fixed inset-0 z-[100] overflow-hidden" x-cloak>
|
||||
<div x-show="showSlotDrawer" x-transition class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm" @click="showSlotDrawer = false"></div>
|
||||
<div x-show="showSlotDrawer" x-transition:enter="transform transition ease-in-out duration-300" x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0" x-transition:leave="transform transition ease-in-out duration-300" x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full"
|
||||
class="fixed inset-y-0 right-0 w-full sm:max-w-lg luxury-card rounded-l-3xl shadow-2xl overflow-y-auto z-10">
|
||||
<div class="p-8">
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h3 class="text-xl font-black text-slate-800 dark:text-white font-display" x-text="selectedMachine?.name"></h3>
|
||||
<p class="text-xs font-mono font-bold text-slate-500 mt-1" x-text="selectedMachine?.serial_no"></p>
|
||||
</div>
|
||||
<button @click="showSlotDrawer = false" class="text-slate-400 hover:text-slate-600 transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div x-show="loading" class="py-20 text-center text-slate-400 font-bold">{{ __('Loading...') }}</div>
|
||||
|
||||
<div x-show="!loading" class="space-y-3">
|
||||
<template x-for="slot in slots" :key="slot.id">
|
||||
<div class="p-4 bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-100 dark:border-slate-800">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-lg bg-slate-200 dark:bg-slate-800 flex items-center justify-center text-xs font-black text-slate-500" x-text="slot.slot_no"></div>
|
||||
<div>
|
||||
<p class="font-extrabold text-slate-800 dark:text-slate-100 text-sm" x-text="slot.product?.name || '{{ __('Empty') }}'"></p>
|
||||
<p class="text-xs font-bold text-slate-400 mt-0.5" x-show="slot.expiry_date" x-text="'{{ __('Expiry') }}: ' + (slot.expiry_date || '')"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-lg font-black" :class="slot.stock <= 0 ? 'text-rose-500' : (slot.stock <= (slot.max_stock * 0.2) ? 'text-amber-500' : 'text-emerald-500')" x-text="slot.stock"></span>
|
||||
<span class="text-xs font-bold text-slate-400">/ <span x-text="slot.max_stock"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div x-show="slots.length === 0" class="py-10 text-center text-slate-400 font-bold">{{ __('No slot data') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
149
resources/views/admin/warehouses/replenishments.blade.php
Normal file
149
resources/views/admin/warehouses/replenishments.blade.php
Normal file
@ -0,0 +1,149 @@
|
||||
@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); },
|
||||
}">
|
||||
{{-- 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">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
|
||||
<span>{{ __('New Replenishment') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Filters --}}
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
<form action="{{ route('admin.warehouses.replenishments') }}" method="GET" class="mb-8">
|
||||
<div class="flex items-center p-1 bg-slate-100/50 dark:bg-slate-900/50 backdrop-blur-md rounded-2xl border border-slate-200/50 dark:border-slate-700/50 w-fit">
|
||||
<a href="{{ route('admin.warehouses.replenishments') }}"
|
||||
class="px-5 py-2 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ !request()->filled('status') ? 'bg-white dark:bg-slate-800 text-cyan-600 shadow-lg shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600' }}">
|
||||
{{ __('All') }}
|
||||
</a>
|
||||
@foreach(['pending' => 'Pending', 'prepared' => 'Prepared', 'delivering' => 'Delivering', 'completed' => 'Completed'] as $key => $label)
|
||||
<a href="{{ route('admin.warehouses.replenishments', ['status' => $key]) }}"
|
||||
class="px-5 py-2 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ request('status') === $key ? 'bg-white dark:bg-slate-800 text-cyan-600 shadow-lg shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600' }}">
|
||||
{{ __($label) }}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</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">{{ __('Order No.') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Machine') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Warehouse') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Items') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Status') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Created By') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||
@forelse($orders as $order)
|
||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||
<td class="px-6 py-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">
|
||||
<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
|
||||
<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>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="7" class="px-6 py-20 text-center text-slate-400 font-bold">{{ __('No replenishment orders found') }}</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">{{ $orders->links('vendor.pagination.luxury') }}</div>
|
||||
</div>
|
||||
|
||||
{{-- 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">
|
||||
@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>
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
173
resources/views/admin/warehouses/transfers.blade.php
Normal file
173
resources/views/admin/warehouses/transfers.blade.php
Normal file
@ -0,0 +1,173 @@
|
||||
@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); },
|
||||
}">
|
||||
{{-- 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">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{{-- 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>
|
||||
</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">{{ __('Order No.') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Type') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('From') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('To') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Items') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Status') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||
@forelse($orders as $order)
|
||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||
<td class="px-6 py-5"><span class="font-mono font-extrabold text-slate-800 dark:text-slate-100">{{ $order->order_no }}</span></td>
|
||||
<td class="px-6 py-5 text-center">
|
||||
@if($order->type === 'warehouse_to_warehouse')
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black bg-indigo-500/10 text-indigo-500 border border-indigo-500/20 tracking-widest uppercase">W2W</span>
|
||||
@else
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black bg-amber-500/10 text-amber-500 border border-amber-500/20 tracking-widest uppercase">M2W</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-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
|
||||
<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">
|
||||
@if($order->status === 'draft')
|
||||
<form action="{{ route('admin.warehouses.transfers.confirm', $order->id) }}" method="POST" class="inline" onsubmit="return confirm('{{ __('Confirm this transfer?') }}')">
|
||||
@csrf @method('PATCH')
|
||||
<button type="submit" class="px-4 py-2 rounded-xl text-xs font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 hover:bg-emerald-500 hover:text-white transition-all">{{ __('Confirm') }}</button>
|
||||
</form>
|
||||
@else
|
||||
<span class="text-xs font-bold text-slate-400">{{ $order->completed_at?->format('Y-m-d H:i') }}</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="7" class="px-6 py-20 text-center text-slate-400 font-bold">{{ __('No transfer orders found') }}</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">{{ $orders->links('vendor.pagination.luxury') }}</div>
|
||||
</div>
|
||||
|
||||
{{-- 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">
|
||||
@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>
|
||||
<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>
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@ -124,16 +124,26 @@
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu" data-sidebar-sub>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.index') }}">{{ __('Warehouse List (All)') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.personal') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.personal') }}">{{ __('Warehouse List (Individual)') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.stock-management') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.stock-management') }}">{{ __('Stock Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.transfers') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.transfers') }}">{{ __('Transfers') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.purchases') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.purchases') }}">{{ __('Purchases') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.replenishments') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.replenishments') }}">{{ __('Replenishments') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.replenishment-records') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.replenishment-records') }}">{{ __('Replenishment Records') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.machine-stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.machine-stock') }}">{{ __('Stock & Expiry') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.staff-stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.staff-stock') }}">{{ __('Staff Stock') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.returns') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.returns') }}">{{ __('Returns') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.index') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /></svg>
|
||||
{{ __('Warehouse Overview') }}
|
||||
</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.inventory') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.inventory') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg>
|
||||
{{ __('Inventory Management') }}
|
||||
</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.transfers') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.transfers') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" /></svg>
|
||||
{{ __('Transfer Orders') }}
|
||||
</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.replenishments') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.replenishments') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="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>
|
||||
{{ __('Machine Replenishment') }}
|
||||
</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.machine-inventory') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.machine-inventory') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
|
||||
{{ __('Machine Inventory Overview') }}
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
$currentLimit = $paginator->perPage();
|
||||
$limits = [10, 25, 50, 100];
|
||||
@endphp
|
||||
<select onchange="const params = new URLSearchParams(window.location.search); params.set('per_page', this.value); params.delete('page'); window.location.href = window.location.pathname + '?' + params.toString();"
|
||||
<select onchange="{ const params = new URLSearchParams(window.location.search); params.set('per_page', this.value); params.delete('page'); const url = window.location.pathname + '?' + params.toString(); const evt = new CustomEvent('ajax:navigate', { detail: { url: url }, bubbles: true, cancelable: true }); if (this.dispatchEvent(evt)) { window.location.href = url; } }"
|
||||
class="h-7 pl-2 pr-7 rounded-lg bg-white dark:bg-slate-800 border-none text-[11px] font-black text-slate-600 dark:text-slate-300 appearance-none focus:ring-4 focus:ring-cyan-500/10 outline-none transition-all cursor-pointer shadow-sm leading-none py-0 !bg-none">
|
||||
@foreach($limits as $l)
|
||||
<option value="{{ $l }}" {{ $currentLimit == $l ? 'selected' : '' }}>{{ $l }}</option>
|
||||
@ -51,7 +51,7 @@
|
||||
|
||||
{{-- Unified Quick Jump Selection (Desktop & Mobile) --}}
|
||||
<div class="relative group">
|
||||
<select onchange="window.location.href = this.value"
|
||||
<select onchange="{ const url = this.value; const evt = new CustomEvent('ajax:navigate', { detail: { url: url }, bubbles: true, cancelable: true }); if (this.dispatchEvent(evt)) { window.location.href = url; } }"
|
||||
class="h-9 pl-4 pr-10 rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 text-[11px] sm:text-xs font-black text-slate-600 dark:text-slate-300 appearance-none focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 outline-none transition-all cursor-pointer shadow-sm hover:border-slate-300 dark:hover:border-slate-600 !bg-none {{ $paginator->lastPage() <= 1 ? 'opacity-50 cursor-not-allowed pointer-events-none' : '' }}"
|
||||
{{ $paginator->lastPage() <= 1 ? 'disabled' : '' }}>
|
||||
@for ($i = 1; $i <= $paginator->lastPage(); $i++)
|
||||
|
||||
@ -70,17 +70,31 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
|
||||
|
||||
// 5. 倉庫管理
|
||||
Route::prefix('warehouses')->name('warehouses.')->group(function () {
|
||||
// 模組 1:倉庫總覽
|
||||
Route::get('/', [App\Http\Controllers\Admin\WarehouseController::class, 'index'])->name('index');
|
||||
Route::get('/personal', [App\Http\Controllers\Admin\WarehouseController::class, 'personal'])->name('personal');
|
||||
Route::get('/stock-management', [App\Http\Controllers\Admin\WarehouseController::class, 'stockManagement'])->name('stock-management');
|
||||
Route::post('/', [App\Http\Controllers\Admin\WarehouseController::class, 'store'])->name('store');
|
||||
Route::put('/{warehouse}', [App\Http\Controllers\Admin\WarehouseController::class, 'update'])->name('update');
|
||||
Route::delete('/{warehouse}', [App\Http\Controllers\Admin\WarehouseController::class, 'destroy'])->name('destroy');
|
||||
Route::patch('/{warehouse}/toggle-status', [App\Http\Controllers\Admin\WarehouseController::class, 'toggleStatus'])->name('toggle-status');
|
||||
|
||||
// 模組 2:庫存管理
|
||||
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');
|
||||
|
||||
// 模組 3:調撥單
|
||||
Route::get('/transfers', [App\Http\Controllers\Admin\WarehouseController::class, 'transfers'])->name('transfers');
|
||||
Route::get('/purchases', [App\Http\Controllers\Admin\WarehouseController::class, 'purchases'])->name('purchases');
|
||||
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');
|
||||
|
||||
// 模組 4:機台庫存總覽
|
||||
Route::get('/machine-inventory', [App\Http\Controllers\Admin\WarehouseController::class, 'machineInventory'])->name('machine-inventory');
|
||||
Route::get('/machine-inventory/{machine}/slots', [App\Http\Controllers\Admin\WarehouseController::class, 'machineSlots'])->name('machine-inventory.slots');
|
||||
|
||||
// 模組 5:機台補貨
|
||||
Route::get('/replenishments', [App\Http\Controllers\Admin\WarehouseController::class, 'replenishments'])->name('replenishments');
|
||||
Route::get('/replenishment-records', [App\Http\Controllers\Admin\WarehouseController::class, 'replenishmentRecords'])->name('replenishment-records');
|
||||
Route::get('/replenishment-records-all', [App\Http\Controllers\Admin\WarehouseController::class, 'replenishmentRecordsAll'])->name('replenishment-records-all');
|
||||
Route::get('/machine-stock', [App\Http\Controllers\Admin\WarehouseController::class, 'machineStock'])->name('machine-stock');
|
||||
Route::get('/staff-stock', [App\Http\Controllers\Admin\WarehouseController::class, 'staffStock'])->name('staff-stock');
|
||||
Route::get('/returns', [App\Http\Controllers\Admin\WarehouseController::class, 'returns'])->name('returns');
|
||||
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');
|
||||
});
|
||||
|
||||
// 6. 銷售管理
|
||||
@ -204,6 +218,7 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
|
||||
// 15. 權限設定
|
||||
Route::prefix('permission')->name('permission.')->group(function () {
|
||||
Route::patch('companies/{company}/toggle-status', [App\Http\Controllers\Admin\CompanyController::class, 'toggleStatus'])->name('companies.status.toggle')->middleware('can:menu.permissions.companies');
|
||||
Route::put('companies/{company}/settings', [App\Http\Controllers\Admin\CompanyController::class, 'updateSettings'])->name('companies.settings.update')->middleware('can:menu.permissions.companies');
|
||||
Route::resource('companies', App\Http\Controllers\Admin\CompanyController::class)->except(['show', 'create', 'edit'])->middleware('can:menu.permissions.companies');
|
||||
Route::get('/accounts', [App\Http\Controllers\Admin\PermissionController::class, 'accounts'])->name('accounts')->middleware('can:menu.permissions.accounts');
|
||||
Route::patch('/accounts/{id}/toggle-status', [App\Http\Controllers\Admin\PermissionController::class, 'toggleAccountStatus'])->name('accounts.status.toggle')->middleware('can:menu.permissions.accounts');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user