[FEAT] 實作通行碼編輯功能與 UI 組件優化

1. 實作通行碼 (Pass Code) 編輯功能,包含後端 Controller 邏輯與前端 Alpine.js 編輯彈窗。
2. 優化 searchable-select 組件的空值處理與樣式。
3. 更新繁體中文語系檔,統一通行碼相關操作之回傳訊息翻譯。
4. 修正通行碼更新邏輯,支援 nullable 狀態與到期日空值處理。
5. 在桌機版與手機版介面加入編輯操作入口。
This commit is contained in:
sky121113 2026-04-30 09:28:07 +08:00
parent b42695c4b8
commit 84bf7de750
6 changed files with 325 additions and 186 deletions

View File

@ -211,10 +211,17 @@ class SalesController extends Controller
$validated = $request->validate([
'name' => 'nullable|string|max:50',
'expires_at' => 'nullable|date',
'status' => 'required|in:active,disabled',
'status' => 'nullable|in:active,disabled',
]);
$passCode->update($validated);
// 確保 expires_at 為空字串時轉為 null
if (isset($validated['expires_at']) && empty($validated['expires_at'])) {
$validated['expires_at'] = null;
}
$passCode->update(array_filter($validated, function ($value, $key) use ($request) {
return $request->has($key);
}, ARRAY_FILTER_USE_BOTH));
return back()->with('success', __('Pass code updated.'));
}
@ -225,7 +232,7 @@ class SalesController extends Controller
public function destroyPassCode(PassCode $passCode)
{
$passCode->update(['status' => 'disabled']);
return back()->with('success', __('Pass code disabled.'));
return back()->with('success', __('Pass code cancelled.'));
}
// 來店禮設定

View File

@ -931,7 +931,8 @@
"Pass Codes": "通行碼",
"Pass code created: :code": "通行碼已建立::code",
"Pass code deleted.": "通行碼已刪除",
"Pass code disabled.": "通行碼已停用。",
"Pass code cancelled.": "通行碼已取消。",
"Pass code disabled.": "通行碼已取消。",
"Pass code updated.": "通行碼已更新",
"Password": "密碼",
"Password updated successfully.": "密碼已成功變更。",

View File

