[FEAT] 實作機台序號編輯功能與多語系支援
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 49s

1. 更新機台控制器 (MachineController),在更新時執行 serial_no 的必填與唯一性驗證。
2. 修改機台管理首頁 (index.blade.php),在快速編輯彈窗中加入機台序號輸入欄位。
3. 修正基礎設定中機台編輯頁面 (edit.blade.php),將原本唯讀的機台序號欄位改為可編輯輸入框,並加入必填標記。
4. 補齊並統一繁體中文、英文、日文翻譯檔中關於「機台序號」的翻譯 Key。
This commit is contained in:
2026-04-07 14:55:24 +08:00
parent f2147ae6c4
commit 253ae8afd4
4 changed files with 91 additions and 48 deletions

View File

@@ -102,8 +102,8 @@
<input type="text" name="name" value="{{ old('name', $machine->name) }}" class="luxury-input w-full" required>
</div>
<div>
<label class="block text-xs font-bold text-slate-400 uppercase tracking-[0.15em] mb-2">{{ __('Serial Number') }}</label>
<input type="text" value="{{ $machine->serial_no }}" class="luxury-input w-full bg-slate-50/50 dark:bg-slate-900/50 text-slate-400 cursor-not-allowed" readonly>
<label class="block text-xs font-bold text-slate-400 uppercase tracking-[0.15em] mb-2">{{ __('Machine Serial No') }} <span class="text-rose-500">*</span></label>
<input type="text" name="serial_no" value="{{ old('serial_no', $machine->serial_no) }}" class="luxury-input w-full" required>
</div>
<div>
<label class="block text-xs font-bold text-slate-400 uppercase tracking-[0.15em] mb-2">{{ __('Location') }}</label>

View File

