[STYLE] 標準化商品管理與廣告彈窗 UI,完善商品分類多語系 CRUD 功能
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m2s

This commit is contained in:
2026-04-01 08:38:03 +08:00
parent 759fae4380
commit e27eee78f5
12 changed files with 764 additions and 283 deletions

View File

@@ -10,18 +10,18 @@
<div class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm"></div>
<div class="flex items-center justify-center min-h-screen p-4">
<div class="relative w-full max-w-xl bg-white dark:bg-slate-900 rounded-[2rem] shadow-2xl border border-slate-200 dark:border-white/10 overflow-hidden animate-luxury-in"
<div class="flex items-center justify-center min-h-screen p-4 sm:p-0">
<div class="relative 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 animate-luxury-in"
@click.away="isAdModalOpen = false">
<!-- Modal Header -->
<div class="bg-slate-50/50 dark:bg-slate-800/50 px-8 py-5 border-b border-slate-100 dark:border-white/5 flex items-center justify-between">
<div class="flex justify-between items-center mb-8">
<div>
<h3 class="text-xl font-black text-slate-800 dark:text-white uppercase tracking-tight" x-text="adFormMode === 'add' ? '{{ __("Add Advertisement") }}' : '{{ __("Edit Advertisement") }}'"></h3>
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight" x-text="adFormMode === 'add' ? '{{ __("Add Advertisement") }}' : '{{ __("Edit Advertisement") }}'"></h3>
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">{{ __('Manage your ad material details') }}</p>
</div>
<button @click="isAdModalOpen = false" class="p-2 text-slate-400 hover:text-cyan-500 transition-colors">
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
<button @click="isAdModalOpen = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
@@ -29,7 +29,7 @@
method="POST"
enctype="multipart/form-data"
@submit.prevent="submitAdForm"
class="px-8 pt-4 pb-8 space-y-4">
class="space-y-6">
@csrf
<template x-if="adFormMode === 'edit'">
@method('PUT')

View File

@@ -10,22 +10,22 @@
<div class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm"></div>
<div class="flex items-center justify-center min-h-screen p-4">
<div class="relative w-full max-w-lg bg-white dark:bg-slate-900 rounded-[2rem] shadow-2xl border border-slate-200 dark:border-white/10 overflow-visible animate-luxury-in"
<div class="flex items-center justify-center min-h-screen p-4 sm:p-0">
<div class="relative 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-lg sm:w-full overflow-visible animate-luxury-in"
@click.away="isAssignModalOpen = false">
<!-- Modal Header -->
<div class="bg-slate-50/50 dark:bg-slate-800/50 rounded-t-[2rem] px-8 py-6 border-b border-slate-100 dark:border-white/5 flex items-center justify-between">
<div class="flex justify-between items-center mb-8">
<div>
<h3 class="text-xl font-black text-slate-800 dark:text-white uppercase tracking-tight">{{ __('Assign Advertisement') }}</h3>
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Assign Advertisement') }}</h3>
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">{{ __('Select a material to play on this machine') }}</p>
</div>
<button @click="isAssignModalOpen = false" class="p-2 text-slate-400 hover:text-cyan-500 transition-colors">
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
<button @click="isAssignModalOpen = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<form @submit.prevent="submitAssignment" class="p-8 space-y-6">
<form @submit.prevent="submitAssignment" class="space-y-6">
<!-- Machine & Position Info (Read-only) -->
<div class="grid grid-cols-2 gap-4">
<div class="p-4 bg-slate-50 dark:bg-slate-800/50 rounded-2xl border border-slate-100 dark:border-white/5">

View File

@@ -170,13 +170,7 @@
<x-searchable-select id="product-category" name="category_id" x-model="formData.category_id" :placeholder="__('Uncategorized')">
<option value="">{{ __('Uncategorized') }}</option>
@foreach($categories as $category)
@php
$catName = $category->name;
if ($category->name_dictionary_key) {
$catName = __($category->name_dictionary_key);
}
@endphp
<option value="{{ $category->id }}" data-title="{{ $catName }}">{{ $catName }}</option>
<option value="{{ $category->id }}" data-title="{{ $category->localized_name }}">{{ $category->localized_name }}</option>
@endforeach
</x-searchable-select>
</div>

