[FEAT] 優化通行碼與取貨碼模組、補齊多國語系系統及介面風格調整

1. 通行碼與取貨碼:新增代碼預覽、重新產生功能與到期時間預覽邏輯。
2. 通行碼:允許天數設為 0 (永久),並改用搜尋式下拉選單選取機台。
3. 取貨碼:加強表單檢核並整合自訂 Toast 提示。
4. 語系優化:補齊繁中、英文鍵值,並完整建立日文語系目錄 (lang/ja) 與 JSON 檔案。
5. 組件優化:更新 PageHeader 與 TabNav 組件,提升 UI 互動性與一致性。
6. 訪客功能:新增通行碼訪客端查詢介面與控制器。
7. 其他模組:同步調整機台、遠端與倉庫模組視圖以符合極簡奢華風。
This commit is contained in:
sky121113 2026-04-29 16:54:21 +08:00
parent 7356304d6f
commit b42695c4b8
33 changed files with 6992 additions and 3771 deletions

View File

@ -33,12 +33,12 @@ class SalesController extends Controller
$query = PickupCode::with(['machine.slots.product', 'creator'])->latest();
if ($request->search) {
$query->where(function($q) use ($request) {
$query->where(function ($q) use ($request) {
$q->where('code', 'like', "%{$request->search}%")
->orWhereHas('machine', function($mq) use ($request) {
$mq->where('name', 'like', "%{$request->search}%")
->orWhere('serial_no', 'like', "%{$request->search}%");
});
->orWhereHas('machine', function ($mq) use ($request) {
$mq->where('name', 'like', "%{$request->search}%")
->orWhere('serial_no', 'like', "%{$request->search}%");
});
});
}
@ -69,6 +69,7 @@ class SalesController extends Controller
'machine_id' => 'required|exists:machines,id',
'slot_no' => 'required|string',
'expires_hours' => 'nullable|integer|min:1|max:720', // 最長一個月
'custom_code' => 'nullable|string|min:4|max:12',
]);
$machine = Machine::findOrFail($validated['machine_id']);
@ -77,7 +78,7 @@ class SalesController extends Controller
$pickupCode = PickupCode::create([
'machine_id' => $validated['machine_id'],
'slot_no' => $validated['slot_no'],
'code' => PickupCode::generateUniqueCode($validated['machine_id']),
'code' => $request->custom_code ?? PickupCode::generateUniqueCode($validated['machine_id']),
'expires_at' => $expiresAt,
'status' => 'active',
'company_id' => $machine->company_id,
@ -136,16 +137,33 @@ class SalesController extends Controller
$query = PassCode::with(['machine', 'creator'])->latest();
if ($request->search) {
$query->where(function($q) use ($request) {
$query->where(function ($q) use ($request) {
$q->where('code', 'like', "%{$request->search}%")
->orWhere('name', 'like', "%{$request->search}%")
->orWhereHas('machine', function($mq) use ($request) {
$mq->where('name', 'like', "%{$request->search}%")
->orWhere('serial_no', 'like', "%{$request->search}%");
});
->orWhere('name', 'like', "%{$request->search}%")
->orWhereHas('machine', function ($mq) use ($request) {
$mq->where('name', 'like', "%{$request->search}%")
->orWhere('serial_no', 'like', "%{$request->search}%");
});
});
}
if ($request->status && trim($request->status) !== '') {
$status = trim($request->status);
if ($status === 'active') {
$query->where('status', 'active')
->where(function ($q) {
$q->whereNull('expires_at')
->orWhere('expires_at', '>', now());
});
} elseif ($status === 'expired') {
$query->where('status', 'active')
->whereNotNull('expires_at')
->where('expires_at', '<=', now());
} else {
$query->where('status', $status);
}
}
$passCodes = $query->paginate(15)->withQueryString();
$machines = Machine::all();
@ -164,9 +182,9 @@ class SalesController extends Controller
{
$validated = $request->validate([
'machine_id' => 'required|exists:machines,id',
'name' => 'nullable|string|max:50',
'expires_days' => 'nullable|integer|min:1',
'custom_code' => 'nullable|string|min:4|max:12',
'name' => 'required|string|max:50',
'expires_days' => 'nullable|integer|min:0',
'custom_code' => 'required|string|min:4|max:12',
]);
$machine = Machine::findOrFail($validated['machine_id']);
@ -202,12 +220,12 @@ class SalesController extends Controller
}
/**
* 刪除通行碼
* 刪除通行碼 (改為停用)
*/
public function destroyPassCode(PassCode $passCode)
{
$passCode->delete();
return back()->with('success', __('Pass code deleted.'));
$passCode->update(['status' => 'disabled']);
return back()->with('success', __('Pass code disabled.'));
}
// 來店禮設定

View File

@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Guest;
use App\Http\Controllers\Controller;
use App\Models\Transaction\PassCode;
use Illuminate\Http\Request;
class PassCodeController extends Controller
{
/**
* 顯示公開通行憑證頁面
*/
public function show($slug)
{
$passCode = PassCode::with(['machine'])
->where('slug', $slug)
->where('status', 'active')
->where(function($query) {
$query->whereNull('expires_at')
->orWhere('expires_at', '>', now());
})
->firstOrFail();
return view('guest.pass-code.show', compact('passCode'));
}
}

View File

@ -17,6 +17,7 @@ class PassCode extends Model
'machine_id',
'name',
'code',
'slug',
'expires_at',
'status',
'created_by',
@ -26,6 +27,26 @@ class PassCode extends Model
'expires_at' => 'datetime',
];
/**
* 獲取公開通行連結
*/
public function getTicketUrlAttribute()
{
return $this->slug ? route('pass-code.ticket', $this->slug) : null;
}
/**
* Boot the model.
*/
protected static function booted()
{
static::creating(function ($passCode) {
if (!$passCode->slug) {
$passCode->slug = \Illuminate\Support\Str::random(16);
}
});
}
/**
* 關聯機台
*/

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('pass_codes', function (Blueprint $table) {
$table->string('slug')->nullable()->unique()->after('code');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('pass_codes', function (Blueprint $table) {
$table->dropColumn('slug');
});
}
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

20
lang/ja/auth.php Normal file
View File

@ -0,0 +1,20 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => '認証情報が一致しません。',
'password' => '入力されたパスワードが正しくありません。',
'throttle' => 'ログイン試行回数が多すぎます。:seconds 秒後に再試行してください。',
];

19
lang/ja/pagination.php Normal file
View File

@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '&laquo; 前へ',
'next' => '次へ &raquo;',
];

22
lang/ja/passwords.php Normal file
View File

@ -0,0 +1,22 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Password Reset Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| outcome such as failure due to an invalid password / reset token.
|
*/
'reset' => 'パスワードがリセットされました。',
'sent' => 'パスワードリセットリンクをメールで送信しました。',
'throttled' => '再試行するまでしばらくお待ちください。',
'token' => 'このパスワードリセットトークンは無効です。',
'user' => 'そのメールアドレスのユーザーが見つかりません。',
];

213
lang/ja/validation.php Normal file
View File

@ -0,0 +1,213 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
'accepted' => ':attribute を承認する必要があります。',
'accepted_if' => ':other が :value の場合、:attribute を承認する必要があります。',
'active_url' => ':attribute は有効な URL である必要があります。',
'after' => ':attribute は :date より後の日付である必要があります。',
'after_or_equal' => ':attribute は :date 以降の日付である必要があります。',
'alpha' => ':attribute は英字のみで構成される必要があります。',
'alpha_dash' => ':attribute は英字、数字、ダッシュ、アンダースコアのみで構成される必要があります。',
'alpha_num' => ':attribute は英字と数字のみで構成される必要があります。',
'any_of' => ':attribute が無効です。',
'array' => ':attribute は配列である必要があります。',
'ascii' => ':attribute はシングルバイトの英数字と記号のみで構成される必要があります。',
'before' => ':attribute は :date より前の日付である必要があります。',
'before_or_equal' => ':attribute は :date 以前の日付である必要があります。',
'between' => [
'array' => ':attribute は :min 〜 :max 個の要素を含む必要があります。',
'file' => ':attribute は :min 〜 :max キロバイトの間である必要があります。',
'numeric' => ':attribute は :min 〜 :max の間である必要があります。',
'string' => ':attribute は :min 〜 :max 文字の間である必要があります。',
],
'boolean' => ':attribute は true か false である必要があります。',
'can' => ':attribute に無効な値が含まれています。',
'confirmed' => ':attribute の確認が一致しません。',
'contains' => ':attribute に必要な値が含まれていません。',
'current_password' => 'パスワードが正しくありません。',
'date' => ':attribute は有効な日付である必要があります。',
'date_equals' => ':attribute は :date と同じ日付である必要があります。',
'date_format' => ':attribute は :format 形式と一致する必要があります。',
'decimal' => ':attribute は小数点以下 :decimal 桁である必要があります。',
'declined' => ':attribute を拒否する必要があります。',
'declined_if' => ':other が :value の場合、:attribute を拒否する必要があります。',
'different' => ':attribute と :other は異なる必要があります。',
'digits' => ':attribute は :digits 桁である必要があります。',
'digits_between' => ':attribute は :min 〜 :max 桁の間である必要があります。',
'dimensions' => ':attribute の画像サイズが無効です。',
'distinct' => ':attribute に重複する値が含まれています。',
'doesnt_contain' => ':attribute には次のいずれも含めることはできません: :values。',
'doesnt_end_with' => ':attribute は次のいずれかで終わることはできません: :values。',
'doesnt_start_with' => ':attribute は次のいずれかで始まることはできません: :values。',
'email' => ':attribute は有効なメールアドレスである必要があります。',
'encoding' => ':attribute は :encoding でエンコードされている必要があります。',
'ends_with' => ':attribute は次のいずれかで終わる必要があります: :values。',
'enum' => '選択された :attribute は無効です。',
'exists' => '選択された :attribute は無効です。',
'extensions' => ':attribute は次のいずれかの拡張子である必要があります: :values。',
'file' => ':attribute はファイルである必要があります。',
'filled' => ':attribute は値を持つ必要があります。',
'gt' => [
'array' => ':attribute は :value 個より多い要素を含む必要があります。',
'file' => ':attribute は :value キロバイトより大きい必要があります。',
'numeric' => ':attribute は :value より大きい必要があります。',
'string' => ':attribute は :value 文字より多い必要があります。',
],
'gte' => [
'array' => ':attribute は :value 個以上の要素を含む必要があります。',
'file' => ':attribute は :value キロバイト以上である必要があります。',
'numeric' => ':attribute は :value 以上である必要があります。',
'string' => ':attribute は :value 文字以上である必要があります。',
],
'hex_color' => ':attribute は有効な16進数カラーコードである必要があります。',
'image' => ':attribute は画像である必要があります。',
'in' => '選択された :attribute は無効です。',
'in_array' => ':attribute は :other に存在する必要があります。',
'in_array_keys' => ':attribute には次のキーのいずれかを含む必要があります: :values。',
'integer' => ':attribute は整数である必要があります。',
'ip' => ':attribute は有効な IP アドレスである必要があります。',
'ipv4' => ':attribute は有効な IPv4 アドレスである必要があります。',
'ipv6' => ':attribute は有効な IPv6 アドレスである必要があります。',
'json' => ':attribute は有効な JSON 文字列である必要があります。',
'list' => ':attribute はリストである必要があります。',
'lowercase' => ':attribute は小文字である必要があります。',
'lt' => [
'array' => ':attribute は :value 個未満の要素を含む必要があります。',
'file' => ':attribute は :value キロバイト未満である必要があります。',
'numeric' => ':attribute は :value 未満である必要があります。',
'string' => ':attribute は :value 文字未満である必要があります。',
],
'lte' => [
'array' => ':attribute は :value 個以下の要素を含む必要があります。',
'file' => ':attribute は :value キロバイト以下である必要があります。',
'numeric' => ':attribute は :value 以下である必要があります。',
'string' => ':attribute は :value 文字以下である必要があります。',
],
'mac_address' => ':attribute は有効な MAC アドレスである必要があります。',
'max' => [
'array' => ':attribute は :max 個を超える要素を含むことはできません。',
'file' => ':attribute は :max キロバイトを超えることはできません。',
'numeric' => ':attribute は :max を超えることはできません。',
'string' => ':attribute は :max 文字を超えることはできません。',
],
'max_digits' => ':attribute は :max 桁を超えることはできません。',
'mimes' => ':attribute は次のいずれかのファイルタイプである必要があります: :values。',
'mimetypes' => ':attribute は次のいずれかのファイルタイプである必要があります: :values。',
'min' => [
'array' => ':attribute は少なくとも :min 個の要素を含む必要があります。',
'file' => ':attribute は少なくとも :min キロバイトである必要があります。',
'numeric' => ':attribute は少なくとも :min である必要があります。',
'string' => ':attribute は少なくとも :min 文字である必要があります。',
],
'min_digits' => ':attribute は少なくとも :min 桁である必要があります。',
'missing' => ':attribute は存在してはいけません。',
'missing_if' => ':other が :value の場合、:attribute は存在してはいけません。',
'missing_unless' => ':other が :value でない限り、:attribute は存在してはいけません。',
'missing_with' => ':values が存在する場合、:attribute は存在してはいけません。',
'missing_with_all' => ':values がすべて存在する場合、:attribute は存在してはいけません。',
'multiple_of' => ':attribute は :value の倍数である必要があります。',
'not_in' => '選択された :attribute は無効です。',
'not_regex' => ':attribute の形式が無効です。',
'numeric' => ':attribute は数値である必要があります。',
'password' => [
'letters' => ':attribute には少なくとも1つの文字を含む必要があります。',
'mixed' => ':attribute には少なくとも1つの大文字と1つの小文字を含む必要があります。',
'numbers' => ':attribute には少なくとも1つの数字を含む必要があります。',
'symbols' => ':attribute には少なくとも1つの記号を含む必要があります。',
'uncompromised' => '指定された :attribute はデータ漏洩に含まれています。別の :attribute を選択してください。',
],
'present' => ':attribute が存在する必要があります。',
'present_if' => ':other が :value の場合、:attribute が存在する必要があります。',
'present_unless' => ':other が :value でない限り、:attribute が存在する必要があります。',
'present_with' => ':values が存在する場合、:attribute が存在する必要があります。',
'present_with_all' => ':values がすべて存在する場合、:attribute が存在する必要があります。',
'prohibited' => ':attribute は禁止されています。',
'prohibited_if' => ':other が :value の場合、:attribute は禁止されています。',
'prohibited_if_accepted' => ':other が承認された場合、:attribute は禁止されています。',
'prohibited_if_declined' => ':other が拒否された場合、:attribute は禁止されています。',
'prohibited_unless' => ':other が :values にない限り、:attribute は禁止されています。',
'prohibits' => ':attribute は :other の存在を禁止します。',
'regex' => ':attribute の形式が無効です。',
'required' => ':attribute は必須です。',
'required_array_keys' => ':attribute には次のエントリが含まれる必要があります: :values。',
'required_if' => ':other が :value の場合、:attribute は必須です。',
'required_if_accepted' => ':other が承認された場合、:attribute は必須です。',
'required_if_declined' => ':other が拒否された場合、:attribute は必須です。',
'required_unless' => ':other が :values にない限り、:attribute は必須です。',
'required_with' => ':values が存在する場合、:attribute は必須です。',
'required_with_all' => ':values がすべて存在する場合、:attribute は必須です。',
'required_without' => ':values が存在しない場合、:attribute は必須です。',
'required_without_all' => ':values がすべて存在しない場合、:attribute は必須です。',
'same' => ':attribute は :other と一致する必要があります。',
'size' => [
'array' => ':attribute は :size 個の要素を含む必要があります。',
'file' => ':attribute は :size キロバイトである必要があります。',
'numeric' => ':attribute は :size である必要があります。',
'string' => ':attribute は :size 文字である必要があります。',
],
'starts_with' => ':attribute は次のいずれかで始まる必要があります: :values。',
'string' => ':attribute は文字列である必要があります。',
'timezone' => ':attribute は有効なタイムゾーンである必要があります。',
'unique' => ':attribute は既に使用されています。',
'uploaded' => ':attribute のアップロードに失敗しました。',
'uppercase' => ':attribute は大文字である必要があります。',
'url' => ':attribute は有効な URL である必要があります。',
'ulid' => ':attribute は有効な ULID である必要があります。',
'uuid' => ':attribute は有効な UUID である必要があります。',
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'is_confirmed' => [
'accepted' => '顧客への通知と署名取得を確認するチェックボックスにチェックを入れてください。',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap our attribute placeholder
| with something more reader friendly such as "E-Mail Address" instead
| of "email". This simply helps us make our message more expressive.
|
*/
'attributes' => [
'username' => 'ユーザー名',
'name' => '名前',
'email' => 'メールアドレス',
'password' => 'パスワード',
'current_password' => '現在のパスワード',
'password_confirmation' => 'パスワード確認',
'phone' => '電話番号',
'machine_id' => '機台',
'category' => 'カテゴリ',
'maintenance_at' => 'メンテナンス日',
'content' => 'メンテナンス内容',
'is_confirmed' => '確認チェックボックス',
],
];

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,169 +1,240 @@
{{-- Machines Tab Content (Partial) --}}
{{-- 此檔案被 index.blade.php @include AJAX 模式共用 --}}
{{-- Toolbar 區:搜尋框 --}}
<div class="flex items-center justify-between mb-8">
<div class="flex items-center gap-4">
<form @submit.prevent="searchInTab('machines')" 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" x-model="machineSearch"
placeholder="{{ __('Search machines...') }}"
class="luxury-input py-2.5 pl-12 pr-6 block w-64">
</form>
{{-- Toolbar 區:搜尋框 + 按鈕 (同列) --}}
<div class="flex flex-wrap items-center gap-3 sm:gap-4 mb-8">
{{-- 搜尋輸入框 --}}
<div class="relative group flex-1 sm:flex-none">
<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" x-model="machineSearch"
@keydown.enter.prevent="searchInTab('machines')"
placeholder="{{ __('Search machines...') }}"
class="luxury-input py-2.5 pl-11 pr-4 block w-full sm:w-72 text-sm font-bold">
</div>
<div class="flex items-center gap-2 ml-auto sm:ml-0">
{{-- 搜尋按鈕 --}}
<button type="button" @click="searchInTab('machines')"
class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 transition-all active:scale-95"
title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
{{-- 重置按鈕 --}}
<button type="button"
@click="machineSearch = ''; searchInTab('machines')"
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all active:scale-95"
title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
</div>
{{-- Machine Table --}}
<div class="overflow-x-auto">
{{-- ── 桌面表格 (xl+) ── --}}
<div class="hidden xl:block overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
<th
class="px-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">
<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">
{{ __('Machine 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">
<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">
{{ __('Machine Model') }}</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">
<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">
{{ __('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">
<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">
{{ __('Card Reader') }}</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">
<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">
{{ __('Owner') }}</th>
<th
class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800 text-right">
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800 text-right">
{{ __('Action') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
@forelse($machines as $machine)
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
@php $status = $machine->calculated_status; @endphp
<tr class="group hover:bg-slate-50/50 dark:hover:bg-white/[0.02] transition-colors duration-200">
<td class="px-6 py-6 cursor-pointer" @click='openDetail({{ $machine->toJson() }}, {{ $machine->id }}, "{{ $machine->serial_no }}")'>
<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 group-hover:border-cyan-500 shadow-sm group-hover:shadow-cyan-500/50 transition-all duration-300 overflow-hidden">
<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 overflow-hidden">
@if(isset($machine->image_urls[0]))
<img src="{{ $machine->image_urls[0] }}" class="w-full h-full object-cover">
@else
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
<svg 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="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
@endif
</div>
<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">
<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 tracking-tight">
{{ $machine->name }}</div>
<div class="flex items-center gap-2 mt-0.5">
<span
class="text-xs font-mono font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{
$machine->serial_no }}</span>
</div>
<p class="text-xs font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ $machine->serial_no }}</p>
</div>
</div>
</td>
<td class="px-6 py-6 cursor-pointer" @click='openDetail({{ $machine->toJson() }}, {{ $machine->id }}, "{{ $machine->serial_no }}")'>
<span
class="text-xs font-bold text-slate-600 dark:text-slate-300 uppercase tracking-widest">
<span class="text-xs font-bold text-slate-600 dark:text-slate-300 uppercase tracking-widest">
{{ $machine->machineModel->name ?? '--' }}
</span>
</td>
<td class="px-6 py-6">
@php
$status = $machine->calculated_status;
@endphp
<div class="flex items-center gap-2.5">
<div class="relative flex h-2.5 w-2.5">
@if($status === 'online')
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500"></span>
@elseif($status === 'error')
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-amber-500"></span>
@else
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-slate-300 dark:bg-slate-600"></span>
@endif
</div>
<span class="text-xs font-bold uppercase tracking-wider {{
$status === 'online' ? 'text-emerald-500' : ($status === 'error' ? 'text-amber-500' : 'text-slate-500 dark:text-slate-400')
}}">
{{ __($status === 'online' ? 'Online' : ($status === 'error' ? 'Error' : 'Offline')) }}
</span>
<x-status-badge :status="$status" size="sm" />
</td>
<td class="px-6 py-6">
<div class="text-sm font-bold text-slate-700 dark:text-slate-200">
{{ $machine->card_reader_seconds ?? 0 }}s <span class="text-slate-300 dark:text-slate-700 mx-1.5">/</span> <span class="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase tracking-widest">No.{{ $machine->card_reader_no ?? '--' }}</span>
</div>
</td>
<td class="px-6 py-6">
<div class="text-sm font-bold text-slate-700 dark:text-slate-200">
{{ $machine->card_reader_seconds ?? 0 }}s <span
class="text-slate-300 dark:text-slate-700 mx-1.5">/</span> <span
class="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase tracking-widest">No.{{
$machine->card_reader_no ?? '--' }}</span>
</div>
</td>
<td class="px-6 py-6">
<span
class="px-2.5 py-1 rounded-lg text-xs font-bold border border-sky-100 dark:border-sky-900/30 bg-sky-50 dark:bg-sky-900/20 text-sky-600 dark:text-sky-400 tracking-widest">
{{ $machine->company->name ?? __('System') }}
</span>
</td>
<td class="px-6 py-6 text-right flex items-center justify-end gap-2">
<button @click="openMaintenanceQr(@js($machine->only(['name', 'serial_no'])))"
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 dark:hover:bg-emerald-500/10 border border-transparent hover:border-emerald-500/20 transition-all inline-flex group/btn"
title="{{ __('Maintenance QR Code') }}">
<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="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 6.75h.75v.75h-.75v-.75zM6.75 16.5h.75v.75h-.75v-.75zM16.5 6.75h.75v.75h-.75v-.75zM13.5 13.5h.75v.75h-.75v-.75zM13.5 19.5h.75v.75h-.75v-.75zM19.5 13.5h.75v.75h-.75v-.75zM19.5 19.5h.75v.75h-.75v-.75zM16.5 16.5h.75v.75h-.75v-.75z" />
</svg>
</button>
<button @click="openPhotoModal(@js($machine->only(['id', 'name', 'image_urls'])))"
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all inline-flex group/btn"
title="{{ __('Machine Images') }}">
<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.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>
</button>
<a href="{{ route('admin.basic-settings.machines.edit', $machine) }}"
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all inline-flex group/btn"
title="{{ __('Edit Settings') }}">
<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
@click="openDetail(@js($machine->only(['name', 'serial_no', 'status', 'location', 'last_heartbeat_at', 'card_reader_no', 'card_reader_seconds', 'firmware_version', 'api_token', 'heating_start_time', 'heating_end_time', 'image_urls'])))"
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all inline-flex group/btn"
title="{{ __('View Details') }}">
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
</button>
</td>
</tr>
@empty
<tr>
<td colspan="5"
class="px-6 py-20 text-center text-slate-500 dark:text-slate-400 font-bold tracking-widest uppercase">
{{ __('No data available') }}
</td>
</tr>
@endforelse
</tbody>
<td class="px-6 py-6">
<span class="px-2.5 py-1 rounded-lg text-xs font-bold border border-sky-100 dark:border-sky-900/30 bg-sky-50 dark:bg-sky-900/20 text-sky-600 dark:text-sky-400 tracking-widest">
{{ $machine->company->name ?? __('System') }}
</span>
</td>
<td class="px-6 py-6 text-right">
<div class="flex items-center justify-end gap-2">
<button @click="openMaintenanceQr(@js($machine->only(['name', 'serial_no'])))"
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 dark:hover:bg-emerald-500/10 border border-transparent hover:border-emerald-500/20 transition-all"
title="{{ __('Maintenance QR Code') }}">
<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="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 6.75h.75v.75h-.75v-.75zM6.75 16.5h.75v.75h-.75v-.75zM16.5 6.75h.75v.75h-.75v-.75zM13.5 13.5h.75v.75h-.75v-.75zM13.5 19.5h.75v.75h-.75v-.75zM19.5 13.5h.75v.75h-.75v-.75zM19.5 19.5h.75v.75h-.75v-.75zM16.5 16.5h.75v.75h-.75v-.75z" />
</svg>
</button>
<button @click="openPhotoModal(@js($machine->only(['id', 'name', 'image_urls'])))"
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all"
title="{{ __('Machine Images') }}">
<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.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>
</button>
<a href="{{ route('admin.basic-settings.machines.edit', $machine) }}"
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all"
title="{{ __('Edit Settings') }}">
<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
@click="openDetail(@js($machine->only(['name', 'serial_no', 'status', 'location', 'last_heartbeat_at', 'card_reader_no', 'card_reader_seconds', 'firmware_version', 'api_token', 'heating_start_time', 'heating_end_time', 'image_urls'])))"
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all"
title="{{ __('View Details') }}">
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
</button>
</div>
</td>
</tr>
@empty
<x-empty-state mode="table" :colspan="6" :message="__('No data available')" />
@endforelse
</tbody>
</table>
</div>
{{-- ── Mobile / Tablet Card Grid (< xl) ── --}}
<div class="xl:hidden grid grid-cols-1 md:grid-cols-2 gap-4">
@forelse($machines as $machine)
@php $status = $machine->calculated_status; @endphp
<div class="luxury-card p-5 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group">
{{-- Card Header --}}
<div class="flex items-start justify-between gap-4 mb-5">
<div class="flex items-center gap-3 min-w-0">
<div class="w-14 h-14 rounded-2xl 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 overflow-hidden shadow-sm shrink-0"
@click='openDetail({{ $machine->toJson() }}, {{ $machine->id }}, "{{ $machine->serial_no }}")'>
@if(isset($machine->image_urls[0]))
<img src="{{ $machine->image_urls[0] }}" class="w-full h-full object-cover">
@else
<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="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
@endif
</div>
<div class="min-w-0 cursor-pointer" @click='openDetail({{ $machine->toJson() }}, {{ $machine->id }}, "{{ $machine->serial_no }}")'>
<h3 class="text-base font-extrabold text-slate-800 dark:text-slate-100 truncate hover:text-cyan-600 dark:hover:text-cyan-400 transition-colors tracking-tight">
{{ $machine->name }}
</h3>
<p class="text-xs font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest truncate">
{{ $machine->serial_no }}
</p>
</div>
</div>
<x-status-badge :status="$status" size="sm" />
</div>
{{-- Info Grid --}}
<div class="grid grid-cols-2 gap-y-4 mb-5 border-y border-slate-100 dark:border-slate-800/50 py-4">
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Machine Model') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300">{{ $machine->machineModel->name ?? '--' }}</p>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Owner') }}</p>
<p class="text-sm font-bold text-sky-600 dark:text-sky-400">{{ $machine->company->name ?? __('System') }}</p>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Card Reader') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 font-mono">{{ $machine->card_reader_seconds ?? 0 }}s / No.{{ $machine->card_reader_no ?? '--' }}</p>
</div>
</div>
{{-- Action Buttons --}}
<div class="flex items-center gap-2">
<button @click="openMaintenanceQr(@js($machine->only(['name', 'serial_no'])))"
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-500 dark:text-slate-400 font-bold hover:bg-emerald-500 hover:text-white transition-all duration-300 border border-slate-200/50 dark:border-slate-700/50"
title="{{ __('Maintenance QR Code') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 6.75h.75v.75h-.75v-.75zM6.75 16.5h.75v.75h-.75v-.75zM16.5 6.75h.75v.75h-.75v-.75zM13.5 13.5h.75v.75h-.75v-.75zM13.5 19.5h.75v.75h-.75v-.75zM19.5 13.5h.75v.75h-.75v-.75zM19.5 19.5h.75v.75h-.75v-.75zM16.5 16.5h.75v.75h-.75v-.75z" />
</svg>
</button>
<button @click="openPhotoModal(@js($machine->only(['id', 'name', 'image_urls'])))"
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-500 dark:text-slate-400 font-bold hover:bg-cyan-500 hover:text-white transition-all duration-300 border border-slate-200/50 dark:border-slate-700/50"
title="{{ __('Machine Images') }}">
<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="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>
</button>
<a href="{{ route('admin.basic-settings.machines.edit', $machine) }}"
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-500 dark:text-slate-400 font-bold hover:bg-cyan-500 hover:text-white transition-all duration-300 border border-slate-200/50 dark:border-slate-700/50"
title="{{ __('Edit') }}">
<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.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
@click="openDetail(@js($machine->only(['name', 'serial_no', 'status', 'location', 'last_heartbeat_at', 'card_reader_no', 'card_reader_seconds', 'firmware_version', 'api_token', 'heating_start_time', 'heating_end_time', 'image_urls'])))"
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-500 dark:text-slate-400 font-bold hover:bg-cyan-500 hover:text-white transition-all duration-300 border border-slate-200/50 dark:border-slate-700/50"
title="{{ __('Detail') }}">
<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="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
</button>
</div>
</div>
@empty
<div class="col-span-full">
<x-empty-state :message="__('No data available')" />
</div>
@endforelse
</div>
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
{{ $machines->appends(['tab' => 'machines'])->links('vendor.pagination.luxury') }}
</div>

View File

@ -1,85 +1,95 @@
{{-- Models Tab Content (Partial) --}}
{{-- 此檔案被 index.blade.php @include AJAX 模式共用 --}}
{{-- Toolbar 區:搜尋框 --}}
<div class="flex items-center justify-between mb-8">
<div class="flex items-center gap-4">
<form @submit.prevent="searchInTab('models')" 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" x-model="modelSearch"
placeholder="{{ __('Search models...') }}"
class="luxury-input py-2.5 pl-12 pr-6 block w-64">
</form>
{{-- Toolbar 區:搜尋框 + 按鈕 (同列) --}}
<div class="flex flex-wrap items-center gap-3 sm:gap-4 mb-8">
{{-- 搜尋輸入框 --}}
<div class="relative group flex-1 sm:flex-none">
<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" x-model="modelSearch"
@keydown.enter.prevent="searchInTab('models')"
placeholder="{{ __('Search models...') }}"
class="luxury-input py-2.5 pl-11 pr-4 block w-full sm:w-72 text-sm font-bold">
</div>
<div class="flex items-center gap-2 ml-auto sm:ml-0">
{{-- 搜尋按鈕 --}}
<button type="button" @click="searchInTab('models')"
class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 transition-all active:scale-95"
title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
{{-- 重置按鈕 --}}
<button type="button"
@click="modelSearch = ''; searchInTab('models')"
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all active:scale-95"
title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
</div>
{{-- Model Table --}}
<div class="overflow-x-auto">
{{-- ── 桌面表格 (xl+) ── --}}
<div class="hidden xl:block overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
<th
class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">
{{ __('Model Name') }}</th>
<th
class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">
{{ __('Machine Count') }}</th>
<th
class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">
{{ __('Last Updated') }}</th>
<th
class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800 text-right">
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800 text-right">
{{ __('Action') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
@forelse($models_list as $model)
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
<tr class="group hover:bg-slate-50/50 dark:hover:bg-white/[0.02] transition-colors duration-200">
<td class="px-6 py-6">
<div class="flex items-center gap-x-3">
<div
class="flex-shrink-0 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="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round"
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
<div class="flex-shrink-0 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="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
</div>
<div
class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">
<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 tracking-tight">
{{ $model->name }}</div>
</div>
</td>
<td class="px-6 py-6">
<span
class="px-2.5 py-1 rounded-lg text-xs font-bold border border-sky-100 dark:border-sky-900/30 bg-sky-50 dark:bg-sky-900/20 text-sky-600 dark:text-sky-400 tracking-widest">
<span class="px-2.5 py-1 rounded-lg text-xs font-bold border border-sky-100 dark:border-sky-900/30 bg-sky-50 dark:bg-sky-900/20 text-sky-600 dark:text-sky-400 tracking-widest">
{{ $model->machines_count ?? 0 }} {{ __('Items') }}
</span>
</td>
<td class="px-6 py-6">
<div
class="text-xs font-black text-slate-400 dark:text-slate-400/80 uppercase tracking-widest leading-none">
<div class="text-xs font-black text-slate-400 dark:text-slate-400/80 uppercase tracking-widest font-mono">
{{ $model->updated_at->format('Y/m/d H:i') }}
</div>
</td>
<td class="px-6 py-6 text-right space-x-2">
<td class="px-6 py-6 text-right">
<div class="flex items-center justify-end gap-2">
<button @click="currentModel = @js($model->only(['name'])); modelActionUrl = '{{ route('admin.basic-settings.machine-models.update', $model) }}'; showEditModelModal = true"
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 dark:hover:text-cyan-400 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all group/btn"
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 dark:hover:text-cyan-400 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all"
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.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
</svg>
</button>
<form :id="'delete-model-form-' + {{ $model->id }}"
action="{{ route('admin.basic-settings.machine-models.destroy', $model) }}"
method="POST" class="inline">
@ -89,10 +99,8 @@
@click="confirmDelete('{{ route('admin.basic-settings.machine-models.destroy', $model) }}')"
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 dark:hover:text-rose-400 hover:bg-rose-500/5 dark:hover:bg-rose-500/10 border border-transparent hover:border-rose-500/20 transition-all"
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 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
<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 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>
</button>
</form>
@ -100,16 +108,63 @@
</td>
</tr>
@empty
<tr>
<td colspan="4"
class="px-6 py-20 text-center text-slate-500 dark:text-slate-400 font-bold tracking-widest uppercase">
{{ __('No data available') }}
</td>
</tr>
<x-empty-state mode="table" :colspan="4" :message="__('No data available')" />
@endforelse
</tbody>
</table>
</div>
{{-- ── Mobile / Tablet Card Grid (< xl) ── --}}
<div class="xl:hidden grid grid-cols-1 md:grid-cols-2 gap-4">
@forelse($models_list as $model)
<div class="luxury-card p-5 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group">
{{-- Card Header --}}
<div class="flex items-center gap-3 mb-5">
<div class="w-14 h-14 rounded-2xl 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 shrink-0">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
</div>
<div class="min-w-0 flex-1">
<h3 class="text-base font-extrabold text-slate-800 dark:text-slate-100 truncate tracking-tight">
{{ $model->name }}
</h3>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">
{{ $model->updated_at->format('Y/m/d H:i') }}
</p>
</div>
<span class="px-2.5 py-1 rounded-lg text-xs font-bold border border-sky-100 dark:border-sky-900/30 bg-sky-50 dark:bg-sky-900/20 text-sky-600 dark:text-sky-400 tracking-widest shrink-0">
{{ $model->machines_count ?? 0 }} {{ __('Items') }}
</span>
</div>
{{-- Action Buttons --}}
<div class="flex items-center gap-2">
<button @click="currentModel = @js($model->only(['name'])); modelActionUrl = '{{ route('admin.basic-settings.machine-models.update', $model) }}'; showEditModelModal = true"
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-500 dark:text-slate-400 font-bold text-xs hover:bg-cyan-500 hover:text-white transition-all duration-300 border border-slate-200/50 dark:border-slate-700/50">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931Z" />
</svg>
{{ __('Edit') }}
</button>
<button type="button"
@click="confirmDelete('{{ route('admin.basic-settings.machine-models.destroy', $model) }}')"
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-500 dark:text-slate-400 font-bold text-xs hover:bg-rose-500 hover:text-white transition-all duration-300 border border-slate-200/50 dark:border-slate-700/50">
<svg class="w-3.5 h-3.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 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>
{{ __('Delete') }}
</button>
</div>
</div>
@empty
<div class="col-span-full">
<x-empty-state :message="__('No data available')" />
</div>
@endforelse
</div>
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
{{ $models_list->appends(['tab' => 'models'])->links('vendor.pagination.luxury') }}
</div>

View File

@ -1,94 +1,100 @@
{{-- Permissions Tab Content (Partial) --}}
{{-- 此檔案被 index.blade.php @include AJAX 模式共用 --}}
{{-- Toolbar 區:搜尋框 + 公司篩選 --}}
<div class="flex items-center justify-between mb-8">
<div class="flex items-center gap-4">
<form @submit.prevent="searchInTab('permissions')" 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" x-model="permissionSearch"
placeholder="{{ __('Search accounts...') }}"
class="luxury-input py-2.5 pl-12 pr-6 block w-64">
</form>
{{-- Toolbar 區:搜尋框 + 公司篩選 + 按鈕 (Scenario B: Stacked) --}}
<div class="flex flex-wrap items-center gap-3 sm:gap-4 mb-8">
{{-- 搜尋輸入框 --}}
<div class="relative group flex-1 sm:flex-none">
<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" x-model="permissionSearch"
@keydown.enter.prevent="searchInTab('permissions')"
placeholder="{{ __('Search accounts...') }}"
class="luxury-input py-2.5 pl-11 pr-4 block w-full sm:w-64 text-sm font-bold">
</div>
@if(auth()->user()->isSystemAdmin())
<div class="w-72">
<x-searchable-select name="company_filter" :options="$companies" :selected="request('company_id')"
:placeholder="__('All Companies')"
x-on:change="permissionCompanyId = $event.target.value; searchInTab('permissions')" />
</div>
@endif
@if(auth()->user()->isSystemAdmin())
<div class="w-full sm:w-72">
<x-searchable-select name="company_filter" :options="$companies" :selected="request('company_id')"
:placeholder="__('All Companies')"
x-on:change="permissionCompanyId = $event.target.value; searchInTab('permissions')" />
</div>
@endif
<div class="flex items-center gap-2 ml-auto sm:ml-0">
{{-- 搜尋按鈕 --}}
<button type="button" @click="searchInTab('permissions')"
class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 transition-all active:scale-95"
title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
{{-- 重置按鈕 --}}
<button type="button"
@click="permissionSearch = ''; permissionCompanyId = ''; searchInTab('permissions')"
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all active:scale-95"
title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
</div>
{{-- Permissions Table --}}
<div class="overflow-x-auto">
{{-- ── 桌面表格 (xl+) ── --}}
<div class="hidden xl:block overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
<th
class="px-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">
<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">
{{ __('Account 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-left">
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-left">
{{ __('Company Name') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Authorized Machines') }}</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">
<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">
{{ __('Action') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
@forelse($users_list ?? [] as $user)
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
<tr class="group hover:bg-slate-50/50 dark:hover:bg-white/[0.02] transition-colors duration-200">
<td class="px-6 py-6 font-display text-left">
<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">
<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="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" />
<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>
</div>
<div class="flex flex-col text-left">
<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">{{
$user->name }}</span>
<span
class="text-xs font-mono font-bold text-slate-500 tracking-widest uppercase">{{
$user->username }}</span>
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight">{{ $user->name }}</span>
<span class="text-xs font-mono font-bold text-slate-500 tracking-widest uppercase">{{ $user->username }}</span>
</div>
</div>
</td>
<td class="px-6 py-6 text-left">
<span
class="px-2.5 py-1 rounded-lg text-xs font-bold border border-sky-100 dark:border-sky-900/30 bg-sky-50 dark:bg-sky-900/20 text-sky-600 dark:text-sky-400 tracking-widest uppercase">
<span class="px-2.5 py-1 rounded-lg text-xs font-bold border border-sky-100 dark:border-sky-900/30 bg-sky-50 dark:bg-sky-900/20 text-sky-600 dark:text-sky-400 tracking-widest uppercase">
{{ $user->company->name ?? __('System') }}
</span>
</td>
<td class="px-6 py-4 text-center">
<div
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">
<div class="flex flex-wrap gap-2 justify-center max-w-[420px] mx-auto max-h-[140px] overflow-y-auto pr-2 custom-scrollbar py-1 text-left">
@forelse($user->machines as $m)
<div
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>
<div 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>
</div>
@empty
<div class="w-full text-center lg:text-left">
<span
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest opacity-40 italic">--
{{ __('None') }} --</span>
<div class="w-full text-center">
<span class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest opacity-40">-- {{ __('None') }} --</span>
</div>
@endforelse
</div>
@ -96,32 +102,82 @@
<td class="px-6 py-6 text-right">
<button
@click='openPermissionModal({{ json_encode(["id" => $user->id, "name" => $user->name]) }})'
class="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 hover:bg-cyan-500 hover:text-white transition-all duration-300 text-xs font-black uppercase tracking-widest shadow-sm shadow-cyan-500/5 group/auth">
class="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 hover:bg-cyan-500 hover:text-white transition-all duration-300 text-xs font-black uppercase tracking-widest shadow-sm shadow-cyan-500/5">
<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 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 00-2 2zm10-10V7a4 4 0 00-8 0v4h8z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
<span>{{ __('Authorize') }}</span>
</button>
</td>
</tr>
@empty
<tr>
<td colspan="4" class="px-6 py-24 text-center">
<div class="flex flex-col items-center gap-3 opacity-20">
<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>
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No accounts found') }}</p>
</div>
</td>
</tr>
<x-empty-state mode="table" :colspan="4" :message="__('No accounts found')" />
@endforelse
</tbody>
</table>
</div>
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6 text-left mb-6">
{{-- ── Mobile / Tablet Card Grid (< xl) ── --}}
<div class="xl:hidden grid grid-cols-1 md:grid-cols-2 gap-4">
@forelse($users_list ?? [] as $user)
<div class="luxury-card p-5 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group">
{{-- Card Header --}}
<div class="flex items-start justify-between gap-4 mb-5">
<div class="flex items-center gap-3 min-w-0">
<div class="w-14 h-14 rounded-2xl 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 shrink-0">
<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="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>
</div>
<div class="min-w-0">
<h3 class="text-base font-extrabold text-slate-800 dark:text-slate-100 truncate tracking-tight">{{ $user->name }}</h3>
<p class="text-xs font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest truncate">{{ $user->username }}</p>
</div>
</div>
<span class="px-2.5 py-1 rounded-lg text-xs font-bold border border-sky-100 dark:border-sky-900/30 bg-sky-50 dark:bg-sky-900/20 text-sky-600 dark:text-sky-400 tracking-widest uppercase shrink-0">
{{ $user->company->name ?? __('System') }}
</span>
</div>
{{-- Machines Summary --}}
@if($user->machines->count() > 0)
<div class="mb-5 border-y border-slate-100 dark:border-slate-800/50 py-4">
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-2">{{ __('Authorized Machines') }}</p>
<div class="flex flex-wrap gap-1.5">
@foreach($user->machines->take(4) as $m)
<span class="px-2.5 py-1 rounded-xl bg-slate-50 dark:bg-slate-800/40 border border-slate-100 dark:border-white/5 text-xs font-black text-slate-700 dark:text-slate-200">{{ $m->name }}</span>
@endforeach
@if($user->machines->count() > 4)
<span class="px-2.5 py-1 rounded-xl bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 text-xs font-black">+{{ $user->machines->count() - 4 }}</span>
@endif
</div>
</div>
@else
<div class="mb-5 border-y border-slate-100 dark:border-slate-800/50 py-4">
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Authorized Machines') }}</p>
<p class="text-xs font-bold text-slate-400 opacity-60">-- {{ __('None') }} --</p>
</div>
@endif
{{-- Action Buttons --}}
<button
@click='openPermissionModal({{ json_encode(["id" => $user->id, "name" => $user->name]) }})'
class="w-full flex items-center justify-center gap-2 py-3 rounded-xl bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 font-bold text-xs hover:bg-cyan-500 hover:text-white transition-all duration-300 border border-cyan-500/20">
<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 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
{{ __('Authorize') }}
</button>
</div>
@empty
<div class="col-span-full">
<x-empty-state :message="__('No accounts found')" />
</div>
@endforelse
</div>
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6 mb-6">
@if($users_list)
{{ $users_list->appends(['tab' => 'permissions'])->links('vendor.pagination.luxury') }}
@endif

View File

@ -1,36 +1,58 @@
<div class="space-y-0">
<!-- 1. Header with Search and Filter -->
<div class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-6">
<div class="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
<!-- Search Bar -->
<div class="relative group w-full md:w-64">
<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" x-model="machineSearch" @input.debounce.500ms="searchInTab('system_settings')"
placeholder="{{ __('搜尋機台名稱或代號...') }}"
class="py-2.5 pl-12 pr-6 block w-full luxury-input">
</div>
<div class="flex flex-wrap items-center gap-3 sm:gap-4 mb-8">
<!-- Search Bar -->
<div class="relative group flex-1 sm:flex-none">
<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" x-model="machineSearch" @input.debounce.500ms="searchInTab('system_settings')"
@keydown.enter.prevent="searchInTab('system_settings')"
placeholder="{{ __('搜尋機台名稱或代號...') }}"
class="luxury-input py-2.5 pl-11 pr-4 block w-full sm:w-72 text-sm font-bold">
</div>
<!-- Company Filter -->
<div class="w-full md:w-48">
<x-searchable-select
x-model="permissionCompanyId"
@change="searchInTab('system_settings')"
:placeholder="__('所有公司')">
@foreach ($companies as $company)
<option value="{{ $company->id }}">{{ $company->name }}</option>
@endforeach
</x-searchable-select>
</div>
<!-- Company Filter -->
<div class="w-full sm:w-72">
<x-searchable-select
x-model="permissionCompanyId"
@change="searchInTab('system_settings')"
:placeholder="__('所有公司')">
@foreach ($companies as $company)
<option value="{{ $company->id }}">{{ $company->name }}</option>
@endforeach
</x-searchable-select>
</div>
<div class="flex items-center gap-2 ml-auto sm:ml-0">
{{-- 搜尋按鈕 --}}
<button type="button" @click="searchInTab('system_settings')"
class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 transition-all active:scale-95"
title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
{{-- 重置按鈕 --}}
<button type="button"
@click="machineSearch = ''; permissionCompanyId = ''; searchInTab('system_settings')"
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all active:scale-95"
title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
</div>
<!-- 2. Table Content -->
<div class="overflow-x-auto">
<!-- ── 桌面表格 (xl+) ── -->
<div class="hidden xl:block overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/30">
@ -50,7 +72,18 @@
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/50">
@forelse($machines as $machine)
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
@php
$activeFeatures = [];
if ($machine->tax_invoice_enabled) $activeFeatures[] = __('Electronic Invoice');
if ($machine->card_terminal_enabled) $activeFeatures[] = __('Includes Credit Card/Mobile Pay');
if ($machine->scan_pay_esun_enabled) $activeFeatures[] = __('E.SUN Bank Scan');
if ($machine->scan_pay_linepay_enabled) $activeFeatures[] = __('LinePay Payment');
if ($machine->shopping_cart_enabled) $activeFeatures[] = __('Shopping Cart');
if ($machine->welcome_gift_enabled) $activeFeatures[] = __('Welcome Gift');
if ($machine->cash_module_enabled) $activeFeatures[] = __('Coin/Banknote Machine');
if ($machine->member_system_enabled) $activeFeatures[] = __('Member System');
@endphp
<tr class="group hover:bg-slate-50/50 dark:hover:bg-white/[0.02] transition-colors duration-200">
<td class="px-6 py-6 w-1/4">
<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">
@ -59,12 +92,8 @@
</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">{{ $machine->name }}</span>
</div>
<div class="flex items-center gap-2 mt-0.5">
<span class="text-xs font-mono font-bold text-slate-500 dark:text-slate-400 tracking-widest uppercase">{{ $machine->serial_no }}</span>
</div>
<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 tracking-tight">{{ $machine->name }}</span>
<span class="text-xs font-mono font-bold text-slate-500 dark:text-slate-400 tracking-widest uppercase">{{ $machine->serial_no }}</span>
</div>
</div>
</td>
@ -73,22 +102,10 @@
</td>
<td class="px-6 py-6">
<div class="flex flex-wrap gap-2">
@php
$activeFeatures = [];
if ($machine->tax_invoice_enabled) $activeFeatures[] = __('Electronic Invoice');
if ($machine->card_terminal_enabled) $activeFeatures[] = __('Includes Credit Card/Mobile Pay');
if ($machine->scan_pay_esun_enabled) $activeFeatures[] = __('E.SUN Bank Scan');
if ($machine->scan_pay_linepay_enabled) $activeFeatures[] = __('LinePay Payment');
if ($machine->shopping_cart_enabled) $activeFeatures[] = __('Shopping Cart');
if ($machine->welcome_gift_enabled) $activeFeatures[] = __('Welcome Gift');
if ($machine->cash_module_enabled) $activeFeatures[] = __('Coin/Banknote Machine');
if ($machine->member_system_enabled) $activeFeatures[] = __('Member System');
@endphp
@forelse($activeFeatures as $feature)
<span class="px-2.5 py-1 rounded-lg bg-cyan-500/10 text-cyan-500 dark:text-cyan-400 text-[13px] font-bold uppercase tracking-wider">{{ $feature }}</span>
<span class="px-2.5 py-1 rounded-lg bg-cyan-500/10 text-cyan-500 dark:text-cyan-400 text-[11px] font-bold uppercase tracking-wider">{{ $feature }}</span>
@empty
<span class="px-2.5 py-1 rounded-lg bg-slate-500/10 text-slate-500 dark:text-slate-400 text-[13px] font-bold uppercase tracking-wider">{{ __('None') }}</span>
<span class="px-2.5 py-1 rounded-lg bg-slate-500/10 text-slate-500 dark:text-slate-400 text-[11px] font-bold uppercase tracking-wider">{{ __('None') }}</span>
@endforelse
</div>
</td>
@ -103,23 +120,70 @@
</td>
</tr>
@empty
<tr>
<td colspan="4" 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 stroke-[2]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<p class="text-slate-400 font-bold">{{ __('No machines found') }}</p>
</div>
</td>
</tr>
<x-empty-state mode="table" :colspan="4" :message="__('No machines found')" />
@endforelse
</tbody>
</table>
</div>
<!-- ── Mobile / Tablet Card Grid (< xl) ── -->
<div class="xl:hidden grid grid-cols-1 md:grid-cols-2 gap-4">
@forelse($machines as $machine)
@php
$activeFeatures = [];
if ($machine->tax_invoice_enabled) $activeFeatures[] = __('Electronic Invoice');
if ($machine->card_terminal_enabled) $activeFeatures[] = __('Includes Credit Card/Mobile Pay');
if ($machine->scan_pay_esun_enabled) $activeFeatures[] = __('E.SUN Bank Scan');
if ($machine->scan_pay_linepay_enabled) $activeFeatures[] = __('LinePay Payment');
if ($machine->shopping_cart_enabled) $activeFeatures[] = __('Shopping Cart');
if ($machine->welcome_gift_enabled) $activeFeatures[] = __('Welcome Gift');
if ($machine->cash_module_enabled) $activeFeatures[] = __('Coin/Banknote Machine');
if ($machine->member_system_enabled) $activeFeatures[] = __('Member System');
@endphp
<div class="luxury-card p-5 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group">
{{-- Card Header --}}
<div class="flex items-center gap-3 mb-5">
<div class="w-14 h-14 rounded-2xl 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 shrink-0">
<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="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<div class="min-w-0 flex-1">
<h3 class="text-base font-extrabold text-slate-800 dark:text-slate-100 truncate tracking-tight">{{ $machine->name }}</h3>
<p class="text-xs font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest truncate">{{ $machine->serial_no }}</p>
</div>
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 shrink-0">{{ $machine->company?->name ?? __('System') }}</span>
</div>
{{-- Active Features --}}
<div class="mb-5 border-y border-slate-100 dark:border-slate-800/50 py-4">
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-2">{{ __('Enabled Features') }}</p>
<div class="flex flex-wrap gap-1.5">
@forelse($activeFeatures as $feature)
<span class="px-2.5 py-1 rounded-lg bg-cyan-500/10 text-cyan-500 dark:text-cyan-400 text-[10px] font-black uppercase tracking-wider">{{ $feature }}</span>
@empty
<span class="px-2.5 py-1 rounded-lg bg-slate-500/10 text-slate-500 dark:text-slate-400 text-[10px] font-black uppercase tracking-wider">{{ __('None') }}</span>
@endforelse
</div>
</div>
{{-- Action Button --}}
<button type="button" @click="openMachineSettingsModal({{ json_encode($machine) }})"
class="w-full flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-500 dark:text-slate-400 font-bold text-xs hover:bg-cyan-500 hover:text-white transition-all duration-300 border border-slate-200/50 dark:border-slate-700/50">
<svg class="w-3.5 h-3.5" 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.931Z" />
</svg>
{{ __('Edit Settings') }}
</button>
</div>
@empty
<div class="col-span-full">
<x-empty-state :message="__('No machines found')" />
</div>
@endforelse
</div>
<!-- 3. Pagination -->
<div class="mt-8 border-t border-slate-100 dark:border-slate-800 pt-6">
{{ $machines->appends(['tab' => 'system_settings', 'search' => request('search'), 'company_id' => request('company_id')])->links('vendor.pagination.luxury') }}

View File

@ -507,41 +507,29 @@
<div class="space-y-2 pb-20" x-data="remoteControlApp({{ Js::from($selectedMachine ? $selectedMachine->id : '') }})">
<div class="flex items-center gap-4">
<!-- Back Button for Detail/Control Mode -->
<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="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
</svg>
</button>
</template>
<div>
<h1 class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display">
{{ __($title ?? 'Remote Command Center') }}
</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">
{{ __($subtitle ?? 'Execute maintenance and operational commands remotely') }}
</p>
</div>
</div>
<!-- Header -->
<x-page-header
:title="__($title ?? 'Remote Command Center')"
:subtitle="__($subtitle ?? 'Execute maintenance and operational commands remotely')"
>
<x-slot:back>
<template x-if="viewMode === 'control'">
<button @click="backToList()"
class="p-2 sm:p-2.5 rounded-xl bg-white dark:bg-slate-800 text-slate-500 border border-slate-200/60 dark:border-slate-700/60 shadow-sm hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-all active:scale-95 shrink-0">
<svg class="w-5 h-5 sm:w-6 sm:h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
</svg>
</button>
</template>
</x-slot:back>
</x-page-header>
<!-- Tab Navigation (Only visible when not in specific machine control) -->
<template x-if="viewMode !== 'control'">
<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="viewMode = 'history'"
:class="viewMode === 'history' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200'"
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all duration-300">
{{ __('Operation Records') }}
</button>
<button @click="viewMode = 'list'"
:class="viewMode === '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 duration-300">
{{ __('New Command') }}
</button>
</div>
<x-tab-nav model="viewMode">
<x-tab-nav-item value="history" :label="__('Operation Records')" model="viewMode" />
<x-tab-nav-item value="list" :label="__('New Command')" model="viewMode" />
</x-tab-nav>
</template>
<div class="mt-6">

View File

@ -19,13 +19,13 @@
</div>
<!-- Filters Area -->
<div class="mb-8">
<form @submit.prevent="searchInTab('history')" class="flex flex-wrap items-center gap-4">
<!-- Search Box -->
<div class="relative group flex-[1.5] min-w-[200px]">
<div class="flex flex-wrap items-center gap-3 sm:gap-4 mb-6">
<form @submit.prevent="searchInTab('history')" class="contents">
{{-- 搜尋輸入框 --}}
<div class="relative group flex-1 sm:flex-none">
<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"
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>
@ -33,11 +33,11 @@
</span>
<input type="text" name="search" value="{{ request('search') }}"
placeholder="{{ __('Search machines...') }}"
class="luxury-input py-2.5 pl-12 pr-6 block w-full">
class="luxury-input py-2.5 pl-12 pr-6 block w-full sm:w-72 text-sm font-bold">
</div>
<!-- Date Range -->
<div class="relative group flex-[2] min-w-[340px]" x-data="{
{{-- 日期範圍 --}}
<div class="relative group w-full sm:w-72 sm:flex-none" x-data="{
fp: null,
startDate: '{{ request('start_date') }}',
endDate: '{{ request('end_date') }}'
@ -58,8 +58,7 @@
}
}
})">
<span
class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10 text-slate-400 group-focus-within:text-cyan-500 transition-colors">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10 text-slate-400 group-focus-within:text-cyan-500 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
@ -71,11 +70,11 @@
<input type="text" x-ref="dateRange"
value="{{ request('start_date') && request('end_date') ? request('start_date') . ' ' . __('to') . ' ' . request('end_date') : (request('start_date') ?: '') }}"
placeholder="{{ __('Select Date Range') }}"
class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer">
class="luxury-input py-2.5 pl-12 pr-6 block w-full text-sm font-bold cursor-pointer">
</div>
<!-- Command Type -->
<div class="flex-[0.8] min-w-[160px]">
{{-- 指令類型 --}}
<div class="w-full sm:w-48">
<x-searchable-select name="command_type" :options="[
'reboot' => __('Machine Reboot'),
'reboot_card' => __('Card Reader Reboot'),
@ -89,8 +88,8 @@
@change="searchInTab('history')" />
</div>
<!-- Status -->
<div class="flex-[0.8] min-w-[160px]">
{{-- 狀態 --}}
<div class="w-full sm:w-48">
<x-searchable-select name="status" :options="[
'pending' => __('Pending'),
'sent' => __('Sent'),
@ -100,23 +99,23 @@
]" :selected="request('status')" :placeholder="__('All Status')" :hasSearch="false" @change="searchInTab('history')" />
</div>
<!-- Actions -->
<div class="flex items-center gap-2">
<button type="submit" class="p-2.5 rounded-xl bg-cyan-600 text-white hover:bg-cyan-500 shadow-lg shadow-cyan-500/20 transition-all active:scale-95">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
{{-- 搜尋 + 重置按鈕 --}}
<div class="flex items-center gap-2 ml-auto sm:ml-0">
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 transition-all active:scale-95" title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
<button type="button" @click="searchInTab('history', true)" class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all active:scale-95">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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" />
<button type="button" @click="searchInTab('history', true)" class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all active:scale-95" title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
</form>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0 text-sm">
<table class="hidden xl:table w-full text-left border-separate border-spacing-y-0 text-sm">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
<th
@ -255,6 +254,96 @@
</table>
</div>
{{-- Mobile Card View --}}
<div class="xl:hidden grid grid-cols-1 md:grid-cols-2 gap-6">
@forelse ($history as $item)
<div class="luxury-card p-6 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group"
@click="selectMachine({{ Js::from($item->machine) }})">
<div class="flex items-start justify-between gap-4 mb-6">
<div class="flex items-center gap-4 min-w-0">
<div class="w-14 h-14 rounded-2xl 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 overflow-hidden shadow-sm shrink-0">
<template x-if="{{ Js::from(!empty($item->machine->image_urls) && isset($item->machine->image_urls[0])) }}">
<img src="{{ !empty($item->machine->image_urls) ? $item->machine->image_urls[0] : '' }}" class="w-full h-full object-cover">
</template>
<template x-if="{{ Js::from(empty($item->machine->image_urls) || !isset($item->machine->image_urls[0])) }}">
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-5.25v9" />
</svg>
</template>
</div>
<div class="min-w-0 flex-1">
<h3 class="text-base sm:text-lg font-black text-slate-800 dark:text-slate-100 tracking-tight group-hover:text-cyan-600 transition-colors tracking-tight">{{ $item->machine->name }}</h3>
<p class="text-[11px] font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ $item->machine->serial_no }}</p>
</div>
</div>
<div class="shrink-0">
<div class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black border tracking-widest uppercase shadow-sm" :class="getCommandBadgeClass({{ Js::from($item->status) }})">
<div class="w-1.5 h-1.5 rounded-full mr-1.5" :class="{
'bg-amber-500 animate-pulse': {{ Js::from($item->status) }} === 'pending',
'bg-cyan-500': {{ Js::from($item->status) }} === 'sent',
'bg-emerald-500': {{ Js::from($item->status) }} === 'success',
'bg-rose-500': {{ Js::from($item->status) }} === 'failed',
'bg-slate-400': {{ Js::from($item->status) }} === 'superseded'
}"></div>
<span x-text="getCommandStatus({{ Js::from($item->status) }})"></span>
</div>
</div>
</div>
<div class="grid grid-cols-1 gap-y-4 mb-6 border-y border-slate-100 dark:border-slate-800/50 py-4">
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Command Type') }}</p>
<div class="flex flex-col gap-1.5">
<span class="text-sm font-bold text-slate-700 dark:text-slate-200" x-text="getCommandName({{ Js::from($item->command_type) }})"></span>
<div class="flex flex-wrap gap-1">
<span x-show="getPayloadDetails({{ Js::from($item) }})"
class="text-[10px] font-bold text-cyan-600 dark:text-cyan-400/80 bg-cyan-500/5 px-2 py-0.5 rounded-md border border-cyan-500/10 w-fit"
:class="{'line-through opacity-60': {{ Js::from($item->status) }} === 'failed'}"
x-text="getPayloadDetails({{ Js::from($item) }})"></span>
@if($item->note)
<span class="text-[10px] text-slate-400 italic" x-text="translateNote({{ Js::from($item->note) }})"></span>
@endif
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Creation Time') }}</p>
<div class="flex flex-col">
<span class="text-sm font-bold text-slate-700 dark:text-slate-200">{{ $item->created_at->format('Y-m-d') }}</span>
<span class="text-[10px] font-mono text-slate-400">{{ $item->created_at->format('H:i:s') }}</span>
</div>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Operator') }}</p>
<div class="flex items-center gap-2">
<div class="w-5 h-5 rounded-full bg-cyan-500/10 flex items-center justify-center text-[9px] font-black text-cyan-600 dark:text-cyan-400 border border-cyan-500/20">
{{ mb_substr($item->user ? $item->user->name : __('System'), 0, 1) }}
</div>
<span class="text-sm font-bold text-slate-700 dark:text-slate-200 truncate">{{ $item->user ? $item->user->name : __('System') }}</span>
</div>
</div>
</div>
</div>
<div class="flex items-center gap-3">
<button type="button" @click.stop="selectMachine({{ Js::from($item->machine) }})"
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-black text-[10px] sm:text-xs tracking-widest hover:bg-slate-200 dark:hover:bg-slate-700 transition-all duration-300 border border-slate-200 dark:border-slate-700 shadow-sm">
<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>
{{ __('New Command') }}
</button>
</div>
</div>
@empty
<x-empty-state
icon="<path stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4'/>"
:title="__('No records found')"
:subtitle="''"
class="md:col-span-2"
/>
@endforelse
</div>
<!-- Pagination Area -->
<div class="mt-8">
{{ $history->appends(request()->query())->links('vendor.pagination.luxury') }}

View File

@ -1,6 +1,6 @@
{{-- 庫存操作紀錄 Partial (AJAX 可替換) --}}
<!-- Filters Area -->
<div class="mb-8">
<div class="mb-6">
<form method="GET" action="{{ route('admin.remote.stock') }}" class="flex flex-wrap items-center gap-4" @submit.prevent="searchInTab()">
<!-- Search Box -->
<div class="relative group flex-[1.5] min-w-[200px]">
@ -86,7 +86,7 @@
</form>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0 text-sm">
<table class="hidden xl:table w-full text-left border-separate border-spacing-y-0 text-sm">
<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">{{ __('Machine Information') }}</th>
@ -185,6 +185,92 @@
@endif
</tbody>
</table>
</table>
</div>
{{-- Mobile Card View --}}
<div class="xl:hidden grid grid-cols-1 md:grid-cols-2 gap-6">
@forelse ($history as $item)
<div class="luxury-card p-6 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group"
@click="selectMachine(@js($item->machine))">
<div class="flex items-start justify-between gap-4 mb-6">
<div class="flex items-center gap-4 min-w-0">
<div class="w-14 h-14 rounded-2xl 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 overflow-hidden shadow-sm shrink-0">
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-5.25v9" />
</svg>
</div>
<div class="min-w-0 flex-1">
<h3 class="text-base sm:text-lg font-black text-slate-800 dark:text-slate-100 tracking-tight group-hover:text-cyan-600 transition-colors tracking-tight">{{ $item->machine->name }}</h3>
<p class="text-[11px] font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ $item->machine->serial_no }}</p>
</div>
</div>
<div class="shrink-0">
<div class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black border tracking-widest uppercase shadow-sm" :class="getCommandBadgeClass(@js($item->status))">
<div class="w-1.5 h-1.5 rounded-full mr-1.5" :class="{
'bg-amber-500 animate-pulse': @js($item->status) === 'pending',
'bg-cyan-500': @js($item->status) === 'sent',
'bg-emerald-500': @js($item->status) === 'success',
'bg-rose-500': @js($item->status) === 'failed',
'bg-slate-400': @js($item->status) === 'superseded'
}"></div>
<span x-text="getCommandStatus(@js($item->status))"></span>
</div>
</div>
</div>
<div class="grid grid-cols-1 gap-y-4 mb-6 border-y border-slate-100 dark:border-slate-800/50 py-4">
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Command Type') }}</p>
<div class="flex flex-col gap-1.5">
<span class="text-sm font-bold text-slate-700 dark:text-slate-300 tracking-tight" x-text="getCommandName(@js($item->command_type))"></span>
<div class="flex flex-wrap gap-1">
<span x-show="getPayloadDetails(@js($item))"
class="text-[10px] font-bold text-cyan-600 dark:text-cyan-400/80 bg-cyan-500/5 px-2 py-0.5 rounded-md border border-cyan-500/10 w-fit"
:class="{'line-through opacity-60': @js($item->status) === 'failed'}"
x-text="getPayloadDetails(@js($item))"></span>
@if($item->note)
<span class="text-[10px] text-slate-400 italic" x-text="translateNote(@js($item->note))"></span>
@endif
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Creation Time') }}</p>
<div class="flex flex-col">
<span class="text-sm font-bold text-slate-700 dark:text-slate-200">{{ $item->created_at->format('Y-m-d') }}</span>
<span class="text-[10px] font-mono text-slate-400">{{ $item->created_at->format('H:i:s') }}</span>
</div>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Operator') }}</p>
<div class="flex items-center gap-2">
<div class="w-5 h-5 rounded-full bg-cyan-500/10 flex items-center justify-center text-[9px] font-black text-cyan-600 dark:text-cyan-400 border border-cyan-500/20">
{{ mb_substr($item->user ? $item->user->name : __('System'), 0, 1) }}
</div>
<span class="text-sm font-bold text-slate-700 dark:text-slate-200 truncate">{{ $item->user ? $item->user->name : __('System') }}</span>
</div>
</div>
</div>
</div>
<div class="flex items-center gap-3">
<button type="button" @click.stop="selectMachine(@js($item->machine))"
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-black text-[10px] sm:text-xs tracking-widest hover:bg-slate-200 dark:hover:bg-slate-700 transition-all duration-300 border border-slate-200 dark:border-slate-700 shadow-sm">
<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>
{{ __('Manage') }}
</button>
</div>
</div>
@empty
<x-empty-state
icon="<path stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4'/>"
:title="__('No records found')"
:subtitle="''"
class="md:col-span-2"
/>
@endforelse
</div>
<!-- Pagination Area -->

View File

@ -19,23 +19,44 @@
</div>
<!-- Filters Area -->
<div class="flex items-center justify-between mb-8">
<form @submit.prevent="searchInTab('list')" class="relative group">
<div class="flex flex-wrap items-center gap-3 sm:gap-4 mb-6">
{{-- 搜尋輸入框 --}}
<div class="relative group flex-1 sm:flex-none">
<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"
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') }}" placeholder="{{ __('Search machines...') }}"
class="luxury-input py-2.5 pl-12 pr-6 block w-72">
</form>
@keydown.enter.prevent="searchInTab('list')"
class="luxury-input py-2.5 pl-12 pr-6 block w-full sm:w-72 text-sm font-bold">
</div>
{{-- 搜尋 + 重置按鈕 --}}
<div class="flex items-center gap-2 ml-auto sm:ml-0">
<button type="button" @click="searchInTab('list')"
class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 transition-all active:scale-95"
title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
<button type="button" @click="searchInTab('list', true)"
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all active:scale-95"
title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
</div>
<div class="overflow-x-auto pb-4">
<table class="w-full text-left border-separate border-spacing-y-0 text-sm whitespace-nowrap">
<table class="hidden xl:table w-full text-left border-separate border-spacing-y-0 text-sm whitespace-nowrap">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
<th
@ -145,6 +166,76 @@
</table>
</div>
{{-- Mobile Card View --}}
<div class="xl:hidden grid grid-cols-1 md:grid-cols-2 gap-6">
@foreach($machines as $machine)
<div class="luxury-card p-6 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group"
@click="selectMachine({{ Js::from($machine) }})">
<div class="flex items-start justify-between gap-4 mb-6">
<div class="flex items-center gap-4 min-w-0">
<div class="w-14 h-14 rounded-2xl 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 overflow-hidden shadow-sm shrink-0">
@if(!empty($machine->image_urls) && isset($machine->image_urls[0]))
<img src="{{ $machine->image_urls[0] }}" class="w-full h-full object-cover">
@else
<svg class="w-7 h-7 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-5.25v9" />
</svg>
@endif
</div>
<div class="min-w-0 flex-1">
<h3 class="text-base sm:text-lg font-black text-slate-800 dark:text-slate-100 tracking-tight group-hover:text-cyan-600 transition-colors tracking-tight">{{ $machine->name }}</h3>
<p class="text-[11px] font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ $machine->serial_no }}</p>
</div>
</div>
<div class="shrink-0">
@if($machine->status === 'online' || !$machine->status)
<div class="flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-emerald-500/10 border border-emerald-500/20">
<div class="relative flex h-1.5 w-1.5">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-1.5 w-1.5 bg-emerald-500"></span>
</div>
<span class="text-[9px] font-black text-emerald-600 dark:text-emerald-400 tracking-[0.1em] uppercase">{{ __('Online') }}</span>
</div>
@elseif($machine->status === 'offline')
<div class="flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-slate-500/10 border border-slate-500/20">
<div class="h-1.5 w-1.5 rounded-full bg-slate-400"></div>
<span class="text-[9px] font-black text-slate-500 dark:text-slate-400 tracking-[0.1em] uppercase">{{ __('Offline') }}</span>
</div>
@else
<div class="flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-rose-500/10 border border-rose-500/20">
<div class="relative flex h-1.5 w-1.5">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-1.5 w-1.5 bg-rose-500"></span>
</div>
<span class="text-[9px] font-black text-rose-600 dark:text-rose-400 tracking-[0.1em] uppercase">{{ __('Abnormal') }}</span>
</div>
@endif
</div>
</div>
<div class="grid grid-cols-2 gap-y-4 mb-6 border-y border-slate-100 dark:border-slate-800/50 py-4">
<div class="col-span-2">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Last Communication') }}</p>
<div class="flex flex-col">
<span class="text-sm font-bold text-slate-700 dark:text-slate-200 mt-0.5" x-text="formatTime({{ Js::from($machine->last_heartbeat_at) }})"></span>
<span class="text-[10px] font-mono text-slate-400">
{{ $machine->last_heartbeat_at ? \Illuminate\Support\Carbon::parse($machine->last_heartbeat_at)->format('Y-m-d') : '--' }}
</span>
</div>
</div>
</div>
<div class="flex items-center gap-3">
<button type="button" @click.stop="selectMachine({{ Js::from($machine) }})"
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-black text-[10px] sm:text-xs tracking-widest hover:bg-slate-200 dark:hover:bg-slate-700 transition-all duration-300 border border-slate-200 dark:border-slate-700 shadow-sm">
<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>
{{ __('New Command') }}
</button>
</div>
</div>
@endforeach
</div>
<!-- Pagination Area -->
<div class="mt-8">
{{ $machines->appends(request()->query())->links('vendor.pagination.luxury') }}

View File

@ -1,5 +1,5 @@
<div class="overflow-x-auto pb-4">
<table class="w-full text-left border-separate border-spacing-y-0 text-sm whitespace-nowrap">
<table class="hidden xl:table w-full text-left border-separate border-spacing-y-0 text-sm whitespace-nowrap">
<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">
@ -92,16 +92,7 @@
</div>
</td>
<td class="px-6 py-6 text-right">
<div class="flex justify-end gap-2">
<a href="{{ route('admin.warehouses.machine-inventory', ['machine_id' => $machine->id]) }}"
@click.stop
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="{{ __('View Overview') }}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
</a>
<div class="flex justify-end">
<button @click.stop="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') }}">
@ -130,6 +121,103 @@
</table>
</div>
{{-- Mobile Card View --}}
<div class="xl:hidden grid grid-cols-1 md:grid-cols-2 gap-6">
@forelse($machines as $machine)
<div class="luxury-card p-6 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group"
@click="selectMachine({{ Js::from($machine) }})">
<div class="flex items-start justify-between gap-4 mb-6">
<div class="flex items-center gap-4 min-w-0">
<div class="w-14 h-14 rounded-2xl 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 overflow-hidden shadow-sm shrink-0">
@if($machine->image_urls && isset($machine->image_urls[0]))
<img src="{{ $machine->image_urls[0] }}" class="w-full h-full object-cover">
@else
<svg class="w-7 h-7 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-5.25v9" />
</svg>
@endif
</div>
<div class="min-w-0 flex-1">
<h3 class="text-base sm:text-lg font-black text-slate-800 dark:text-slate-100 tracking-tight group-hover:text-cyan-600 transition-colors tracking-tight">{{ $machine->name }}</h3>
<p class="text-[11px] font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ $machine->serial_no }}</p>
</div>
</div>
<div class="shrink-0">
@if($machine->status === 'online' || !$machine->status)
<div class="flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-emerald-500/10 border border-emerald-500/20">
<div class="relative flex h-1.5 w-1.5">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-1.5 w-1.5 bg-emerald-500"></span>
</div>
<span class="text-[9px] font-black text-emerald-600 dark:text-emerald-400 tracking-[0.1em] uppercase">{{ __('Online') }}</span>
</div>
@elseif($machine->status === 'offline')
<div class="flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-slate-500/10 border border-slate-500/20">
<div class="h-1.5 w-1.5 rounded-full bg-slate-400"></div>
<span class="text-[9px] font-black text-slate-500 dark:text-slate-400 tracking-[0.1em] uppercase">{{ __('Offline') }}</span>
</div>
@else
<div class="flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-rose-500/10 border border-rose-500/20">
<div class="relative flex h-1.5 w-1.5">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-1.5 w-1.5 bg-rose-500"></span>
</div>
<span class="text-[9px] font-black text-rose-600 dark:text-rose-400 tracking-[0.1em] uppercase">{{ __('Abnormal') }}</span>
</div>
@endif
</div>
</div>
<div class="grid grid-cols-1 gap-y-4 mb-6 border-y border-slate-100 dark:border-slate-800/50 py-4">
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5">{{ __('Inventory Status') }}</p>
<div class="flex flex-wrap gap-2">
@if($machine->low_stock_count > 0)
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-rose-500/10 text-rose-500 text-[10px] font-black border border-rose-500/20 uppercase tracking-widest leading-none shadow-sm shadow-rose-500/5">
<span class="w-1.5 h-1.5 rounded-full bg-rose-500 animate-pulse"></span>
{{ $machine->low_stock_count }}&nbsp;{{ __('Low') }}
</span>
@endif
@if($machine->expiring_soon_count > 0)
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-amber-500/10 text-amber-500 text-[10px] font-black border border-amber-500/20 uppercase tracking-widest leading-none shadow-sm shadow-amber-500/5">
<span class="w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse"></span>
{{ $machine->expiring_soon_count }}&nbsp;{{ __('Expiring') }}
</span>
@endif
@if(!$machine->low_stock_count && !$machine->expiring_soon_count)
<span class="text-[10px] font-bold text-slate-400 dark:text-slate-600 uppercase tracking-[0.1em] border border-slate-200/50 dark:border-slate-800 rounded-lg px-2.5 py-1">{{ __('Inventory Stable') }}</span>
@endif
</div>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Last Sync') }}</p>
<div class="flex flex-col">
<span class="text-sm font-bold text-slate-700 dark:text-slate-200 mt-0.5" x-text="formatTime({{ Js::from($machine->last_heartbeat_at) }})"></span>
<span class="text-[10px] font-mono text-slate-400">
{{ $machine->last_heartbeat_at ? \Illuminate\Support\Carbon::parse($machine->last_heartbeat_at)->format('Y-m-d') : '--' }}
</span>
</div>
</div>
</div>
<div class="flex items-center gap-3">
<button type="button" @click.stop="selectMachine({{ Js::from($machine) }})"
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 font-black text-[10px] sm:text-xs tracking-widest hover:bg-cyan-500/20 transition-all duration-300 border border-cyan-500/20 shadow-sm">
<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>
{{ __('Manage') }}
</button>
</div>
</div>
@empty
<x-empty-state
icon="<path stroke-linecap='round' stroke-linejoin='round' stroke-width='1.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'/>"
:title="__('No machines found')"
:subtitle="''"
class="md:col-span-2"
/>
@endforelse
</div>
{{-- 標準化分頁底欄 --}}
<div class="mt-8">
{{ $machines->appends(request()->except('machine_page'))->links('vendor.pagination.luxury') }}

View File

@ -351,41 +351,29 @@
<div class="space-y-2 pb-20" x-data="stockApp('{{ $selectedMachine ? $selectedMachine->id : '' }}')"
@keydown.escape.window="showEditModal = false">
<div class="flex items-center gap-4">
<!-- Back Button for Detail Mode -->
<template x-if="viewMode === 'detail'">
<button @click="backToList()"
class="p-2.5 rounded-xl bg-white dark:bg-slate-900 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-all border border-slate-200/50 dark:border-slate-700/50 shadow-sm hover:shadow-md active:scale-95">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
</svg>
</button>
</template>
<div>
<h1 class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display">
{{ __($title ?? 'Stock & Expiry Management') }}
</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">
{{ __($subtitle ?? 'Manage inventory and monitor expiry dates across all machines') }}
</p>
</div>
</div>
<!-- Header -->
<x-page-header
:title="__($title ?? 'Stock & Expiry Management')"
:subtitle="__($subtitle ?? 'Manage inventory and monitor expiry dates across all machines')"
>
<x-slot:back>
<template x-if="viewMode === 'detail'">
<button @click="backToList()"
class="p-2 sm:p-2.5 rounded-xl bg-white dark:bg-slate-800 text-slate-500 border border-slate-200/60 dark:border-slate-700/60 shadow-sm hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-all active:scale-95 shrink-0">
<svg class="w-5 h-5 sm:w-6 sm:h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
</svg>
</button>
</template>
</x-slot:back>
</x-page-header>
<!-- Tab Navigation (Only visible when not in specific machine detail) -->
<template x-if="viewMode !== 'detail'">
<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="viewMode = 'history'"
:class="viewMode === 'history' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200'"
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all duration-300">
{{ __('Operation Records') }}
</button>
<button @click="viewMode = 'list'"
:class="viewMode === '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 duration-300">
{{ __('Adjust Stock & Expiry') }}
</button>
</div>
<x-tab-nav model="viewMode">
<x-tab-nav-item value="history" :label="__('Operation Records')" model="viewMode" />
<x-tab-nav-item value="list" :label="__('Adjust Stock & Expiry')" model="viewMode" />
</x-tab-nav>
</template>
<div class="mt-6">

View File

@ -6,15 +6,36 @@
showDeleteModal: false,
deleteTargetForm: '',
selectedMachine: '',
name: '',
passCodeName: '',
expiresDays: 7,
customCode: '',
isLoadingTable: false,
status: '{{ request('status', '') }}',
showQrModal: false,
activeQrCode: '',
activeTicketUrl: '',
generateRandomCode() {
this.customCode = Math.floor(Math.random() * 90000000 + 10000000).toString();
},
calculateExpiry() {
if (!this.expiresDays || parseInt(this.expiresDays) <= 0) {
return '{{ __('Permanent') }}';
}
const date = new Date();
const days = parseInt(this.expiresDays) || 0;
date.setDate(date.getDate() + days);
return date.toLocaleString('zh-TW', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
}).replace(/\//g, '-');
},
async fetchPage(url) {
if (!url || this.isLoadingTable) return;
this.isLoadingTable = true;
@ -46,13 +67,80 @@
if (form) form.submit();
}
this.showDeleteModal = false;
},
syncSelect(id, value) {
this.$nextTick(() => {
const el = document.getElementById(id);
if (el) {
const valStr = (value !== undefined && value !== null && value.toString().trim() !== '') ? value.toString() : ' ';
el.value = valStr;
if (window.HSSelect) {
const inst = window.HSSelect.getInstance(el);
if (inst) inst.setValue(valStr);
}
}
});
},
validateAndSubmit(e) {
if (!this.selectedMachine) {
window.showToast('{{ __('Please select a machine') }}', 'error');
return;
}
if (!this.passCodeName.trim()) {
window.showToast('{{ __('Please enter a description') }}', 'error');
return;
}
if (!this.customCode.trim()) {
window.showToast('{{ __('Please enter or generate a pass code') }}', 'error');
return;
}
e.target.submit();
},
copyQrCode() {
if (!this.activeQrCode) return;
navigator.clipboard.writeText(this.activeQrCode).then(() => {
window.showToast('{{ __('Code Copied') }}', 'success');
});
},
copyQrLink() {
if (!this.activeTicketUrl) return;
navigator.clipboard.writeText(this.activeTicketUrl).then(() => {
window.showToast('{{ __('Link Copied') }}', 'success');
});
},
async downloadQrCode() {
const url = '{{ route('admin.basic-settings.qr-code') }}?size=500&data=' + encodeURIComponent(this.activeQrCode);
fetch(url)
.then(response => response.blob())
.then(blob => {
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `pass-code-${this.activeQrCode}.png`;
link.click();
URL.revokeObjectURL(link.href);
});
},
init() {
this.$watch('showCreateModal', (val) => {
if (val) {
this.selectedMachine = '';
this.passCodeName = '';
this.expiresDays = 7;
this.generateRandomCode();
this.syncSelect('modal-pass-machine', '');
}
});
}
}">
} ">
{{-- Page Header --}}
<x-page-header
:title="__('Pass Codes')"
:subtitle="__('Manage multi-use authorization codes for testing and maintenance')"
>
<x-page-header :title="__('Pass Codes')"
:subtitle="__('Manage multi-use authorization codes for testing and maintenance')">
<button @click="showCreateModal = true; generateRandomCode()" 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" />
@ -68,26 +156,52 @@
<div id="ajax-content-container" :class="{ 'opacity-30 pointer-events-none': isLoadingTable }">
<form action="{{ route('admin.sales.pass-codes') }}" method="GET"
class="flex flex-col md:flex-row md:items-center gap-3 mb-8"
@submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())">
class="flex flex-col md:flex-row md:items-center gap-3 mb-8"
@submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())">
<div class="relative group flex-1 md:flex-none">
<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"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</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 by code, name or machine...') }}">
</div>
<div class="w-full md:w-48">
<x-searchable-select name="status" id="filter-status" :placeholder="__('All Status')"
x-model="status"
@change="$el.closest('form').dispatchEvent(new Event('submit'))">
<option value="active" {{ request('status')==='active' ? 'selected' : '' }}
data-title="{{ __('Active') }}">{{ __('Active') }}</option>
<option value="expired" {{ request('status')==='expired' ? 'selected' : '' }}
data-title="{{ __('Expired') }}">{{ __('Expired') }}</option>
<option value="disabled" {{ request('status')==='disabled' ? 'selected' : '' }}
data-title="{{ __('Cancelled') }}">{{ __('Cancelled') }}</option>
</x-searchable-select>
</div>
<div class="flex items-center gap-2 ml-auto md:ml-0 shrink-0">
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 transition-all" title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
<button type="submit"
class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 transition-all"
title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
<button type="button"
@click="$el.closest('form').querySelector('input[name=search]').value=''; $el.closest('form').dispatchEvent(new Event('submit'))"
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all" title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
@click="$el.closest('form').querySelector('input[name=search]').value=''; status = ''; syncSelect('filter-status', ''); $el.closest('form').dispatchEvent(new Event('submit'))"
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all"
title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
</form>
@ -97,64 +211,109 @@
<table class="w-full text-left border-separate border-spacing-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">
<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">
{{ __('Pass Code') }}
</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">
<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">
{{ __('Name / Machine') }}
</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">
<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">
{{ __('Expires At') }}
</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">
<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">
{{ __('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">
<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-100 dark:divide-slate-800/80">
@forelse ($passCodes as $code)
<tr class="group hover:bg-slate-50/50 dark:hover:bg-white/[0.02] transition-colors duration-200">
<td class="px-6 py-6 whitespace-nowrap">
<span class="text-lg font-black font-mono text-slate-700 dark:text-slate-200 tracking-widest bg-white dark:bg-slate-900 px-4 py-1.5 rounded-xl border border-slate-200 dark:border-slate-700 shadow-sm group-hover:border-cyan-500/50 transition-colors">
{{ $code->code }}
</span>
</td>
<td class="px-6 py-6">
<div class="text-base font-extrabold text-slate-800 dark:text-slate-100 tracking-tight">{{ $code->name }}</div>
<div class="text-xs font-bold text-slate-400 uppercase tracking-widest">{{ $code->machine->name }}</div>
</td>
<td class="px-6 py-6 whitespace-nowrap">
@if($code->expires_at)
<div class="text-sm font-black text-slate-600 dark:text-slate-300 font-mono tracking-widest">{{ $code->expires_at->format('Y-m-d H:i') }}</div>
<div class="text-xs font-bold {{ $code->expires_at->isPast() ? 'text-rose-500' : 'text-slate-400' }} mt-0.5">
{{ $code->expires_at->isPast() ? __('Expired') : $code->expires_at->diffForHumans() }}
</div>
@else
<span class="text-xs font-black uppercase tracking-widest text-emerald-500 bg-emerald-500/10 px-2 py-0.5 rounded-lg border border-emerald-500/20">{{ __('Permanent') }}</span>
<tr
class="group hover:bg-slate-50/50 dark:hover:bg-white/[0.02] transition-colors duration-200">
<td class="px-6 py-6 whitespace-nowrap">
@if($code->status === 'active' && (!$code->expires_at || $code->expires_at->isFuture()))
<span
@click="activeQrCode = '{{ $code->code }}'; activeTicketUrl = '{{ $code->ticket_url }}'; showQrModal = true"
class="text-lg font-black font-mono text-cyan-600 dark:text-cyan-400 tracking-widest bg-cyan-50 dark:bg-cyan-500/10 px-4 py-1.5 rounded-xl border border-cyan-100 dark:border-cyan-500/20 shadow-sm cursor-pointer hover:bg-cyan-100 dark:hover:bg-cyan-500/20 transition-all">
{{ $code->code }}
</span>
@else
<span
class="text-lg font-black font-mono text-slate-400 dark:text-slate-600 tracking-widest bg-slate-50 dark:bg-slate-900/50 px-4 py-1.5 rounded-xl border border-slate-100 dark:border-slate-800/50 shadow-sm">
{{ $code->code }}
</span>
@endif
</td>
<td class="px-6 py-6">
<div class="text-base font-extrabold text-slate-800 dark:text-slate-100 tracking-tight">
{{ $code->name }}</div>
<div class="text-xs font-bold text-slate-400 uppercase tracking-widest">{{
$code->machine->name }}</div>
</td>
<td class="px-6 py-6 whitespace-nowrap">
@if($code->expires_at)
<div
class="text-sm font-black text-slate-600 dark:text-slate-300 font-mono tracking-widest">
{{ $code->expires_at->format('Y-m-d H:i') }}</div>
<div
class="text-xs font-bold {{ $code->expires_at->isPast() ? 'text-rose-500' : 'text-slate-400' }} mt-0.5">
{{ $code->expires_at->isPast() ? __('Expired') : $code->expires_at->diffForHumans()
}}
</div>
@else
<span
class="text-xs font-black uppercase tracking-widest text-emerald-500 bg-emerald-500/10 px-2 py-0.5 rounded-lg border border-emerald-500/20">{{
__('Permanent') }}</span>
@endif
</td>
<td class="px-6 py-6 whitespace-nowrap">
@php
$displayStatus = $code->status === 'active' && $code->expires_at?->isPast() ? 'expired' : $code->status;
@endphp
<x-status-badge :status="$displayStatus" />
</td>
<td class="px-6 py-6 whitespace-nowrap text-right">
<div class="flex items-center justify-end gap-2">
@if($code->status === 'active' && (!$code->expires_at || $code->expires_at->isFuture()))
<button type="button"
@click="activeQrCode = '{{ $code->code }}'; activeTicketUrl = '{{ $code->ticket_url }}'; showQrModal = true"
class="p-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all duration-300 border border-slate-100 dark:border-slate-800"
title="{{ __('View QR') }}">
<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="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 6.75h.75v.75h-.75v-.75zM6.75 16.5h.75v.75h-.75v-.75zM16.5 6.75h.75v.75h-.75v-.75zM13.5 13.5h.75v.75h-.75v-.75zM13.5 19.5h.75v.75h-.75v-.75zM19.5 13.5h.75v.75h-.75v-.75zM19.5 19.5h.75v.75h-.75v-.75zM16.5 16.5h.75v.75h-.75v-.75z" />
</svg>
</button>
@endif
</td>
<td class="px-6 py-6 whitespace-nowrap">
<x-status-badge :status="$code->isValid() ? 'active' : 'disabled'" />
</td>
<td class="px-6 py-6 whitespace-nowrap text-right">
<div class="flex items-center justify-end gap-2">
<form id="delete-form-desktop-{{ $code->id }}" action="{{ route('admin.sales.pass-codes.destroy', $code) }}" method="POST">
@csrf
@method('DELETE')
<button type="button" @click="confirmDelete('delete-form-desktop-{{ $code->id }}')" class="p-2 text-rose-500 hover:bg-rose-500/10 rounded-xl transition-all duration-200" title="{{ __('Delete Code') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v2m3 4h.01" />
</svg>
</button>
</form>
</div>
</td>
</tr>
@if($code->status === 'active')
<form id="delete-form-desktop-{{ $code->id }}"
action="{{ route('admin.sales.pass-codes.destroy', $code) }}" method="POST">
@csrf
@method('DELETE')
<button type="button"
@click="confirmDelete('delete-form-desktop-{{ $code->id }}')"
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="{{ __('Cancel Code') }}">
<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 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-3.38a2.25 2.25 0 00-2.25-2.25h-3.51a2.25 2.25 0 00-2.25 2.25v3.38" />
</svg>
</button>
</form>
@endif
</div>
</td>
</tr>
@empty
<x-empty-state mode="table" :colspan="5" :message="__('No pass codes found')" />
<x-empty-state mode="table" :colspan="5" :message="__('No pass codes found')" />
@endforelse
</tbody>
</table>
@ -163,59 +322,99 @@
{{-- Card Grid (Mobile) --}}
<div class="xl:hidden grid grid-cols-1 md:grid-cols-2 gap-6">
@forelse ($passCodes as $code)
<div class="luxury-card p-6 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group">
{{-- Card Header --}}
<div class="flex items-start justify-between gap-4 mb-6">
<div class="flex items-center gap-4 min-w-0">
<div class="w-14 h-14 rounded-2xl 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 overflow-hidden shadow-sm shrink-0">
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
</div>
<div class="min-w-0">
<h3 class="text-base font-extrabold text-slate-800 dark:text-slate-100 truncate hover:text-cyan-600 dark:hover:text-cyan-400 transition-colors tracking-tight">
{{ $code->code }}
</h3>
<p class="text-xs font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest truncate">
{{ $code->name }}
</p>
</div>
<div
class="luxury-card p-6 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group">
{{-- Card Header --}}
<div class="flex items-start justify-between gap-4 mb-6">
<div class="flex items-center gap-4 min-w-0">
<div
class="w-14 h-14 rounded-2xl 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 overflow-hidden shadow-sm shrink-0">
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
</div>
<x-status-badge :status="$code->isValid() ? 'active' : 'disabled'" size="sm" />
</div>
{{-- Info Grid --}}
<div class="grid grid-cols-2 gap-y-4 mb-6 border-y border-slate-100 dark:border-slate-800/50 py-4">
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Machine') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 truncate">{{ $code->machine->name }}</p>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Expires At') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 font-mono tracking-tighter">
{{ $code->expires_at ? $code->expires_at->format('Y-m-d H:i') : __('Permanent') }}
<div class="min-w-0">
@if($code->status === 'active' && (!$code->expires_at || $code->expires_at->isFuture()))
<h3 @click="activeQrCode = '{{ $code->code }}'; activeTicketUrl = '{{ $code->ticket_url }}'; showQrModal = true"
class="text-base font-extrabold text-slate-800 dark:text-slate-100 truncate hover:text-cyan-600 dark:hover:text-cyan-400 transition-colors tracking-tight cursor-pointer">
{{ $code->code }}
</h3>
@else
<h3
class="text-base font-extrabold text-slate-400 dark:text-slate-600 truncate tracking-tight">
{{ $code->code }}
</h3>
@endif
<p
class="text-xs font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest truncate">
{{ $code->name }}
</p>
</div>
</div>
<div class="min-w-0">
@php
$displayStatusMobile = $code->status === 'active' && $code->expires_at?->isPast() ? 'expired' : $code->status;
@endphp
<x-status-badge :status="$displayStatusMobile" size="sm" />
</div>
</div>
{{-- Action Buttons --}}
<div class="flex items-center gap-3">
<form id="delete-form-mobile-{{ $code->id }}" action="{{ route('admin.sales.pass-codes.destroy', $code) }}" method="POST" class="flex-1">
@csrf
@method('DELETE')
<button type="button" @click="confirmDelete('delete-form-mobile-{{ $code->id }}')" class="w-full flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-500 dark:text-slate-400 font-bold text-xs hover:bg-rose-500 hover:text-white transition-all duration-300 border border-slate-200/50 dark:border-slate-700/50">
<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" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v2m3 4h.01" />
</svg>
{{ __('Delete') }}
</button>
</form>
{{-- Info Grid --}}
<div class="grid grid-cols-2 gap-y-4 mb-6 border-y border-slate-100 dark:border-slate-800/50 py-4">
<div>
<p
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">
{{ __('Machine') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 truncate">{{
$code->machine->name }}</p>
</div>
<div>
<p
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">
{{ __('Expires At') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 font-mono tracking-tighter">
{{ $code->expires_at ? $code->expires_at->format('Y-m-d H:i') : __('Permanent') }}
</p>
</div>
</div>
@empty
<div class="col-span-full">
<x-empty-state :message="__('No pass codes found')" />
{{-- Action Buttons --}}
<div class="mt-6 pt-6 border-t border-slate-100 dark:border-slate-800 flex gap-3">
@if($code->status === 'active' && (!$code->expires_at || $code->expires_at->isFuture()))
<button type="button"
@click="activeQrCode = '{{ $code->code }}'; activeTicketUrl = '{{ $code->ticket_url }}'; showQrModal = true"
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-black text-xs uppercase tracking-widest border border-slate-100 dark:border-slate-800 hover:text-emerald-500 hover:bg-emerald-500/5 transition-all duration-300">
<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="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 6.75h.75v.75h-.75v-.75zM6.75 16.5h.75v.75h-.75v-.75zM16.5 6.75h.75v.75h-.75v-.75zM13.5 13.5h.75v.75h-.75v-.75zM13.5 19.5h.75v.75h-.75v-.75zM19.5 13.5h.75v.75h-.75v-.75zM19.5 19.5h.75v.75h-.75v-.75zM16.5 16.5h.75v.75h-.75v-.75z" />
</svg>
{{ __('QR Code') }}
</button>
@endif
@if($code->status === 'active')
<form id="delete-form-mobile-{{ $code->id }}"
action="{{ route('admin.sales.pass-codes.destroy', $code) }}" method="POST"
class="flex-1">
@csrf
@method('DELETE')
<button type="button" @click="confirmDelete('delete-form-mobile-{{ $code->id }}')"
class="w-full flex items-center justify-center gap-2 py-3 rounded-xl bg-rose-500/5 text-rose-500 text-xs font-black uppercase tracking-widest border border-rose-500/20 hover:bg-rose-500 hover:text-white transition-all duration-300">
<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 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-3.38a2.25 2.25 0 00-2.25-2.25h-3.51a2.25 2.25 0 00-2.25 2.25v3.38" />
</svg>
{{ __('Cancel') }}
</button>
</form>
@endif
</div>
</div>
@empty
<div class="col-span-full">
<x-empty-state :message="__('No pass codes found')" />
</div>
@endforelse
</div>
@ -227,67 +426,123 @@
</div>
{{-- Create Modal --}}
<div x-show="showCreateModal"
class="fixed inset-0 z-50 overflow-y-auto"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
x-cloak>
<div x-show="showCreateModal" class="fixed inset-0 z-50 overflow-y-auto"
x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
<div class="flex items-center justify-center min-h-screen px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 transition-opacity bg-slate-900/60 backdrop-blur-sm" @click="showCreateModal = false"></div>
<div class="fixed inset-0 transition-opacity bg-slate-900/60 backdrop-blur-sm"
@click="showCreateModal = false"></div>
<div x-show="showCreateModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4 sm:scale-95" x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100 sm:scale-100" x-transition:leave-end="opacity-0 translate-y-4 sm:scale-95"
<div x-show="showCreateModal" x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:scale-95"
class="inline-block px-8 py-10 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-2xl sm:w-full overflow-visible">
<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">{{ __('Add Pass Code') }}</h3>
<button @click="showCreateModal = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12"/></svg>
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{
__('Add Pass Code') }}</h3>
<button @click="showCreateModal = 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 action="{{ route('admin.sales.pass-codes.store') }}" method="POST" class="space-y-6">
<form action="{{ route('admin.sales.pass-codes.store') }}" method="POST" class="space-y-6" @submit.prevent="validateAndSubmit">
@csrf
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<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" x-model="selectedMachine" class="luxury-input w-full" required>
<option value="">{{ __('Please select a machine') }}</option>
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
{{ __('Target Machine') }} <span class="text-rose-500">*</span>
</label>
<x-searchable-select name="machine_id" id="modal-pass-machine"
:placeholder="__('Please select a machine')" x-model="selectedMachine"
@change="selectedMachine = $event.target.value">
@foreach($machines as $machine)
<option value="{{ $machine->id }}">{{ $machine->name }} ({{ $machine->serial_no }})</option>
<option value="{{ $machine->id }}"
data-title="{{ $machine->name }} ({{ $machine->serial_no }})">{{ $machine->name }}
({{ $machine->serial_no }})</option>
@endforeach
</select>
</x-searchable-select>
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Description / Name') }}</label>
<input type="text" name="name" x-model="name" class="luxury-input w-full" placeholder="{{ __('e.g. Test Code for Maintenance') }}" required>
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
{{ __('Description / Name') }} <span class="text-rose-500">*</span>
</label>
<input type="text" name="name" x-model="passCodeName" class="luxury-input w-full"
placeholder="{{ __('e.g. Test Code for Maintenance') }}">
</div>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between mb-1">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Pass Code (8 Digits)') }}</label>
<button type="button" @click="generateRandomCode()" class="text-[10px] font-black text-cyan-500 hover:text-cyan-600 uppercase tracking-widest flex items-center gap-1 transition-colors">
<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="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>
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
{{ __('Pass Code (8 Digits)') }} <span class="text-rose-500">*</span>
</label>
<button type="button" @click="generateRandomCode()"
class="text-[10px] font-black text-cyan-500 hover:text-cyan-600 uppercase tracking-widest flex items-center gap-1 transition-colors">
<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="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>
{{ __('Regenerate') }}
</button>
</div>
<input type="text" name="custom_code" x-model="customCode" class="luxury-input w-full py-4 font-mono text-2xl tracking-[0.3em] text-center text-cyan-600 dark:text-cyan-400 bg-cyan-50/30 dark:bg-cyan-500/5" maxlength="12" required>
<input type="text" name="custom_code" x-model="customCode"
class="luxury-input w-full py-4 font-mono text-2xl tracking-[0.3em] text-center text-cyan-600 dark:text-cyan-400 bg-cyan-50/30 dark:bg-cyan-500/5"
maxlength="12" required>
</div>
<div class="space-y-2">
<div class="space-y-4">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Validity Period (Days)') }}</label>
<div class="relative">
<input type="number" name="expires_days" x-model="expiresDays" class="luxury-input w-full pr-16" min="1" placeholder="{{ __('Permanent') }}">
<div class="absolute right-4 top-1/2 -translate-y-1/2 text-xs font-bold text-slate-400 uppercase tracking-widest pointer-events-none">{{ __('Days') }}</div>
<div class="bg-slate-50 dark:bg-slate-800/40 rounded-[2rem] p-6 border border-slate-100 dark:border-slate-800/50 shadow-sm">
<div class="flex items-center gap-6 mb-6">
<button type="button" @click="expiresDays = Math.max(0, parseInt(expiresDays || 0) - 1)"
class="shrink-0 w-12 h-12 rounded-xl bg-white dark:bg-slate-800 flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:shadow-lg active:scale-90 transition-all border border-slate-100 dark:border-slate-800">
<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="M20 12H4" />
</svg>
</button>
<div class="flex-1 relative">
<input type="number" name="expires_days" x-model="expiresDays"
class="w-full bg-transparent border-none text-center font-black text-3xl text-slate-800 dark:text-white focus:ring-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
placeholder="{{ __('Permanent') }}">
<div class="text-center mt-1 text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">{{ __('Days') }}</div>
</div>
<button type="button" @click="expiresDays = parseInt(expiresDays || 0) + 1"
class="shrink-0 w-12 h-12 rounded-xl bg-white dark:bg-slate-800 flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:shadow-lg active:scale-90 transition-all border border-slate-100 dark:border-slate-800">
<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 4v16m8-8H4" />
</svg>
</button>
</div>
<div class="pt-6 border-t border-slate-100 dark:border-slate-800/50">
<div class="flex items-center justify-between mb-3 px-1">
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Estimated Expiry') }}</span>
<template x-if="!expiresDays || expiresDays <= 0">
<span class="text-[10px] font-black text-emerald-500 uppercase tracking-widest px-2 py-0.5 bg-emerald-500/10 rounded-lg border border-emerald-500/20">{{ __('Permanent Code') }}</span>
</template>
</div>
<div class="bg-white dark:bg-slate-900/50 rounded-2xl p-4 border border-slate-100 dark:border-slate-700/50 flex items-center justify-center gap-3">
<svg class="w-5 h-5 text-cyan-500/50 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-xl font-mono font-black text-slate-700 dark:text-slate-200 tracking-wider" x-text="calculateExpiry()"></span>
</div>
<p class="text-center mt-3 text-[10px] font-bold text-slate-400 uppercase tracking-widest">{{ __('Leave empty or 0 for permanent code') }}</p>
</div>
</div>
<p class="text-[10px] font-bold text-slate-400 mt-1 uppercase tracking-widest">{{ __('Leave empty for permanent code') }}</p>
</div>
<div class="flex justify-end gap-x-4 pt-8">
<button type="button" @click="showCreateModal = false" class="btn-luxury-ghost px-8">{{ __('Cancel') }}</button>
<button type="button" @click="showCreateModal = false" class="btn-luxury-ghost px-8">{{
__('Cancel') }}</button>
<button type="submit" class="btn-luxury-primary px-12">
{{ __('Create') }}
</button>
@ -297,15 +552,109 @@
</div>
</div>
{{-- Confirm Delete Modal --}}
<x-confirm-modal
alpineVar="showDeleteModal"
:title="__('Delete Pass Code')"
:message="__('Are you sure you want to delete this pass code?')"
:confirmText="__('Yes, Delete')"
confirmAction="submitDelete()"
confirmColor="rose"
iconType="danger"
/>
{{-- Confirm Cancel Modal --}}
<x-confirm-modal alpineVar="showDeleteModal" :title="__('Cancel Pass Code')"
:message="__('Are you sure you want to cancel this pass code? This action cannot be undone.')" :confirmText="__('Yes, Cancel')"
confirmAction="submitDelete()" confirmColor="rose" iconType="danger" />
{{-- QR Code Modal --}}
<div x-show="showQrModal" class="fixed inset-0 z-[60] overflow-y-auto"
x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
<div class="flex items-center justify-center min-h-screen px-4">
<div class="fixed inset-0 transition-opacity bg-slate-900/60 backdrop-blur-sm" @click="showQrModal = false">
</div>
<div x-show="showQrModal" x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:scale-95"
class="inline-block w-full max-w-sm transition-all transform luxury-card rounded-[2.5rem] bg-white dark:bg-slate-900 border-slate-200/50 dark:border-slate-700/50 shadow-2xl overflow-hidden">
<div class="p-8 pb-0 flex justify-between items-start">
<div>
<h3 class="text-xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Pass Code QR') }}</h3>
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mt-1" x-text="activeQrCode"></p>
</div>
<button @click="showQrModal = false"
class="p-2 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>
<div class="p-10 flex flex-col items-center gap-6">
<div class="p-4 bg-white rounded-3xl shadow-xl border border-slate-100">
<x-qr-code data="activeQrCode" size="200" class="w-48 h-48" />
</div>
<div class="text-center w-full space-y-4">
<p class="text-xs font-bold text-slate-500 dark:text-slate-400 leading-relaxed px-4">
{{ __('Scan this code at the machine to authorize testing or maintenance.') }}
</p>
<div class="flex items-center justify-center gap-3">
{{-- Copy Link Button --}}
<button @click="copyQrLink()"
class="flex-1 py-3 px-4 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-slate-100 dark:border-slate-700 hover:border-cyan-500/20 transition-all flex items-center justify-center gap-2 group"
title="{{ __('Copy Link') }}">
<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="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
</svg>
<span class="text-xs font-black uppercase tracking-widest">{{ __('Link') }}</span>
</button>
{{-- Copy Code Button --}}
<button @click="copyQrCode()"
class="flex-1 py-3 px-4 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-slate-100 dark:border-slate-700 hover:border-cyan-500/20 transition-all flex items-center justify-center gap-2 group"
title="{{ __('Copy Code') }}">
<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="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<span class="text-xs font-black uppercase tracking-widest">{{ __('Copy') }}</span>
</button>
{{-- Download Image Button --}}
<button @click="downloadQrCode()"
class="flex-1 py-3 px-4 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:text-emerald-500 hover:bg-emerald-500/5 dark:hover:bg-emerald-500/10 border border-slate-100 dark:border-slate-700 hover:border-emerald-500/20 transition-all flex items-center justify-center gap-2 group"
title="{{ __('Download Image') }}">
<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="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M7.5 12l4.5 4.5m0 0l4.5-4.5M12 3v13.5" />
</svg>
<span class="text-xs font-black uppercase tracking-widest">{{ __('Save') }}</span>
</button>
</div>
</div>
</div>
<div class="px-8 py-6 bg-slate-50 dark:bg-slate-900/50 flex justify-center border-t border-slate-100 dark:border-slate-800">
<button @click="showQrModal = false" class="btn-luxury-primary w-full py-4 rounded-2xl">{{ __('Close') }}</button>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('styles')
<style>
/* 移除 Chrome, Safari, Edge, Opera 的原生加減按鈕 */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
appearance: none;
margin: 0;
}
/* 移除 Firefox 的原生加減按鈕 */
input[type=number] {
-moz-appearance: textfield;
appearance: textfield;
}
</style>
@endpush

File diff suppressed because it is too large Load Diff

View File

@ -270,31 +270,24 @@
<!-- Header -->
<div class="flex items-center justify-between gap-4">
<div>
<h1 class="text-2xl sm: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-in orders and movement history') }}
</p>
</div>
<div class="flex items-center gap-3">
<template x-if="activeTab === 'stock'">
<button type="button" @click="openStockInModal()"
class="btn-luxury-primary transition-all duration-300">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
<span>{{ __('New Stock-In Order') }}</span>
</button>
</template>
</div>
</div>
<x-page-header
:title="__('Inventory Management')"
:subtitle="__('Track stock-in orders and movement history')"
>
<template x-if="activeTab === 'stock'">
<button type="button" @click="openStockInModal()" 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>
<span class="text-sm sm:text-base">{{ __('Stock-In Order') }}</span>
</button>
</template>
</x-page-header>
<x-tab-nav>
<x-tab-nav-item value="stock" :label="__('Current Stocks')" />
<x-tab-nav-item value="stock-in" :label="__('Stock-In Orders')" />
<x-tab-nav-item value="movements" :label="__('Movement History')" />
<x-tab-nav model="activeTab">
<x-tab-nav-item value="stock" :label="__('Adjust Inventory')" model="activeTab" />
<x-tab-nav-item value="stock-in" :label="__('Stock-In Orders')" model="activeTab" />
<x-tab-nav-item value="movements" :label="__('Operation Records')" model="activeTab" />
</x-tab-nav>
<!-- Tab Contents -->

View File

@ -1,88 +1,89 @@
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
<form action="{{ route('admin.warehouses.inventory') }}" method="GET" class="flex flex-wrap items-center justify-between mb-8 gap-4"
<form action="{{ route('admin.warehouses.inventory') }}" method="GET" class="flex flex-wrap items-center gap-3 sm:gap-4 mb-8"
@submit.prevent="handleFilterSubmit('movements')">
<input type="hidden" name="tab" value="movements">
<div class="flex flex-wrap gap-4 items-center flex-1">
<div class="w-full sm:w-64">
<x-searchable-select
name="warehouse_id"
:options="$warehouses"
:selected="request('warehouse_id')"
:placeholder="__('All Warehouses')"
/>
</div>
{{-- 倉庫篩選 --}}
<div class="w-full sm:w-72">
<x-searchable-select
name="warehouse_id"
:options="$warehouses"
:selected="request('warehouse_id')"
:placeholder="__('All Warehouses')"
/>
</div>
<div class="w-full sm:w-64">
<x-searchable-select
name="type"
:selected="request('type')"
:placeholder="__('All Types')"
>
@foreach(\App\Models\Warehouse\StockMovement::TYPE_LABELS as $key => $label)
<option value="{{ $key }}" {{ request('type') === $key ? 'selected' : '' }} data-title="{{ __($label) }}">
{{ __($label) }}
</option>
@endforeach
</x-searchable-select>
</div>
{{-- 類型篩選 --}}
<div class="w-full sm:w-72">
<x-searchable-select
name="type"
:selected="request('type')"
:placeholder="__('All Types')"
>
@foreach(\App\Models\Warehouse\StockMovement::TYPE_LABELS as $key => $label)
<option value="{{ $key }}" {{ request('type') === $key ? 'selected' : '' }} data-title="{{ __($label) }}">
{{ __($label) }}
</option>
@endforeach
</x-searchable-select>
</div>
{{-- Date Range --}}
<div class="relative group w-full sm:w-72"
x-data="{ fp: null }"
x-init="fp = flatpickr($refs.dateRange, {
mode: 'range',
dateFormat: 'Y-m-d H:i',
enableTime: true,
time_24hr: true,
defaultHour: 0,
defaultMinute: 0,
locale: 'zh_tw',
defaultDate: '{{ request('start_date') }}' && '{{ request('end_date') }}' ? ['{{ request('start_date') }}', '{{ request('end_date') }}'] : [],
onChange: function(selectedDates, dateStr, instance) {
if (selectedDates.length === 2) {
$refs.startDate.value = instance.formatDate(selectedDates[0], 'Y-m-d H:i');
$refs.endDate.value = instance.formatDate(selectedDates[1], 'Y-m-d H:i');
} else if (selectedDates.length === 0) {
$refs.startDate.value = '';
$refs.endDate.value = '';
}
{{-- 日期範圍 --}}
<div class="relative group w-full sm:w-72 sm:flex-none"
x-data="{ fp: null }"
x-init="fp = flatpickr($refs.dateRange, {
mode: 'range',
dateFormat: 'Y-m-d H:i',
enableTime: true,
time_24hr: true,
defaultHour: 0,
defaultMinute: 0,
locale: 'zh_tw',
defaultDate: '{{ request('start_date') }}' && '{{ request('end_date') }}' ? ['{{ request('start_date') }}', '{{ request('end_date') }}'] : [],
onChange: function(selectedDates, dateStr, instance) {
if (selectedDates.length === 2) {
$refs.startDate.value = instance.formatDate(selectedDates[0], 'Y-m-d H:i');
$refs.endDate.value = instance.formatDate(selectedDates[1], 'Y-m-d H:i');
} else if (selectedDates.length === 0) {
$refs.startDate.value = '';
$refs.endDate.value = '';
}
})">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10 text-slate-400 group-focus-within:text-cyan-500 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
</span>
<input type="hidden" name="start_date" x-ref="startDate" value="{{ request('start_date') }}">
<input type="hidden" name="end_date" x-ref="endDate" value="{{ request('end_date') }}">
<input type="text" x-ref="dateRange"
value="{{ request('start_date') && request('end_date') ? request('start_date') . ' 至 ' . request('end_date') : (request('start_date') ?: '') }}"
placeholder="{{ __('Select Date Range') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer">
</div>
}
})">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10 text-slate-400 group-focus-within:text-cyan-500 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
</span>
<input type="hidden" name="start_date" x-ref="startDate" value="{{ request('start_date') }}">
<input type="hidden" name="end_date" x-ref="endDate" value="{{ request('end_date') }}">
<input type="text" x-ref="dateRange"
value="{{ request('start_date') && request('end_date') ? request('start_date') . ' 至 ' . request('end_date') : (request('start_date') ?: '') }}"
placeholder="{{ __('Select Date Range') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full text-sm font-bold cursor-pointer">
</div>
<div class="flex items-center gap-2 ml-auto sm:ml-0">
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group transition-all active:scale-95" title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
{{-- 搜尋 + 重置按鈕 --}}
<div class="flex items-center gap-2 ml-auto sm:ml-0">
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 transition-all active:scale-95" title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
<button type="button"
@click="
$el.closest('form').querySelectorAll('input[type=text], input[type=hidden]:not([name=tab])').forEach(i => i.value = '');
$el.closest('form').querySelectorAll('select').forEach(s => {
s.value = ' ';
const instance = window.HSSelect.getInstance(s);
if (instance) instance.setValue(' ');
});
$el.closest('form').dispatchEvent(new CustomEvent('multi-select-reset', { bubbles: true }));
handleFilterSubmit('movements');
"
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group transition-all active:scale-95" title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
<button type="button"
@click="
$el.closest('form').querySelectorAll('input[type=text], input[type=hidden]:not([name=tab])').forEach(i => i.value = '');
$el.closest('form').querySelectorAll('select').forEach(s => {
s.value = ' ';
const instance = window.HSSelect.getInstance(s);
if (instance) instance.setValue(' ');
});
$el.closest('form').dispatchEvent(new CustomEvent('multi-select-reset', { bubbles: true }));
handleFilterSubmit('movements');
"
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all active:scale-95" title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
</form>
@ -166,18 +167,18 @@
</div>
{{-- Card Mode (Mobile & Tablet) --}}
<div class="grid grid-cols-1 md:grid-cols-2 xl:hidden gap-6 mt-8">
<div class="xl:hidden grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
@forelse($movements as $mv)
<div class="luxury-card p-6 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group">
<div class="flex items-start justify-between gap-4 mb-6">
<div class="flex items-center gap-4 min-w-0">
<div class="luxury-card p-5 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group">
<div class="flex items-start justify-between gap-4 mb-5">
<div class="flex items-center gap-3 min-w-0">
<div class="w-14 h-14 rounded-2xl 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 overflow-hidden shadow-sm shrink-0">
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<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="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />
</svg>
</div>
<div class="min-w-0 flex-1">
<h3 class="text-base sm:text-lg font-black text-slate-800 dark:text-slate-100 break-all group-hover:text-cyan-600 transition-colors tracking-tight">{{ $mv->product?->localized_name }}</h3>
<h3 class="text-base font-extrabold text-slate-800 dark:text-slate-100 break-all group-hover:text-cyan-600 transition-colors tracking-tight">{{ $mv->product?->localized_name }}</h3>
<p class="text-[10px] font-mono font-bold text-slate-400 uppercase tracking-widest truncate mt-0.5">ID: {{ $mv->product_id }}</p>
</div>
</div>
@ -213,8 +214,8 @@
</div>
@empty
<div class="col-span-full py-20 text-center flex flex-col items-center gap-4 bg-white/50 dark:bg-slate-900/50 rounded-3xl border border-slate-100 dark:border-slate-800/50 luxury-card">
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No movement records found') }}</p>
<div class="col-span-full">
<x-empty-state :message="__('No movement records found')" />
</div>
@endforelse
</div>

