[FIX] 修復角色選單側邊欄顯示異常與重構機台補貨按鈕權限防禦

1. 修復 resources/views/layouts/partials/sidebar-menu.blade.php 中倉庫、銷售、遠端管理子選單未被 Spatie 權限包裹的 UI 異常,完美隱藏未授權子項目。

2. 重構 resources/views/admin/warehouses/machine-inventory.blade.php 的補貨按鈕文字,將「一鍵補貨」變更為「產生補貨單」。

3. 於 machine-inventory.blade.php 的 Alpine.js 控制器中動態宣告 hasReplenishPermission 權限判斷,若當前使用者不具備 menu.warehouses.replenishments 權限,點擊按鈕時將攔截跳轉並透過 dispatch 發送全域紅色 Error Toast 警告。

4. 擴充 zh_TW.json、en.json、ja.json 多語系翻譯檔,補齊「產生補貨單」與「無權限存取補貨單」之三語系翻譯。
This commit is contained in:
sky121113 2026-05-18 09:15:11 +08:00
parent 223fcddbc5
commit 536d10234d
5 changed files with 37 additions and 3 deletions

View File

@ -661,6 +661,7 @@
"Generate": "Generate",
"Generate New Code": "Generate New Code",
"Generate Pickup Code": "Generate Pickup Code",
"Generate Replenishment Order": "Generate Replenishment Order",
"Generate and manage one-time pickup codes for customers": "Generate and manage one-time pickup codes for customers",
"Gift Definitions": "Gift Definitions",
"Global roles accessible by all administrators.": "Global roles accessible by all administrators.",
@ -1808,6 +1809,7 @@
"You can assign or change the personnel handling this order": "You can assign or change the personnel handling this order",
"You cannot assign permissions you do not possess.": "You cannot assign permissions you do not possess.",
"You cannot delete your own account.": "You cannot delete your own account.",
"You do not have permission to access replenishment orders": "You do not have permission to access replenishment orders",
"Your recent account activity": "Your recent account activity",
"[PickupCode] Verification failed: :code": "[PickupCode] Verification failed: :code",
"[StaffCard] Verification failed: :uid": "[StaffCard] Verification failed: :uid",

View File

@ -661,6 +661,7 @@
"Generate": "生成",
"Generate New Code": "新コード生成",
"Generate Pickup Code": "受取コード生成",
"Generate Replenishment Order": "補充伝票生成",
"Generate and manage one-time pickup codes for customers": "顧客用のワンタイム受取コードを生成・管理",
"Gift Definitions": "ギフト設定",
"Global roles accessible by all administrators.": "全管理者がアクセス可能なグローバルロール。",
@ -1808,6 +1809,7 @@
"You can assign or change the personnel handling this order": "この注文を処理する担当者を割り当て、または変更できます",
"You cannot assign permissions you do not possess.": "自身が持っていない権限を割り当てることはできません。",
"You cannot delete your own account.": "自分自身のアカウントは削除できません。",
"You do not have permission to access replenishment orders": "補充伝票のアクセス権限がありません",
"Your recent account activity": "最近のアカウントアクティビティ",
"[PickupCode] Verification failed: :code": "[受取コード] 検証失敗: :code",
"[StaffCard] Verification failed: :uid": "[スタッフカード] 検証失敗: :uid",

View File

@ -668,6 +668,7 @@
"Generate": "產生",
"Generate New Code": "產生新代碼",
"Generate Pickup Code": "產生取貨碼",
"Generate Replenishment Order": "產生補貨單",
"Generate and manage one-time pickup codes for customers": "產生並管理客戶的一次性取貨碼",
"Gift Definitions": "禮品設定",
"Global roles accessible by all administrators.": "適用於所有管理者的全域角色。",
@ -1830,6 +1831,7 @@
"You can assign or change the personnel handling this order": "您可以指派或變更處理此訂單的人員",
"You cannot assign permissions you do not possess.": "您無法指派您自身不具備的權限。",
"You cannot delete your own account.": "您無法刪除自己的帳號。",
"You do not have permission to access replenishment orders": "您沒有機台補貨單的權限",
"Your recent account activity": "最近的帳號活動",
"[PickupCode] Verification failed: :code": "[取貨碼] 驗證失敗: :code",
"[StaffCard] Verification failed: :uid": "[員工卡] 驗證失敗: :uid",

View File