View File

@@ -185,13 +185,7 @@
<x-searchable-select id="product-category" name="category_id" x-model="formData.category_id" :placeholder="__('Uncategorized')">
<option value="">{{ __('Uncategorized') }}</option>
@foreach($categories as $category)
@php
$catName = $category->name;
if ($category->name_dictionary_key) {
$catName = __($category->name_dictionary_key);
}
@endphp
<option value="{{ $category->id }}" {{ $product->category_id == $category->id ? 'selected' : '' }} data-title="{{ $catName }}">{{ $catName }}</option>
<option value="{{ $category->id }}" {{ $product->category_id == $category->id ? 'selected' : '' }} data-title="{{ $category->localized_name }}">{{ $category->localized_name }}</option>
@endforeach
</x-searchable-select>
</div>

View File

@@ -15,7 +15,7 @@ $roleSelectConfig = [
@endphp
@section('content')
<div class="space-y-6 pb-20"
<div class="space-y-2 pb-20"
x-data="productManager"
data-categories="{{ json_encode($categories) }}"
data-settings="{{ json_encode($companySettings) }}"
@@ -24,7 +24,7 @@ $roleSelectConfig = [
data-index-url="{{ route($baseRoute . '.index') }}">
<!-- Header -->
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Product Management') }}</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">
@@ -32,18 +32,47 @@ $roleSelectConfig = [
</p>
</div>
<div class="flex items-center gap-3">
<a href="{{ route($baseRoute . '.create') }}" class="btn-luxury-primary">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
<span>{{ __('Add Product') }}</span>
</a>
<template x-if="activeTab === 'products'">
<a href="{{ route($baseRoute . '.create') }}" class="btn-luxury-primary transition-all duration-300">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
<span>{{ __('Add Product') }}</span>
</a>
</template>
<template x-if="activeTab === 'categories'">
<button @click="openCategoryModal()" type="button" class="btn-luxury-primary transition-all duration-300 bg-emerald-600 hover:bg-emerald-700 shadow-emerald-500/20">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
<span>{{ __('Add Category') }}</span>
</button>
</template>
</div>
</div>
<!-- Tabs Navigation -->
<div class="flex items-center gap-1 p-1.5 bg-slate-100 dark:bg-slate-900/50 rounded-2xl w-fit border border-slate-200/50 dark:border-slate-800/50" aria-label="Tabs">
<button type="button"
@click="activeTab = 'products'"
:class="activeTab === 'products' ? '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">
{{ __('Product List') }}
</button>
<button type="button"
@click="activeTab = 'categories'"
:class="activeTab === 'categories' ? '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">
{{ __('Category Management') }}
</button>
</div>
<!-- Main Content Card -->
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
<!-- Tab Contents -->
<div class="mt-6">
<!-- Products Tab -->
<div x-show="activeTab === 'products'" class="luxury-card rounded-3xl p-8 animate-luxury-in" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4" x-transition:enter-end="opacity-100 translate-y-0" x-cloak>
<!-- Filters & Search -->
<form action="{{ route($routeName) }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 mb-10">
<div class="relative group">
@@ -68,6 +97,7 @@ $roleSelectConfig = [
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Product 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">{{ __('Barcode') }}</th>
@if(auth()->user()->isSystemAdmin())
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Company') }}</th>
@endif
@@ -91,30 +121,21 @@ $roleSelectConfig = [
</div>
<div class="flex flex-col">
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ $product->localized_name }}</span>
<div class="flex items-center gap-2 mt-0.5">
<div class="flex flex-wrap items-center gap-1.5 mt-1">
@php
$catName = $product->category->name ?? __('Uncategorized');
if ($product->category && $product->category->name_dictionary_key) {
$translatedCat = __($product->category->name_dictionary_key);
if ($translatedCat !== $product->category->name_dictionary_key) {
$catName = $translatedCat;
}
}
$catName = $product->category->localized_name ?? __('Uncategorized');
@endphp
<span class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest bg-slate-100 dark:bg-slate-800 px-1.5 py-0.5 rounded transition-colors group-hover:text-slate-600 dark:group-hover:text-slate-300">{{ $catName }}</span>
@if($companySettings['enable_material_code'] ?? false)
<div class="flex items-center gap-1.5 bg-slate-100/50 dark:bg-slate-800/50 px-2 py-0.5 rounded-md border border-slate-100 dark:border-slate-800">
<span class="text-[10px] font-mono font-bold text-cyan-500 tracking-tighter">{{ $product->barcode }}</span>
<span class="h-1 w-1 rounded-full bg-slate-300 dark:bg-slate-700"></span>
<span class="text-[10px] font-mono font-bold text-emerald-500 tracking-tighter">{{ $product->metadata['material_code'] ?? '-' }}</span>
</div>
@else
<span class="text-[10px] font-mono font-bold text-cyan-500 tracking-tighter transition-colors group-hover:text-cyan-600 dark:group-hover:text-cyan-400">{{ $product->barcode }}</span>
@if(($companySettings['enable_material_code'] ?? false) && isset($product->metadata['material_code']))
<span class="text-[10px] font-bold text-emerald-500/80 uppercase tracking-widest bg-emerald-500/10 px-1.5 py-0.5 rounded border border-emerald-500/20">#{{ $product->metadata['material_code'] }}</span>
@endif
</div>
</div>
</div>
</td>
<td class="px-6 py-6 whitespace-nowrap">
<span class="text-sm font-mono font-black text-slate-700 dark:text-slate-200 tracking-tight">{{ $product->barcode ?: '-' }}</span>
</td>
@if(auth()->user()->isSystemAdmin())
<td class="px-6 py-6 text-center">
<span class="text-xs font-bold text-slate-600 dark:text-slate-400 group-hover:text-slate-900 dark:group-hover:text-slate-200 transition-colors">{{ $product->company->name ?? '-' }}</span>
@@ -172,10 +193,64 @@ $roleSelectConfig = [
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="mt-10 pt-6 border-t border-slate-100 dark:border-slate-800/50">
{{ $products->links('vendor.pagination.luxury') }}
<div class="mt-8">
{{ $products->links() }}
</div>
</div>
<!-- Category Tab -->
<div x-show="activeTab === 'categories'" class="luxury-card rounded-3xl p-8 animate-luxury-in" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4" x-transition:enter-end="opacity-100 translate-y-0" x-cloak>
<div class="overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Category Name') }}</th>
@if(auth()->user()->isSystemAdmin())
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Company') }}</th>
@endif
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
@forelse($categories as $category)
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
<td class="px-6 py-6">
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 transition-colors group-hover:text-cyan-600">
{{ $category->localized_name }}
</span>
</td>
@if(auth()->user()->isSystemAdmin())
<td class="px-6 py-6 text-center">
<span class="text-xs font-bold text-slate-600 dark:text-slate-400">{{ $category->company->name ?? __('System Default') }}</span>
</td>
@endif
<td class="px-6 py-6 text-right">
<div class="flex justify-end items-center gap-2">
<button type="button" @click="openCategoryModal(@js($category))" class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20" title="{{ __('Edit') }}">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" /></svg>
</button>
<button type="button" @click="confirmDelete('{{ route('admin.data-config.product-categories.destroy', $category->id) }}')" class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 hover:bg-rose-500/5 transition-all border border-transparent hover:border-rose-500/20" title="{{ __('Delete') }}">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" /></svg>
</button>
</div>
</td>
</tr>
@empty
<tr class="animate-luxury-in">
<td colspan="{{ auth()->user()->isSystemAdmin() ? 3 : 2 }}" class="px-6 py-20 text-center">
<div class="flex flex-col items-center justify-center space-y-4">
<div class="w-16 h-16 rounded-3xl bg-slate-50 dark:bg-slate-800/50 flex items-center justify-center text-slate-300 dark:text-slate-700 shadow-inner">
<svg class="size-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" /></svg>
</div>
<p class="text-slate-400 font-bold tracking-widest text-sm uppercase">{{ __('No categories found.') }}</p>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
@@ -203,6 +278,86 @@ $roleSelectConfig = [
@method('DELETE')
</form>
<!-- Category Modal -->
<div x-show="isCategoryModalOpen" class="fixed inset-0 z-[110] overflow-y-auto" x-cloak>
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<div x-show="isCategoryModalOpen" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" class="fixed inset-0 transition-opacity bg-slate-900/60 backdrop-blur-sm" @click="isCategoryModalOpen = false"></div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">&#8203;</span>
<div x-show="isCategoryModalOpen" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100" x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" class="inline-block w-full max-w-lg p-8 my-8 overflow-hidden text-left align-middle transition-all transform bg-white dark:bg-slate-900 shadow-2xl rounded-3xl border border-slate-100 dark:border-white/10">
<div class="flex justify-between items-center mb-8">
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight" x-text="categoryModalMode === 'create' ? '{{ __('Add Category') }}' : '{{ __('Edit Category') }}'"></h3>
<button @click="isCategoryModalOpen = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<form :action="categoryFormAction" method="POST" class="space-y-6">
@csrf
<template x-if="categoryModalMode === 'edit'">
<input type="hidden" name="_method" value="PUT">
</template>
<div class="space-y-6">
<!-- 1. Company Selection (If Admin) -->
@if(auth()->user()->isSystemAdmin())
<div class="p-6 bg-slate-50 dark:bg-slate-800/30 rounded-3xl border border-slate-100 dark:border-white/5 space-y-3">
<label class="block text-sm font-black text-slate-700 dark:text-slate-200 px-1">{{ __('Affiliated Company') }}</label>
<!-- Searchable Select Wrapper -->
<div id="category_company_select_wrapper" class="relative">
<!-- Will be hydrated by JS -->
</div>
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest px-1">{{ __('Type to search or leave blank for system defaults.') }}</p>
</div>
@endif
<!-- 2. Multilingual Names -->
<div class="space-y-5 px-1">
<!-- zh_TW -->
<div class="space-y-2">
<label class="flex items-center gap-2 text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">
{{ __('Category Name (zh_TW)') }} <span class="text-rose-500">*</span>
</label>
<input type="text" name="names[zh_TW]" x-model="categoryFormFields.names.zh_TW" class="luxury-input w-full focus:ring-emerald-500/20 focus:border-emerald-500" placeholder="{{ __('e.g., Beverage') }}" required>
</div>
<!-- en -->
<div class="space-y-2">
<label class="flex items-center gap-2 text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">
{{ __('Category Name (en)') }}
</label>
<input type="text" name="names[en]" x-model="categoryFormFields.names.en" class="luxury-input w-full" placeholder="{{ __('e.g., Drinks') }}">
</div>
<!-- ja -->
<div class="space-y-2">
<label class="flex items-center gap-2 text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">
{{ __('Category Name (ja)') }}
</label>
<input type="text" name="names[ja]" x-model="categoryFormFields.names.ja" class="luxury-input w-full" placeholder="{{ __('e.g., お飲み物') }}">
</div>
</div>
</div>
<div class="flex items-center justify-end gap-4 mt-12 pt-6 border-t border-slate-100 dark:border-white/5">
<button type="button" @click="isCategoryModalOpen = false"
class="px-6 py-2.5 text-sm font-black text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 uppercase tracking-widest transition-all">
{{ __('Cancel') }}
</button>
<button type="submit"
class="btn-luxury-primary px-10 py-3 shadow-lg"
:class="categoryModalMode === 'create' ? 'bg-emerald-600 hover:bg-emerald-700 shadow-emerald-500/20' : 'bg-cyan-600 hover:bg-cyan-700 shadow-cyan-500/20'">
<span x-text="categoryModalMode === 'create' ? '{{ __('Create') }}' : '{{ __('Save Changes') }}'"></span>
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Product Detail Slide-over -->
<div x-show="isDetailOpen"
class="fixed inset-0 z-[100] overflow-hidden"
@@ -407,10 +562,18 @@ $roleSelectConfig = [
isDetailOpen: false,
isImageZoomed: false,
isStatusConfirmOpen: false,
isCategoryModalOpen: false,
activeTab: '{{ request("tab", "products") }}',
categoryModalMode: 'create',
categoryFormAction: '',
deleteFormAction: '',
toggleFormAction: '',
selectedProduct: null,
categories: [],
categoryFormFields: {
names: { zh_TW: '', en: '', ja: '' },
company_id: ''
},
submitConfirmedForm() {
this.$refs.statusToggleForm.submit();
@@ -418,6 +581,73 @@ $roleSelectConfig = [
init() {
this.categories = JSON.parse(this.$el.dataset.categories || '[]');
this.companies = @js($companies);
// Watch for category modal opening to sync searchable select
this.$watch('isCategoryModalOpen', (value) => {
if (value && document.getElementById('category_company_select_wrapper')) {
this.$nextTick(() => {
this.updateCategoryCompanySelect();
});
}
});
},
updateCategoryCompanySelect() {
const wrapper = document.getElementById('category_company_select_wrapper');
if (!wrapper) return;
wrapper.innerHTML = '';
const selectEl = document.createElement('select');
selectEl.name = 'company_id';
const uniqueId = 'cat-company-' + Date.now();
selectEl.id = uniqueId;
selectEl.className = 'hidden';
const config = {
"placeholder": "{{ __('Select Company (Default: System)') }}",
"hasSearch": true,
"searchPlaceholder": "{{ __('Search Company Title...') }}",
"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",
"dropdownClasses": "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10 rounded-xl shadow-[0_20px_50px_rgba(0,0,0,0.3)] mt-2 z-[100] animate-luxury-in",
"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>'
};
selectEl.setAttribute('data-hs-select', JSON.stringify(config));
// Default System option
const defaultOpt = document.createElement('option');
defaultOpt.value = "";
defaultOpt.textContent = "{{ __('System Default (Common)') }}";
defaultOpt.setAttribute('data-title', defaultOpt.textContent);
if (!this.categoryFormFields.company_id) defaultOpt.selected = true;
selectEl.appendChild(defaultOpt);
// Company options
this.companies.forEach(company => {
const opt = document.createElement('option');
opt.value = company.id;
opt.textContent = company.name;
opt.setAttribute('data-title', company.name);
if (String(this.categoryFormFields.company_id) === String(company.id)) opt.selected = true;
selectEl.appendChild(opt);
});
wrapper.appendChild(selectEl);
selectEl.addEventListener('change', (e) => {
this.categoryFormFields.company_id = e.target.value;
});
this.$nextTick(() => {
if (window.HSStaticMethods && window.HSStaticMethods.autoInit) {
window.HSStaticMethods.autoInit(['select']);
}
});
},
confirmDelete(action) {
@@ -430,6 +660,28 @@ $roleSelectConfig = [
this.isDetailOpen = true;
},
openCategoryModal(category = null) {
if (category) {
this.categoryModalMode = 'edit';
this.categoryFormAction = `{{ url('admin/data-config/product-categories') }}/${category.id}`;
this.categoryFormFields.names = { zh_TW: category.name || '', en: category.name || '', ja: category.name || '' };
if (category.translations && category.translations.length > 0) {
category.translations.forEach(t => {
if (this.categoryFormFields.names.hasOwnProperty(t.locale)) {
this.categoryFormFields.names[t.locale] = t.value;
}
});
}
this.categoryFormFields.company_id = category.company_id || '';
} else {
this.categoryModalMode = 'create';
this.categoryFormAction = `{{ route('admin.data-config.product-categories.store') }}`;
this.categoryFormFields.names = { zh_TW: '', en: '', ja: '' };
this.categoryFormFields.company_id = '';
}
this.isCategoryModalOpen = true;
},
getCategoryName(id) {
const category = this.categories.find(c => c.id == id);
return category ? (category.name || "{{ __('Uncategorized') }}") : "{{ __('Uncategorized') }}";