View File

@ -1,11 +1,11 @@
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
{{-- Filters --}}
<form action="{{ route('admin.warehouses.inventory') }}" method="GET" class="flex flex-wrap items-center justify-between mb-8 gap-4"
<form action="{{ route('admin.warehouses.inventory') }}" method="GET" class="flex flex-wrap items-center gap-3 sm:gap-4 mb-8"
@submit.prevent="handleFilterSubmit('stock-in')">
<input type="hidden" name="tab" value="stock-in">
<div class="flex flex-wrap gap-4 items-center flex-1">
<div class="relative group w-full sm:w-72">
{{-- 搜尋輸入框 --}}
<div class="relative group flex-1 sm:flex-none">
<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">
@ -13,13 +13,13 @@
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</span>
<input type="text" name="search_order" value="{{ request('search_order') }}"
@keydown.enter.prevent="handleFilterSubmit('stock-in')"
class="py-2.5 pl-12 pr-6 block w-full luxury-input" placeholder="{{ __('Search order number...') }}">
</div>
<input type="text" name="search_order" value="{{ request('search_order') }}"
@keydown.enter.prevent="handleFilterSubmit('stock-in')"
class="luxury-input py-2.5 pl-12 pr-6 block w-full sm:w-72 text-sm font-bold" placeholder="{{ __('Search order number...') }}">
</div>
{{-- Date Range --}}
<div class="relative group w-full sm:w-72"
{{-- 日期範圍 --}}
<div class="relative group w-full sm:w-72 sm:flex-none"
x-data="{ fp: null }"
x-init="fp = flatpickr($refs.dateRange, {
mode: 'range',
@ -45,36 +45,12 @@
</span>
<input type="hidden" name="start_date" x-ref="startDate" value="{{ request('start_date') }}">
<input type="hidden" name="end_date" x-ref="endDate" value="{{ request('end_date') }}">
<input type="text" x-ref="dateRange"
value="{{ request('start_date') && request('end_date') ? request('start_date') . ' 至 ' . request('end_date') : (request('start_date') ?: '') }}"
placeholder="{{ __('Select Date Range') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer">
</div>
<div class="flex items-center gap-2 ml-auto sm:ml-0">
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group transition-all active:scale-95" title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
<button type="button"
@click="
$el.closest('form').querySelectorAll('input[type=text], input[type=hidden]:not([name=tab])').forEach(i => i.value = '');
$el.closest('form').querySelectorAll('select').forEach(s => {
s.value = ' ';
const instance = window.HSSelect.getInstance(s);
if (instance) instance.setValue(' ');
});
handleFilterSubmit('stock-in');
"
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group transition-all active:scale-95" title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
<input type="text" x-ref="dateRange"
value="{{ request('start_date') && request('end_date') ? request('start_date') . ' 至 ' . request('end_date') : (request('start_date') ?: '') }}"
placeholder="{{ __('Select Date Range') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full text-sm font-bold cursor-pointer">
</div>
{{-- 狀態篩選 --}}
@if(isset($stock_in_status_options))
<div class="w-full sm:w-48">
<x-searchable-select
@ -89,6 +65,31 @@
</x-searchable-select>
</div>
@endif
{{-- 搜尋 + 重置按鈕 --}}
<div class="flex items-center gap-2 ml-auto sm:ml-0">
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 transition-all active:scale-95" title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
<button type="button"
@click="
$el.closest('form').querySelectorAll('input[type=text], input[type=hidden]:not([name=tab])').forEach(i => i.value = '');
$el.closest('form').querySelectorAll('select').forEach(s => {
s.value = ' ';
const instance = window.HSSelect.getInstance(s);
if (instance) instance.setValue(' ');
});
handleFilterSubmit('stock-in');
"
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all active:scale-95" title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
</form>
{{-- Table Mode (Desktop) --}}
@ -223,19 +224,19 @@
</div>
{{-- Card Mode (Mobile & Tablet) --}}
<div class="grid grid-cols-1 md:grid-cols-2 xl:hidden gap-6 mt-8">
<div class="xl:hidden grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
@forelse($orders as $order)
<div class="luxury-card p-6 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group"
<div class="luxury-card p-5 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group cursor-pointer"
@click="openOrderDetails('{{ $order->id }}')">
<div class="flex items-start justify-between gap-4 mb-6">
<div class="flex items-center gap-4 min-w-0">
<div class="flex items-start justify-between gap-4 mb-5">
<div class="flex items-center gap-3 min-w-0">
<div class="w-14 h-14 rounded-2xl 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 overflow-hidden shadow-sm shrink-0">
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<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="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
</div>
<div class="min-w-0 flex-1">
<h3 class="text-base sm:text-lg font-black text-slate-800 dark:text-slate-100 break-all group-hover:text-cyan-600 transition-colors tracking-tight">{{ $order->order_no }}</h3>
<h3 class="text-base font-extrabold text-slate-800 dark:text-slate-100 break-all group-hover:text-cyan-600 transition-colors tracking-tight">{{ $order->order_no }}</h3>
<p class="text-[10px] font-mono font-bold text-slate-400 uppercase tracking-widest truncate mt-0.5">{{ $order->created_at->format('Y-m-d H:i:s') }}</p>
</div>
</div>
@ -256,7 +257,7 @@
</div>
</div>
<div class="grid grid-cols-2 gap-y-4 mb-6 border-y border-slate-100 dark:border-slate-800/50 py-4">
<div class="grid grid-cols-2 gap-y-4 mb-5 border-y border-slate-100 dark:border-slate-800/50 py-4">
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Warehouse') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 truncate">{{ $order->warehouse?->name }}</p>
@ -292,8 +293,8 @@
</div>
</div>
@empty
<div class="col-span-full py-20 text-center flex flex-col items-center gap-4 bg-white/50 dark:bg-slate-900/50 rounded-3xl border border-slate-100 dark:border-slate-800/50 luxury-card">
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No stock-in orders found') }}</p>
<div class="col-span-full">
<x-empty-state :message="__('No stock-in orders found')" />
</div>
@endforelse
</div>