@ -108,14 +108,21 @@
</div>
<div class="mt-8 flex flex-col items-center gap-5">
<span class="text-base font-black text-slate-500 uppercase tracking-[0.3em]">{{ __('Total Stock Volume') }}</span>
<a :href="'{{ route('admin.warehouses.replenishments') }}?auto_machine_id=' + selectedMachine?.id"
class="px-6 py-3 rounded-2xl text-sm font-black bg-cyan-500 text-white shadow-lg shadow-cyan-500/25 hover:shadow-cyan-500/40 hover:-translate-y-0.5 active:translate-y-0 transition-all flex items-center gap-2 uppercase tracking-widest group">
<a @click.prevent="
if (hasReplenishPermission) {
window.location.href = '{{ route('admin.warehouses.replenishments') }}?auto_machine_id=' + selectedMachine?.id;
} else {
window.dispatchEvent(new CustomEvent('toast', { detail: { message: '{{ __('You do not have permission to access replenishment orders') }}', type: 'error' } }));
}
"
href="#"
class="px-6 py-3 rounded-2xl text-sm font-black bg-cyan-500 text-white shadow-lg shadow-cyan-500/25 hover:shadow-cyan-500/40 hover:-translate-y-0.5 active:translate-y-0 transition-all flex items-center gap-2 uppercase tracking-widest group cursor-pointer">
<svg class="w-5 h-5 group-hover:animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24"
stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
</svg>
{{ __('Auto Replenishment') }}
{{ __('Generate Replenishment Order') }}
</a>
</div>
</div>
@ -754,6 +761,7 @@ document.addEventListener('alpine:init', () => {
selectedMachine: null,
slots: [],
loading: false,
hasReplenishPermission: {{ auth()->user()->can('menu.warehouses.replenishments') ? 'true' : 'false' }},
async init() {
const urlParams = new URLSearchParams(window.location.search);

View File

@ -134,26 +134,36 @@
</button>
<div x-show="open && !sidebarCollapsed" x-collapse>
<ul class="luxury-submenu" data-sidebar-sub>
@can('menu.warehouses.overview')
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.index') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /></svg>
<span class="whitespace-nowrap overflow-hidden transition-all duration-300" :class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">{{ __('Warehouse Overview') }}</span>
</a></li>
@endcan
@can('menu.warehouses.inventory')
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.inventory') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.inventory') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg>
<span class="whitespace-nowrap overflow-hidden transition-all duration-300" :class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">{{ __('Inventory Management') }}</span>
</a></li>
@endcan
@can('menu.warehouses.transfers')
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.transfers') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.transfers') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" /></svg>
<span class="whitespace-nowrap overflow-hidden transition-all duration-300" :class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">{{ __('Transfer Orders') }}</span>
</a></li>
@endcan
@can('menu.warehouses.replenishments')
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.replenishments') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.replenishments') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg>
<span class="whitespace-nowrap overflow-hidden transition-all duration-300" :class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">{{ __('Machine Replenishment') }}</span>
</a></li>
@endcan
@can('menu.warehouses.machine-inventory')
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.machine-inventory') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.machine-inventory') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
<span class="whitespace-nowrap overflow-hidden transition-all duration-300" :class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">{{ __('Machine Inventory Overview') }}</span>
</a></li>
@endcan
</ul>
</div>
</li>
@ -172,14 +182,18 @@
</button>
<div x-show="open && !sidebarCollapsed" x-collapse>
<ul class="luxury-submenu" data-sidebar-sub>
@can('menu.sales.records')
<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>
@endcan
@can('menu.sales.pickup-codes')
<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>
@endcan
{{--
<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>
@ -190,10 +204,12 @@
<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>
--}}
@can('menu.sales.pass-codes')
<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>
@endcan
{{--
<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>
@ -321,14 +337,18 @@
</button>
<div x-show="open && !sidebarCollapsed" x-collapse>
<ul class="luxury-submenu" data-sidebar-sub>
@can('menu.remote.stock')
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.stock') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg>
<span class="whitespace-nowrap overflow-hidden transition-all duration-300" :class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">{{ __('Stock & Expiry') }}</span>
</a></li>
@endcan
@can('menu.remote.commands')
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.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.remote.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 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>
<span class="whitespace-nowrap overflow-hidden transition-all duration-300" :class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">{{ __('Command Center') }}</span>
</a></li>
@endcan
</ul>
</div>
</li>