@@ -118,7 +118,8 @@
};
</script>
<div class="space-y-4 pb-20 mt-4" x-data="machineApp()" @keydown.escape.window="showLogPanel = false; showInventoryPanel = false">
<div class="space-y-4 pb-20 mt-4" x-data="machineApp()"
@keydown.escape.window="showLogPanel = false; showInventoryPanel = false">
<!-- Top Header & Actions -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div class="flex items-center gap-4">
@@ -566,20 +567,21 @@
</div><!-- /Offcanvas -->
<!-- Edit Machine Name Modal -->
<div x-show="showEditModal" class="fixed inset-0 z-[100] overflow-y-auto" style="display: none;" role="dialog" aria-modal="true">
<div x-show="showEditModal" class="fixed inset-0 z-[100] overflow-y-auto" style="display: none;" role="dialog"
aria-modal="true">
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<!-- Background Backdrop -->
<div x-show="showEditModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity" @click="showEditModal = false">
class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity"
@click="showEditModal = false">
</div>
<span class="hidden sm:inline-block sm:align-middle sm:min-h-screen" aria-hidden="true">&#8203;</span>
<!-- Modal Panel -->
<div x-show="showEditModal"
x-transition:enter="ease-out duration-300"
<div x-show="showEditModal" 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"
@@ -588,15 +590,20 @@
class="inline-block align-bottom bg-white dark:bg-slate-900 rounded-[2.5rem] p-10 text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full border border-slate-100 dark:border-slate-800">
<div>
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight leading-none mb-2">{{ __('Edit Machine Name') }}</h3>
<p class="text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Update identification for your asset') }}</p>
<h3
class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight leading-none mb-2">
{{ __('Edit Machine Name') }}</h3>
<p class="text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{
__('Update identification for your asset') }}</p>
<form :action="'/admin/machines/' + editMachineId" method="POST" class="mt-8 space-y-6">
@csrf
@method('PUT')
<div class="space-y-4">
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.1em]">{{ __('New Machine Name') }}</label>
<label
class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.1em]">{{
__('New Machine Name') }}</label>
<input type="text" name="name" x-model="editMachineName" required
class="luxury-input block w-full px-6 py-4 text-base font-bold text-slate-800 dark:text-white bg-slate-50/50 dark:bg-slate-900/50"
placeholder="{{ __('Enter machine name...') }}">
@@ -623,10 +630,12 @@
aria-labelledby="inventory-panel-title" role="dialog" aria-modal="true">
<!-- Background backdrop -->
<div x-show="showInventoryPanel" x-transition:enter="ease-in-out duration-300" x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100" x-transition:leave="ease-in-out duration-300"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity" @click="showInventoryPanel = false">
<div x-show="showInventoryPanel" x-transition:enter="ease-in-out duration-300"
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
x-transition:leave="ease-in-out duration-300" x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity"
@click="showInventoryPanel = false">
</div>
<div class="fixed inset-y-0 right-0 max-w-full flex">
@@ -677,17 +686,26 @@
<!-- 統計摘要 -->
<div class="mt-6 flex items-center gap-4">
<div class="px-5 py-3 rounded-2xl bg-white dark:bg-slate-800/50 flex flex-col items-center min-w-[100px] border border-slate-100 dark:border-slate-800/50">
<span class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-0.5">{{ __('Total Slots') }}</span>
<span class="text-2xl font-black text-slate-700 dark:text-slate-200" x-text="inventorySlots.length"></span>
<div
class="px-5 py-3 rounded-2xl bg-white dark:bg-slate-800/50 flex flex-col items-center min-w-[100px] border border-slate-100 dark:border-slate-800/50">
<span class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-0.5">{{
__('Total Slots') }}</span>
<span class="text-2xl font-black text-slate-700 dark:text-slate-200"
x-text="inventorySlots.length"></span>
</div>
<div class="px-5 py-3 rounded-2xl bg-rose-500/5 border border-rose-500/10 flex flex-col items-center min-w-[100px]">
<span class="text-[9px] font-black text-rose-500 uppercase tracking-widest mb-0.5">{{ __('Low Stock') }}</span>
<span class="text-2xl font-black text-rose-600" x-text="inventorySlots.filter(s => s != null && s.stock <= 5).length"></span>
<div
class="px-5 py-3 rounded-2xl bg-rose-500/5 border border-rose-500/10 flex flex-col items-center min-w-[100px]">
<span class="text-[9px] font-black text-rose-500 uppercase tracking-widest mb-0.5">{{
__('Low Stock') }}</span>
<span class="text-2xl font-black text-rose-600"
x-text="inventorySlots.filter(s => s != null && s.stock <= 5).length"></span>
</div>
<div class="px-5 py-3 rounded-2xl bg-amber-500/5 border border-amber-500/10 flex flex-col items-center min-w-[100px]">
<span class="text-[9px] font-black text-amber-500 uppercase tracking-widest mb-0.5">{{ __('Expiring') }}</span>
<span class="text-2xl font-black text-amber-600" x-text="inventorySlots.filter(s => { if (!s || !s.expiry_date) return false; const diff = Math.round((new Date(s.expiry_date) - new Date()) / 86400000); return diff >= 0 && diff <= 7; }).length"></span>
<div
class="px-5 py-3 rounded-2xl bg-amber-500/5 border border-amber-500/10 flex flex-col items-center min-w-[100px]">
<span class="text-[9px] font-black text-amber-500 uppercase tracking-widest mb-0.5">{{
__('Expiring') }}</span>
<span class="text-2xl font-black text-amber-600"
x-text="inventorySlots.filter(s => { if (!s || !s.expiry_date) return false; const diff = Math.round((new Date(s.expiry_date) - new Date()) / 86400000); return diff >= 0 && diff <= 7; }).length"></span>
</div>
</div>
</div><!-- /Header -->
@@ -711,31 +729,42 @@
<div class="flex items-center gap-6 mb-6" x-show="!inventoryLoading">
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-rose-500 shadow-lg shadow-rose-500/30"></span>
<span class="text-[10px] font-black text-slate-500 uppercase tracking-[0.15em]">{{ __('Expired') }}</span>
<span class="text-[10px] font-black text-slate-500 uppercase tracking-[0.15em]">{{
__('Expired') }}</span>
</div>
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-amber-500 shadow-lg shadow-amber-500/30"></span>
<span class="text-[10px] font-black text-slate-500 uppercase tracking-[0.15em]">{{ __('Warning') }}</span>
<span
class="w-3 h-3 rounded-full bg-amber-500 shadow-lg shadow-amber-500/30"></span>
<span class="text-[10px] font-black text-slate-500 uppercase tracking-[0.15em]">{{
__('Warning') }}</span>
</div>
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-emerald-500 shadow-lg shadow-emerald-500/30"></span>
<span class="text-[10px] font-black text-slate-500 uppercase tracking-[0.15em]">{{ __('Normal') }}</span>
<span
class="w-3 h-3 rounded-full bg-emerald-500 shadow-lg shadow-emerald-500/30"></span>
<span class="text-[10px] font-black text-slate-500 uppercase tracking-[0.15em]">{{
__('Normal') }}</span>
</div>
</div>
<!-- Slots Grid (唯讀) -->
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-5" x-show="!inventoryLoading">
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-5"
x-show="!inventoryLoading">
<template x-for="slot in inventorySlots" :key="slot.id">
<div :class="getSlotColorClass(slot)"
class="min-h-[260px] rounded-[2rem] p-5 flex flex-col items-center justify-center border-2 transition-all duration-300 relative">
class="min-h-[260px] rounded-[2rem] p-5 flex flex-col items-center justify-center border-2 transition-all duration-300 relative">
<!-- Slot Header -->
<div class="absolute top-3.5 left-4 right-4 flex justify-between items-center z-10">
<div class="px-2.5 py-1 rounded-xl bg-slate-900/10 dark:bg-white/10 backdrop-blur-md border border-slate-900/5 dark:border-white/10 flex-shrink-0">
<span class="text-xs font-black uppercase tracking-tighter text-slate-800 dark:text-white" x-text="slot.slot_no"></span>
<div
class="absolute top-3.5 left-4 right-4 flex justify-between items-center z-10">
<div
class="px-2.5 py-1 rounded-xl bg-slate-900/10 dark:bg-white/10 backdrop-blur-md border border-slate-900/5 dark:border-white/10 flex-shrink-0">
<span
class="text-xs font-black uppercase tracking-tighter text-slate-800 dark:text-white"
x-text="slot.slot_no"></span>
</div>
<template x-if="slot.stock <= 2">
<div class="px-2 py-1 rounded-xl bg-rose-500 text-white text-[9px] font-black uppercase tracking-widest shadow-lg shadow-rose-500/30 animate-pulse whitespace-nowrap select-none">
<div
class="px-2 py-1 rounded-xl bg-rose-500 text-white text-[9px] font-black uppercase tracking-widest shadow-lg shadow-rose-500/30 animate-pulse whitespace-nowrap select-none">
{{ __('Low') }}
</div>
</template>
@@ -743,14 +772,19 @@
<!-- Product Image -->
<div class="relative w-16 h-16 mb-3 mt-2">
<div class="absolute inset-0 rounded-2xl bg-white/20 dark:bg-slate-900/40 backdrop-blur-xl border border-white/30 dark:border-white/5 shadow-inner overflow-hidden">
<div
class="absolute inset-0 rounded-2xl bg-white/20 dark:bg-slate-900/40 backdrop-blur-xl border border-white/30 dark:border-white/5 shadow-inner overflow-hidden">
<template x-if="slot.product && slot.product.image_url">
<img :src="slot.product.image_url" class="w-full h-full object-cover">
<img :src="slot.product.image_url"
class="w-full h-full object-cover">
</template>
<template x-if="!slot.product || !slot.product.image_url">
<div class="w-full h-full flex items-center justify-center">
<svg class="w-7 h-7 opacity-20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
<svg class="w-7 h-7 opacity-20" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2.5"
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
</div>
</template>
@@ -760,22 +794,29 @@
<!-- Slot Info -->
<div class="text-center w-full space-y-2">
<template x-if="slot.product">
<div class="text-sm font-black truncate w-full opacity-90 tracking-tight" x-text="slot.product.name"></div>
<div class="text-sm font-black truncate w-full opacity-90 tracking-tight"
x-text="slot.product.name"></div>
</template>
<template x-if="!slot.product">
<div class="text-sm font-bold text-slate-300 dark:text-slate-600 tracking-tight">{{ __('Empty') }}</div>
<div
class="text-sm font-bold text-slate-300 dark:text-slate-600 tracking-tight">
{{ __('Empty') }}</div>
</template>
<div class="space-y-2">
<!-- Stock Level -->
<div class="flex items-baseline justify-center gap-1">
<span class="text-xl font-black tracking-tighter leading-none" x-text="slot.stock"></span>
<span class="text-xl font-black tracking-tighter leading-none"
x-text="slot.stock"></span>
<span class="text-xs font-black opacity-30">/</span>
<span class="text-sm font-bold opacity-50" x-text="slot.max_stock || 10"></span>
<span class="text-sm font-bold opacity-50"
x-text="slot.max_stock || 10"></span>
</div>
<!-- Expiry Date -->
<div class="text-sm font-black tracking-tight leading-none opacity-80" x-text="slot.expiry_date ? slot.expiry_date.replace(/-/g, '/') : '----/--/--'"></div>
<div class="text-sm font-black tracking-tight leading-none opacity-80"
x-text="slot.expiry_date ? slot.expiry_date.replace(/-/g, '/') : '----/--/--'">
</div>
</div>
</div>
</div>
@@ -788,10 +829,10 @@
<div class="flex flex-col items-center">
<div
class="p-4 rounded-full bg-slate-50 dark:bg-slate-800/50 mb-4 border border-slate-100 dark:border-slate-800/50">
<svg class="w-8 h-8 text-slate-300 dark:text-slate-600"
viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5">
<path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
<svg class="w-8 h-8 text-slate-300 dark:text-slate-600" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="1.5">
<path
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
</div>
<span