View File

@ -3,7 +3,8 @@
@submit.prevent="handleFilterSubmit('stock')">
<input type="hidden" name="tab" value="stock">
<div class="relative group w-full sm:w-72">
{{-- 搜尋輸入框 --}}
<div class="relative group flex-1 sm:flex-none">
<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">
@ -11,7 +12,7 @@
<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 luxury-input" placeholder="{{ __('Search products...') }}"
class="py-2.5 pl-12 pr-6 block w-full sm:w-72 luxury-input text-sm font-bold" placeholder="{{ __('Search products...') }}"
@keydown.enter.prevent="handleFilterSubmit('stock')">
</div>
@ -163,26 +164,26 @@
</div>
{{-- Mobile Card View --}}
<div class="grid grid-cols-1 md:grid-cols-2 xl:hidden gap-6 mt-8 items-start">
<div class="xl:hidden grid grid-cols-1 md:grid-cols-2 gap-4 mt-6 items-start">
@forelse($products as $product)
@php
$viewableWarehouseIds = $inventory_warehouses->pluck('id')->toArray();
$totalQuantity = $product->stocks->whereIn('warehouse_id', $viewableWarehouseIds)->sum('quantity');
@endphp
<div x-data="{ localCardOpen: false }"
class="luxury-card p-6 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group">
class="luxury-card p-5 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group">
{{-- Card Header --}}
<div class="flex items-center justify-between gap-4 cursor-pointer" @click.stop="localCardOpen = !localCardOpen">
<div class="flex items-center gap-4 min-w-0">
<div class="flex items-center gap-3 min-w-0">
<div
class="w-14 h-14 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center overflow-hidden border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 transition-all duration-300 shadow-sm shrink-0">
@if($product->image_url)
<img src="{{ $product->image_url }}"
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500">
@else
<svg class="w-7 h-7 text-slate-400 group-hover:text-white transition-colors" fill="none"
stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<svg class="w-6 h-6 text-slate-400 group-hover:text-white transition-colors" fill="none"
stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<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>
@ -190,7 +191,7 @@
</div>
<div class="min-w-0">
<h3
class="text-lg font-black text-slate-800 dark:text-slate-100 truncate group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight">
class="text-base font-extrabold text-slate-800 dark:text-slate-100 truncate group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight">
{{ $product->localized_name }}
</h3>
@if($product->barcode)
@ -252,17 +253,8 @@
</div>
</div>
@empty
<div class="px-6 py-20 text-center luxury-card rounded-[2rem]">
<div class="flex flex-col items-center">
<div
class="w-16 h-16 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center mb-4">
<svg class="w-8 h-8 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div>
<p class="text-slate-400 font-bold tracking-widest uppercase">{{ __('No products found') }}</p>
</div>
<div class="col-span-full">
<x-empty-state :message="__('No products found')" />
</div>
@endforelse
</div>