@ -3,6 +3,7 @@
@section('content')
<div class="space-y-4 pb-20" x-data="{
showCreateModal: false,
showEditModal: false,
showDeleteModal: false,
deleteTargetForm: '',
selectedMachine: '',
@ -14,6 +15,21 @@
showQrModal: false,
activeQrCode: '',
activeTicketUrl: '',
editFormAction: '',
editPassCodeName: '',
editExpiresAt: '',
openEditModal(item) {
this.editFormAction = '{{ route('admin.sales.pass-codes.update', ':id') }}'.replace(':id', item.id);
this.editPassCodeName = item.name;
this.editExpiresAt = item.expires_at;
this.showEditModal = true;
this.$nextTick(() => {
window.dispatchEvent(new CustomEvent('set-date', {
detail: { name: 'expires_at', value: this.editExpiresAt }
}));
});
},
generateRandomCode() {
this.customCode = Math.floor(Math.random() * 90000000 + 10000000).toString();
@ -174,8 +190,7 @@
<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'))">
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' : '' }}
@ -275,20 +290,38 @@
</td>
<td class="px-6 py-6 whitespace-nowrap">
@php
$displayStatus = $code->status === 'active' && $code->expires_at?->isPast() ? 'expired' : $code->status;
$displayStatus = $code->status === 'active' && $code->expires_at?->isPast() ? 'expired'
: ($code->status === 'disabled' ? 'cancelled' : $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()))
@if($code->status === 'active' && (!$code->expires_at ||
$code->expires_at->isFuture()))
<button type="button" @click="openEditModal({
id: {{ $code->id }},
name: '{{ addslashes($code->name) }}',
expires_at: '{{ $code->expires_at ? $code->expires_at->format('Y/m/d H:i') : '' }}'
})"
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="{{ __('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" />
</svg>
</button>
<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 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
@ -302,7 +335,8 @@
@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">
<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>
@ -353,11 +387,12 @@
</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>
@php
$displayStatusMobile = $code->status === 'active' && $code->expires_at?->isPast() ?
'expired' : ($code->status === 'disabled' ? 'cancelled' : $code->status);
@endphp
<x-status-badge :status="$displayStatusMobile" size="sm" />
</div>
</div>
{{-- Info Grid --}}
@ -380,14 +415,28 @@
</div>
{{-- Action Buttons --}}
<div class="mt-6 pt-6 border-t border-slate-100 dark:border-slate-800 flex gap-3">
<div class="mt-6 pt-6 border-t border-slate-100 dark:border-slate-800 flex flex-wrap gap-3">
@if($code->status === 'active' && (!$code->expires_at || $code->expires_at->isFuture()))
<button type="button" @click="openEditModal({
id: {{ $code->id }},
name: '{{ addslashes($code->name) }}',
expires_at: '{{ $code->expires_at ? $code->expires_at->format('Y/m/d H:i') : '' }}'
})"
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-cyan-500 hover:bg-cyan-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="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>
{{ __('Edit') }}
</button>
<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" />
<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>
@ -395,8 +444,7 @@
@if($code->status === 'active')
<form id="delete-form-mobile-{{ $code->id }}"
action="{{ route('admin.sales.pass-codes.destroy', $code) }}" method="POST"
class="flex-1">
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 }}')"
@ -452,7 +500,8 @@
</button>
</div>
<form action="{{ route('admin.sales.pass-codes.store') }}" method="POST" class="space-y-6" @submit.prevent="validateAndSubmit">
<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">
@ -498,13 +547,16 @@
</div>
<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="bg-slate-50 dark:bg-slate-800/40 rounded-[2rem] p-6 border border-slate-100 dark:border-slate-800/50 shadow-sm">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Validity
Period (Days)') }}</label>
<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">
<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>
@ -512,11 +564,14 @@
<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
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">
<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>
@ -524,18 +579,28 @@
<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>
<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>
<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" />
<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>
<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>
<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>
</div>
@ -554,8 +619,8 @@
{{-- 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" />
: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"
@ -568,15 +633,17 @@
<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: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>
<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">
@ -603,7 +670,8 @@
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" />
<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>
@ -613,7 +681,8 @@
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" />
<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>
@ -632,12 +701,74 @@
</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
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>
{{-- Edit Modal --}}
<div x-show="showEditModal" 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="showEditModal = false"></div>
<div x-show="showEditModal" 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-xl 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">{{
__('Edit Pass Code') }}</h3>
<button @click="showEditModal = 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="editFormAction" method="POST" class="space-y-6">
@csrf
@method('PATCH')
<div class="space-y-2">
<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="editPassCodeName" class="luxury-input w-full"
placeholder="{{ __('e.g. Test Code for Maintenance') }}" required>
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Expires
At') }}</label>
<x-luxury-datetime-input name="expires_at" :placeholder="__('Permanent')"
x-model="editExpiresAt" />
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest pl-1 mt-1">{{ __('Leave
empty for permanent code') }}</p>
</div>
<div class="flex justify-end gap-x-4 pt-8">
<button type="button" @click="showEditModal = false" class="btn-luxury-ghost px-8">{{
__('Cancel') }}</button>
<button type="submit" class="btn-luxury-primary px-12">
{{ __('Update') }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection

View File

@ -6,8 +6,10 @@ $slotSelectConfig = [
"hasSearch" => true,
"searchPlaceholder" => __("Search Slot..."),
"isHidePlaceholder" => false,
"searchClasses" => "block w-[calc(100%-16px)] mx-2 py-2 px-3 text-sm border-slate-200 dark:border-white/10 rounded-lg focus:border-cyan-500 focus:ring-cyan-500 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200 placeholder:text-slate-400 dark:placeholder:text-slate-500",
"searchWrapperClasses" => "sticky top-0 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md p-2 z-10",
"searchClasses" => "block w-[calc(100%-16px)] mx-2 py-2 px-3 text-sm border-slate-200 dark:border-white/10 rounded-lg
focus:border-cyan-500 focus:ring-cyan-500 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200
placeholder:text-slate-400 dark:placeholder:text-slate-500",
"searchWrapperClasses" => "sticky top-0 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md p-2 z-10",
"toggleClasses" => "hs-select-toggle luxury-select-toggle",
"toggleTemplate" => '<button type="button" aria-expanded="false"><span class="me-2" data-icon></span><span
class="text-slate-800 dark:text-slate-200" data-title></span>
@ -17,8 +19,11 @@ $slotSelectConfig = [
<path d="m6 9 6 6 6-6" />
</svg></div>
</button>',
"dropdownClasses" => "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10 rounded-xl shadow-[0_20px_50px_rgba(0,0,0,0.3)] mt-2 z-[150] animate-luxury-in",
"optionClasses" => "hs-select-option py-2.5 px-3 mb-0.5 text-sm text-slate-800 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-cyan-500/10 dark:hover:text-cyan-400 rounded-lg flex items-center justify-between transition-all duration-300",
"dropdownClasses" => "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10
rounded-xl shadow-[0_20px_50px_rgba(0,0,0,0.3)] mt-2 z-[150] animate-luxury-in",
"optionClasses" => "hs-select-option py-2.5 px-3 mb-0.5 text-sm text-slate-800 dark:text-slate-300 cursor-pointer
hover:bg-slate-100 dark:hover:bg-cyan-500/10 dark:hover:text-cyan-400 rounded-lg flex items-center justify-between
transition-all duration-300",
"optionTemplate" => '<div class="flex items-center justify-between w-full"><span data-title></span><span
class="hs-select-active-indicator hidden text-cyan-500"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"
@ -127,7 +132,7 @@ $slotSelectConfig = [
</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">
{{ __('Expires At') }}
{{ __('Expiry') }}
</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">
@ -174,11 +179,12 @@ $slotSelectConfig = [
}}: {{ $code->slot_no }}</div>
</td>
<td class="px-6 py-6 whitespace-nowrap">
<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 text-slate-400 mt-0.5">{{
$code->expires_at->diffForHumans() }}</div>
<div class="text-sm font-black text-slate-600 dark:text-slate-300">
{{ $code->expires_at->format('Y-m-d H:i') }}
</div>
<div class="text-[10px] font-bold text-slate-400 mt-0.5">
{{ $code->expires_at->diffForHumans() }}
</div>
</td>
<td class="px-6 py-6 whitespace-nowrap">
@php
@ -290,9 +296,12 @@ $slotSelectConfig = [
<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->format('Y-m-d H:i') }}</p>
{{ __('Expiry') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300">
{{ $code->expires_at->format('m-d H:i') }}
<span class="text-[10px] text-slate-400 ml-1">{{ $code->expires_at->diffForHumans()
}}</span>
</p>
</div>
</div>
@ -372,7 +381,8 @@ $slotSelectConfig = [
</button>
</div>
<form action="{{ route('admin.sales.pickup-codes.store') }}" method="POST" class="space-y-6" @submit.prevent="validateAndSubmit">
<form action="{{ route('admin.sales.pickup-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">
@ -422,57 +432,46 @@ $slotSelectConfig = [
maxlength="12" required>
</div>
{{-- Validity Period Section --}}
<div class="max-w-2xl mx-auto w-full">
{{-- Validity Period Section (Compact) --}}
<div class="space-y-4">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Validity
Period') }}</label>
<div
class="bg-slate-50 dark:bg-slate-800/40 rounded-[2.5rem] p-8 border border-slate-100 dark:border-slate-800/50 shadow-sm">
<div class="flex items-center justify-between mb-8 px-2">
<div class="flex items-center gap-2">
<div class="w-1.5 h-1.5 rounded-full bg-cyan-500/50"></div>
<label
class="text-[13px] font-black text-slate-500 dark:text-slate-400 tracking-wider">{{
__('Validity Period') }}</label>
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">
<div class="flex-1 relative">
<input type="range" name="expires_hours" x-model="expiresHours" min="1"
:max="maxHours"
class="w-full h-1.5 bg-slate-200 dark:bg-slate-700/50 rounded-lg appearance-none cursor-pointer accent-cyan-500 transition-all hover:accent-cyan-400">
</div>
<div
class="shrink-0 w-20 py-3 bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-slate-700 shadow-sm text-center">
<span class="text-xl font-black text-slate-800 dark:text-white leading-none"
x-text="expiresHours"></span>
<span
class="block text-[9px] font-black text-slate-400 uppercase mt-0.5 tracking-widest">{{
__('Hrs') }}</span>
</div>
<span class="text-[11px] font-bold text-slate-400/70 tracking-wide">{{ __('Max 24 Hours') }}</span>
</div>
<div class="space-y-10">
{{-- Slider and Value --}}
<div class="flex items-center gap-8">
<div class="flex-1 relative">
<input type="range" name="expires_hours" x-model="expiresHours" min="1"
:max="maxHours"
class="w-full h-1 bg-slate-200 dark:bg-slate-700/50 rounded-lg appearance-none cursor-pointer accent-cyan-500 transition-all hover:accent-cyan-400">
</div>
<div
class="shrink-0 w-24 py-4 bg-white dark:bg-slate-900 rounded-[1.5rem] border border-slate-200 dark:border-slate-700 shadow-sm text-center ring-4 ring-slate-50 dark:ring-slate-800/50">
<span class="text-2xl font-black text-slate-800 dark:text-white leading-none"
x-text="expiresHours"></span>
<span
class="block text-[10px] font-black text-slate-400 uppercase mt-1 tracking-widest">{{
__('Hrs') }}</span>
</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">{{
__('Expected Expiry') }}</span>
<span class="text-[10px] font-bold text-slate-400/70 tracking-wide">{{ __('Max 24
Hours') }}</span>
</div>
{{-- Result Display --}}
<div class="relative">
<div
class="flex flex-col items-center justify-center py-8 px-8 bg-white dark:bg-slate-900/50 rounded-[2rem] border border-slate-200/60 dark:border-slate-700/50 shadow-inner">
<span
class="text-base font-bold text-slate-600 dark:text-slate-400 mb-5 tracking-wide">{{
__('Expected Expiry Date & Time') }}</span>
<div
class="flex items-center gap-3 py-3 px-6 bg-slate-50 dark:bg-slate-800/50 rounded-2xl border border-slate-100 dark:border-slate-700/50 shadow-sm">
<svg class="w-5 h-5 text-cyan-500/50" 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 sm:text-2xl font-black text-slate-700 dark:text-cyan-100 font-mono tracking-tight"
x-text="calculateExpiry()"></span>
</div>
</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>
</div>
</div>
@ -546,7 +545,8 @@ $slotSelectConfig = [
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" />
<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>

View File

@ -1,21 +1,20 @@
@props([
'name',
'show' => false,
'maxWidth' => '2xl'
'name',
'show' => false,
'maxWidth' => '2xl'
])
@php
$maxWidth = [
'sm' => 'sm:max-w-sm',
'md' => 'sm:max-w-md',
'lg' => 'sm:max-w-lg',
'xl' => 'sm:max-w-xl',
'2xl' => 'sm:max-w-2xl',
'sm' => 'sm:max-w-sm',
'md' => 'sm:max-w-md',
'lg' => 'sm:max-w-lg',
'xl' => 'sm:max-w-xl',
'2xl' => 'sm:max-w-2xl',
][$maxWidth];
@endphp
<div
x-data="{
<div x-data="{
show: @js($show),
focusables() {
// All focusable element types...
@ -30,49 +29,32 @@ $maxWidth = [
prevFocusable() { return this.focusables()[this.prevFocusableIndex()] || this.lastFocusable() },
nextFocusableIndex() { return (this.focusables().indexOf(document.activeElement) + 1) % (this.focusables().length + 1) },
prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) -1 },
}"
x-init="$watch('show', value => {
}" x-init="$watch('show', value => {
if (value) {
document.body.classList.add('overflow-y-hidden');
{{ $attributes->has('focusable') ? 'setTimeout(() => firstFocusable().focus(), 100)' : '' }}
} else {
document.body.classList.remove('overflow-y-hidden');
}
})"
x-on:open-modal.window="$event.detail == '{{ $name }}' ? show = true : null"
x-on:close-modal.window="$event.detail == '{{ $name }}' ? show = false : null"
x-on:close.stop="show = false"
x-on:keydown.escape.window="show = false"
x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
x-show="show"
class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50"
style="display: {{ $show ? 'block' : 'none' }};"
>
<div
x-show="show"
class="fixed inset-0 transform transition-all"
x-on:click="show = false"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
>
})" x-on:open-modal.window="$event.detail == '{{ $name }}' ? show = true : null"
x-on:close-modal.window="$event.detail == '{{ $name }}' ? show = false : null" x-on:close.stop="show = false"
x-on:keydown.escape.window="show = false" x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
x-on:keydown.shift.tab.prevent="prevFocusable().focus()" x-show="show"
class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50" style="display: {{ $show ? 'block' : 'none' }};">
<div x-show="show" class="fixed inset-0 transform transition-all" x-on:click="show = false"
x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
<div class="absolute inset-0 bg-gray-500 dark:bg-gray-900 opacity-75"></div>
</div>
<div
x-show="show"
<div x-show="show"
class="mb-6 bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full {{ $maxWidth }} sm:mx-auto"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-200"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
{{ $slot }}
</div>
</div>
</div>

View File

@ -1,52 +1,70 @@
@props([
'name' => null,
'options' => [],
'selected' => null,
'placeholder' => null,
'id' => null,
'hasSearch' => true,
'name' => null,
'options' => [],
'selected' => null,
'placeholder' => null,
'id' => null,
'hasSearch' => true,
])
@php
$id = $id ?? $name ?? 'select-' . uniqid();
$options = is_iterable($options) ? $options : [];
// Skill Standard: Use " " for empty/all options to bypass Preline hiding while staying 'blank'
$isEmptySelected = (is_null($selected) || (string)$selected === '' || (string)$selected === ' ');
$config = [
"placeholder" => $placeholder ?: __('Select...'),
"hasSearch" => (bool)$hasSearch,
"searchPlaceholder" => $placeholder ?: __('Search...'),
"isHidePlaceholder" => false,
"searchClasses" => "block w-[calc(100%-16px)] mx-2 py-2 px-3 text-sm border-slate-200 dark:border-white/10 rounded-lg focus:border-cyan-500 focus:ring-cyan-500 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200 placeholder:text-slate-400 dark:placeholder:text-slate-500",
"searchWrapperClasses" => "sticky top-0 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md p-2 z-10",
"toggleClasses" => "hs-select-toggle luxury-select-toggle",
"toggleTemplate" => '<button type="button" aria-expanded="false"><span class="me-2" data-icon></span><span class="text-slate-800 dark:text-slate-200" data-title></span><div class="ms-auto"><svg class="size-4 text-slate-400 transition-transform duration-300" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg></div></button>',
"dropdownClasses" => "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10 rounded-xl shadow-[0_20px_50px_rgba(0,0,0,0.3)] mt-2 z-[150] animate-luxury-in",
"optionClasses" => "hs-select-option py-2.5 px-3 mb-0.5 text-sm text-slate-800 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-cyan-500/10 dark:hover:text-cyan-400 rounded-lg flex items-center justify-between transition-all duration-300",
"optionTemplate" => '<div class="flex items-center justify-between w-full"><span data-title></span><span class="hs-select-active-indicator hidden text-cyan-500"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg></span></div>'
];
$id = $id ?? $name ?? 'select-' . uniqid();
$options = is_iterable($options) ? $options : [];
// Skill Standard: Use " " for empty/all options to bypass Preline hiding while staying 'blank'
$isEmptySelected = (is_null($selected) || (string)$selected === '' || (string)$selected === ' ');
$config = [
"placeholder" => $placeholder ?: __('Select...'),
"hasSearch" => (bool)$hasSearch,
"searchPlaceholder" => $placeholder ?: __('Search...'),
"isHidePlaceholder" => false,
"searchClasses" => "block w-[calc(100%-16px)] mx-2 py-2 px-3 text-sm border-slate-200 dark:border-white/10 rounded-lg
focus:border-cyan-500 focus:ring-cyan-500 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200
placeholder:text-slate-400 dark:placeholder:text-slate-500",
"searchWrapperClasses" => "sticky top-0 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md p-2 z-10",
"toggleClasses" => "hs-select-toggle luxury-select-toggle",
"toggleTemplate" => '<button type="button" aria-expanded="false"><span class="me-2" data-icon></span><span
class="text-slate-800 dark:text-slate-200" data-title></span>
<div class="ms-auto"><svg class="size-4 text-slate-400 transition-transform duration-300"
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="m6 9 6 6 6-6" />
</svg></div>
</button>',
"dropdownClasses" => "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10
rounded-xl shadow-[0_20px_50px_rgba(0,0,0,0.3)] mt-2 z-[150] animate-luxury-in",
"optionClasses" => "hs-select-option py-2.5 px-3 mb-0.5 text-sm text-slate-800 dark:text-slate-300 cursor-pointer
hover:bg-slate-100 dark:hover:bg-cyan-500/10 dark:hover:text-cyan-400 rounded-lg flex items-center justify-between
transition-all duration-300",
"optionTemplate" => '<div class="flex items-center justify-between w-full"><span data-title></span><span
class="hs-select-active-indicator hidden text-cyan-500"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg"
width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"
stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"></polyline>
</svg></span></div>'
];
@endphp
<div {{ $attributes->merge(['class' => 'relative w-full'])->only('class') }}>
<select name="{{ $name }}" id="{{ $id }}" data-hs-select='{!! json_encode($config) !!}' class="hidden" {{ $attributes->except(['options', 'selected', 'placeholder', 'id', 'name', 'class', 'hasSearch']) }}>
<select name="{{ $name }}" id="{{ $id }}" data-hs-select='{!! json_encode($config) !!}' class="hidden" {{
$attributes->except(['options', 'selected', 'placeholder', 'id', 'name', 'class', 'hasSearch']) }}>
@if($placeholder)
<option value=" " {{ $isEmptySelected ? 'selected' : '' }} data-title="{{ $placeholder }}">
{{ $placeholder }}
</option>
<option value=" " {{ $isEmptySelected ? 'selected' : '' }} data-title="{{ $placeholder }}">
{{ $placeholder }}
</option>
@endif
{{ $slot }}
@foreach($options as $v => $l)
@php
$val = is_object($l) ? ($l->id ?? $l->value) : $v;
$text = is_object($l) ? ($l->name ?? $l->label ?? $l->title) : $l;
@endphp
<option value="{{ $val }}" {{ (string)$selected === (string)$val ? 'selected' : '' }} data-title="{{ $text }}">
{{ $text }}
</option>
@php
$val = is_object($l) ? ($l->id ?? $l->value) : $v;
$text = is_object($l) ? ($l->name ?? $l->label ?? $l->title) : $l;
@endphp
<option value="{{ $val }}" {{ (string)$selected===(string)$val ? 'selected' : '' }} data-title="{{ $text }}">
{{ $text }}
</option>
@endforeach
</select>
</div>