View File

@ -44,15 +44,22 @@
<div class="flex items-center justify-between gap-4">
{{-- 左側:標題 + 副標題 --}}
<div class="min-w-0">
<h1 class="text-2xl sm:text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display transition-all duration-300 truncate">
{{ $title }}
</h1>
@if($subtitle)
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest truncate">
{{ $subtitle }}
</p>
<div class="flex items-center gap-4 min-w-0">
@if(isset($back))
<div class="shrink-0">
{{ $back }}
</div>
@endif
<div class="min-w-0">
<h1 class="text-2xl sm:text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display transition-all duration-300 truncate">
{{ $title }}
</h1>
@if($subtitle)
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest truncate">
{{ $subtitle }}
</p>
@endif
</div>
</div>
{{-- 右側Action 按鈕(透過 slot 傳入,無傳入則不渲染) --}}

View File

@ -25,9 +25,36 @@
'model' => 'activeTab',
])
<div
class="flex items-center gap-1 p-1.5 bg-slate-100 dark:bg-slate-900/50 rounded-2xl w-full sm:w-fit border border-slate-200/50 dark:border-slate-800/50 overflow-x-auto whitespace-nowrap custom-scrollbar"
aria-label="Tabs"
>
{{ $slot }}
<div x-data="{
isScrollable: false,
isScrolledToRight: true,
checkScroll() {
if (!this.$refs.tabContainer) return;
const el = this.$refs.tabContainer;
this.isScrollable = el.scrollWidth > el.clientWidth;
this.isScrolledToRight = Math.abs(el.scrollWidth - el.clientWidth - el.scrollLeft) < 5;
}
}"
x-init="checkScroll(); window.addEventListener('resize', () => checkScroll())"
class="relative w-full sm:w-fit group">
<div
x-ref="tabContainer"
@scroll="checkScroll"
class="flex items-center gap-1 p-1.5 bg-slate-100 dark:bg-slate-900/50 rounded-2xl w-full sm:w-fit border border-slate-200/50 dark:border-slate-800/50 overflow-x-auto whitespace-nowrap custom-scrollbar [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]"
aria-label="Tabs"
>
{{ $slot }}
</div>
<!-- Right Fade & Arrow Indicator -->
<div x-show="isScrollable && !isScrolledToRight"
x-transition.opacity
class="absolute right-0 top-0 bottom-0 w-16 bg-gradient-to-l from-slate-100 dark:from-slate-900/90 to-transparent pointer-events-none rounded-r-2xl flex items-center justify-end pr-2 z-10">
<div class="w-6 h-6 flex items-center justify-center bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm rounded-full shadow-sm text-slate-400 dark:text-slate-500 animate-pulse border border-slate-200/50 dark:border-slate-700/50">
<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="3" d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</div>

View File

@ -0,0 +1,95 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="h-full">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
<title>{{ __('Pass Code Ticket') }} - {{ config('app.name') }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;900&display=swap" rel="stylesheet">
<style>
[x-cloak] { display: none !important; }
body { font-family: 'Outfit', sans-serif; }
</style>
</head>
<body class="h-full bg-slate-50 dark:bg-slate-950 flex flex-col items-center justify-center p-6 antialiased">
<div class="w-full max-w-sm animate-luxury-in">
{{-- Header Area --}}
<div class="text-center mb-8">
<div class="inline-flex items-center gap-2 px-4 py-2 bg-white dark:bg-white/5 rounded-full shadow-sm border border-slate-100 dark:border-slate-800 mb-6">
<div class="w-2 h-2 rounded-full bg-cyan-500 animate-pulse"></div>
<span class="text-[10px] font-black uppercase tracking-[0.2em] text-slate-500 dark:text-slate-400">{{ __('Valid Authorization') }}</span>
</div>
<h1 class="text-4xl font-black text-slate-800 dark:text-white tracking-tight mb-2 font-display uppercase italic">{{ __('Pass') }} <span class="text-cyan-500">{{ __('Code') }}</span></h1>
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest">{{ __('Scan to authorize testing or maintenance') }}</p>
</div>
{{-- Ticket Card --}}
<div class="relative group">
{{-- Background Glow --}}
<div class="absolute -inset-4 bg-gradient-to-tr from-cyan-500/20 to-emerald-500/20 blur-3xl opacity-50 transition-opacity duration-500 group-hover:opacity-100"></div>
<div class="relative luxury-card rounded-[3rem] overflow-hidden border-slate-200/50 dark:border-slate-700/50 shadow-2xl bg-white dark:bg-slate-900">
{{-- Machine Section --}}
<div class="px-8 pt-10 pb-6 border-b border-dashed border-slate-100 dark:border-slate-800 relative">
{{-- Notch cutouts for ticket look --}}
<div class="absolute -left-3 -bottom-3 w-6 h-6 bg-slate-50 dark:bg-slate-950 rounded-full border-r border-slate-100 dark:border-slate-800"></div>
<div class="absolute -right-3 -bottom-3 w-6 h-6 bg-slate-50 dark:bg-slate-950 rounded-full border-l border-slate-100 dark:border-slate-800"></div>
<div class="flex items-center gap-4 mb-6">
<div class="w-12 h-12 rounded-xl bg-slate-50 dark:bg-slate-800 border border-slate-100 dark:border-slate-700 p-2 flex items-center justify-center overflow-hidden">
<svg class="w-6 h-6 text-cyan-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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 class="flex-1">
<h2 class="text-xl font-black text-slate-800 dark:text-white tracking-tight leading-tight mb-1">
{{ $passCode->machine->name }}
</h2>
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{{ $passCode->machine->serial_no }}</p>
</div>
</div>
<div class="flex items-center justify-between">
<div class="text-center flex-1">
<span class="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Purpose') }}</span>
<span class="text-sm font-black text-slate-800 dark:text-white">{{ $passCode->name ?? __('Testing') }}</span>
</div>
<div class="w-px h-8 bg-slate-100 dark:bg-slate-800"></div>
<div class="text-center flex-1">
<span class="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Status') }}</span>
<span class="text-xs font-black px-3 py-1 rounded-full bg-cyan-500/10 text-cyan-500 uppercase tracking-tighter">{{ __('Active') }}</span>
</div>
</div>
</div>
{{-- QR Section --}}
<div class="p-10 flex flex-col items-center">
<div class="p-6 bg-white rounded-[2.5rem] shadow-xl border border-slate-100 mb-8 transform transition-transform duration-500 group-hover:scale-[1.02]">
<x-qr-code :data="$passCode->code" :dynamic="false" size="300" class="w-48 h-48 sm:w-64 sm:h-64" />
</div>
<div class="text-center space-y-2 mb-2">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em]">{{ __('Pass Code') }}</p>
<p class="text-5xl font-black text-slate-800 dark:text-white tracking-tighter font-display italic leading-none">{{ $passCode->code }}</p>
</div>
</div>
{{-- Expiry Area --}}
<div class="px-8 py-6 bg-slate-50 dark:bg-slate-900/50 border-t border-slate-100 dark:border-slate-800 text-center">
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-1">{{ __('Expiry Time') }}</p>
<p class="text-sm font-black text-slate-600 dark:text-slate-300">
{{ $passCode->expires_at ? $passCode->expires_at->format('Y/m/d H:i') : __('Permanent') }}
</p>
</div>
</div>
</div>
{{-- Footer Branding --}}
<div class="mt-12 text-center">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-[0.4em] opacity-50">{{ config('app.name') }} &copy; {{ date('Y') }}</p>
</div>
</div>
</body>
</html>

View File

@ -172,12 +172,30 @@
</button>
<div x-show="open && !sidebarCollapsed" 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.sales.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.sales.index') }}"><span class="whitespace-nowrap overflow-hidden transition-all duration-300" :class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">{{ __('Sales Records') }}</span></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.sales.pickup-codes') ? '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.sales.pickup-codes') }}"><span class="whitespace-nowrap overflow-hidden transition-all duration-300" :class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">{{ __('Pickup Codes') }}</span></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.sales.orders') ? '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.sales.orders') }}"><span class="whitespace-nowrap overflow-hidden transition-all duration-300" :class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">{{ __('Orders') }}</span></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.sales.promotions') ? '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.sales.promotions') }}"><span class="whitespace-nowrap overflow-hidden transition-all duration-300" :class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">{{ __('Promotions') }}</span></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.sales.pass-codes') ? '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.sales.pass-codes') }}"><span class="whitespace-nowrap overflow-hidden transition-all duration-300" :class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">{{ __('Pass Codes') }}</span></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.sales.store-gifts') ? '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.sales.store-gifts') }}"><span class="whitespace-nowrap overflow-hidden transition-all duration-300" :class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">{{ __('Store Gifts') }}</span></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.sales.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.sales.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="M9 12h6m-6 4h6m2 5H7a2 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>
<span class="whitespace-nowrap overflow-hidden transition-all duration-300" :class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">{{ __('Sales Records') }}</span>
</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.sales.pickup-codes') ? '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.sales.pickup-codes') }}">
<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="M16.5 6v.75m0 3v.75m0 3v.75m0 3V18m-9-5.25h.008v.008H7.5V12.75zm3 0h.008v.008H10.5V12.75zm3 0h.008v.008H13.5V12.75zm-6 3h.008v.008H7.5v-.008zm3 0h.008v.008H10.5v-.008zm3 0h.008v.008H13.5v-.008zm-6 3h.008v.008H7.5v-.008zm3 0h.008v.008H10.5v-.008zm3 0h.008v.008H13.5v-.008zM9 6.75h.008v.008H9V6.75zm3 0h.008v.008H12V6.75zm3 0h.008v.008H15V6.75zm-6 3h.008v.008H9V9.75zm3 0h.008v.008H12V9.75zm3 0h.008v.008H15V9.75zM3.75 3h16.5c.69 0 1.25.56 1.25 1.25v15.5c0 .69-.56 1.25-1.25 1.25H3.75c-.69 0-1.25-.56-1.25-1.25V4.25C2.5 3.56 3.06 3 3.75 3z" /></svg>
<span class="whitespace-nowrap overflow-hidden transition-all duration-300" :class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">{{ __('Pickup Codes') }}</span>
</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.sales.orders') ? '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.sales.orders') }}">
<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="M15.75 10.5V6a3.75 3.75 0 10-7.5 0v4.5m11.356-1.993l1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 01-1.12-1.243l1.264-12A1.125 1.125 0 015.513 7.5h12.974c.576 0 1.059.435 1.119 1.007zM8.625 10.5a.375.375 0 11-.75 0 .375.375 0 01.75 0zm7.5 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" /></svg>
<span class="whitespace-nowrap overflow-hidden transition-all duration-300" :class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">{{ __('Orders') }}</span>
</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.sales.promotions') ? '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.sales.promotions') }}">
<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.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581a1.125 1.125 0 001.591 0l4.318-4.318a1.125 1.125 0 000-1.591l-9.581-9.581c-.422-.422-.994-.659-1.591-.659zM7.5 9a1.5 1.5 0 110-3 1.5 1.5 0 010 3z" /></svg>
<span class="whitespace-nowrap overflow-hidden transition-all duration-300" :class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">{{ __('Promotions') }}</span>
</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.sales.pass-codes') ? '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.sales.pass-codes') }}">
<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="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" /></svg>
<span class="whitespace-nowrap overflow-hidden transition-all duration-300" :class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">{{ __('Pass Codes') }}</span>
</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.sales.store-gifts') ? '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.sales.store-gifts') }}">
<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.25 7.5l-.625 12c-.07.665-.45 1.243-1.119 1.243H5.494c-.669 0-1.189-.578-1.119-1.243l-.625-12m16.5 0a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 7.5m17.25 0h-17.25m6 0v12m6-12v12m-9-12h12" /></svg>
<span class="whitespace-nowrap overflow-hidden transition-all duration-300" :class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">{{ __('Store Gifts') }}</span>
</a></li>
</ul>
</div>
</li>

View File

@ -26,6 +26,7 @@ Route::get('/machines/distribution', [App\Http\Controllers\Admin\BasicSettings\M
// 公開取貨憑證頁面 (無需登入)
Route::get('/p/{slug}', [App\Http\Controllers\Guest\PickupController::class, 'show'])->name('pickup.ticket')->middleware('throttle:60,1');
Route::get('/t/{slug}', [App\Http\Controllers\Guest\PassCodeController::class, 'show'])->name('pass-code.ticket')->middleware('throttle:60,1');
Route::get('/dashboard', function () {
return redirect()->route('admin.dashboard');