[FEAT] 完善帳號管理狀態切換功能、優化多語系提示與 UI 樣式一致性
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 42s

This commit is contained in:
2026-03-25 17:16:41 +08:00
parent c015666f87
commit b7ff8ac01c
17 changed files with 349 additions and 46 deletions

View File

@@ -35,7 +35,55 @@
</div>
@endif
<form action="{{ route('admin.maintenance.store') }}" method="POST" enctype="multipart/form-data" class="space-y-6">
<form action="{{ route('admin.maintenance.store') }}" method="POST" enctype="multipart/form-data" class="space-y-6"
x-data="{
selectedFiles: [null, null, null],
handleFileChange(e, index) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
this.selectedFiles[index] = e.target.result;
};
reader.readAsDataURL(file);
}
},
removeFile(index) {
this.selectedFiles[index] = null;
const input = document.getElementById('photo-input-' + index);
if (input) input.value = '';
},
validate() {
const machineId = this.$el.querySelector('[name=machine_id]')?.value;
const category = this.$el.querySelector('[name=category]:checked');
const maintenanceAt = this.$el.querySelector('[name=maintenance_at]')?.value;
const content = this.$el.querySelector('[name=content]')?.value;
const isConfirmed = this.$el.querySelector('[name=is_confirmed]')?.checked;
if (!machineId || machineId.trim() === '') {
window.Alpine.store('toast').show('請選擇機台', 'error');
return false;
}
if (!category) {
window.Alpine.store('toast').show('請選擇維修類別', 'error');
return false;
}
if (!maintenanceAt) {
window.Alpine.store('toast').show('請選擇維修日期', 'error');
return false;
}
if (!content || content.trim() === '') {
window.Alpine.store('toast').show('請填寫維修內容', 'error');
return false;
}
if (!isConfirmed) {
window.Alpine.store('toast').show('請勾選確認已告知客戶並取得簽名', 'error');
return false;
}
return true;
}
}"
@submit.prevent="if(validate()) $el.submit()">
@csrf
<div class="luxury-card rounded-3xl p-8 animate-luxury-in space-y-8">
@@ -65,11 +113,14 @@
<label class="block text-sm font-black text-slate-700 dark:text-slate-200 uppercase tracking-wider mb-3">{{ __('Select Machine') }}</label>
<x-searchable-select name="machine_id" required :placeholder="__('Search serial no or name...')">
@foreach($machines as $m)
<option value="{{ $m->id }}" data-title="{{ $m->serial_no }} - {{ $m->name }}">
<option value="{{ $m->id }}" data-title="{{ $m->serial_no }} - {{ $m->name }}" {{ old('machine_id') == $m->id ? 'selected' : '' }}>
{{ $m->serial_no }} - {{ $m->name }}
</option>
@endforeach
</x-searchable-select>
@error('machine_id')
<p class="text-xs font-bold text-rose-500 mt-2 uppercase tracking-widest">{{ $message }}</p>
@enderror
</div>
@endif
</div>
@@ -80,45 +131,37 @@
<label class="block text-sm font-black text-slate-700 dark:text-slate-200 uppercase tracking-wider">{{ __('Category') }}</label>
<div class="grid grid-cols-2 gap-3">
@foreach(['Repair', 'Installation', 'Removal', 'Maintenance'] as $cat)
<label class="relative flex items-center justify-center p-3 rounded-2xl border-2 border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 cursor-pointer hover:border-cyan-500/30 transition-all group">
<input type="radio" name="category" value="{{ $cat }}" class="hidden peer" required {{ old('category') === $cat ? 'checked' : '' }}>
<label class="relative flex items-center justify-center p-3 rounded-2xl border-2 {{ $errors->has('category') ? 'border-rose-500/50' : 'border-slate-100 dark:border-slate-800' }} bg-slate-50/50 dark:bg-slate-900/50 cursor-pointer hover:border-cyan-500/30 transition-all group">
<input type="radio" name="category" value="{{ $cat }}" class="hidden peer" {{ old('category') === $cat ? 'checked' : '' }}>
<span class="text-sm font-black text-slate-600 dark:text-slate-400 peer-checked:text-cyan-500 transition-colors uppercase tracking-widest">{{ __($cat) }}</span>
<div class="absolute inset-0 rounded-2xl border-2 border-transparent peer-checked:border-cyan-500/50 peer-checked:bg-cyan-500/5 pointer-events-none transition-all"></div>
</label>
@endforeach
</div>
@error('category')
<p class="text-xs font-bold text-rose-500 mt-1 uppercase tracking-widest">{{ $message }}</p>
@enderror
</div>
<div class="space-y-4">
<label class="block text-sm font-black text-slate-700 dark:text-slate-200 uppercase tracking-wider">{{ __('Maintenance Date') }}</label>
<input type="datetime-local" name="maintenance_at" value="{{ old('maintenance_at', now()->format('Y-m-d\TH:i')) }}" required class="luxury-input w-full">
<input type="datetime-local" name="maintenance_at" value="{{ old('maintenance_at', now()->format('Y-m-d\TH:i')) }}" required class="luxury-input w-full {{ $errors->has('maintenance_at') ? 'border-rose-500/50 ring-4 ring-rose-500/10' : '' }}">
@error('maintenance_at')
<p class="text-xs font-bold text-rose-500 mt-2 uppercase tracking-widest">{{ $message }}</p>
@enderror
</div>
</div>
<div class="space-y-4">
<label class="block text-sm font-black text-slate-700 dark:text-slate-200 uppercase tracking-wider">{{ __('Maintenance Content') }}</label>
<textarea name="content" rows="4" class="luxury-input w-full p-6 text-sm" placeholder="{{ __('Describe the repair or maintenance status...') }}">{{ old('content') }}</textarea>
<textarea name="content" rows="4" class="luxury-input w-full p-6 text-sm {{ $errors->has('content') ? 'border-rose-500/50 ring-4 ring-rose-500/10' : '' }}" placeholder="{{ __('Describe the repair or maintenance status...') }}">{{ old('content') }}</textarea>
@error('content')
<p class="text-xs font-bold text-rose-500 mt-2 uppercase tracking-widest">{{ $message }}</p>
@enderror
</div>
<!-- Photos -->
<div class="space-y-4" x-data="{
selectedFiles: [null, null, null],
handleFileChange(e, index) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
this.selectedFiles[index] = e.target.result;
};
reader.readAsDataURL(file);
}
},
removeFile(index) {
this.selectedFiles[index] = null;
const input = document.getElementById('photo-input-' + index);
if (input) input.value = '';
}
}">
<div class="space-y-4">
<h3 class="text-sm font-black text-indigo-500 uppercase tracking-wider">{{ __('Maintenance Photos') }} ({{ __('Max 3') }})</h3>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6">
<template x-for="i in [0, 1, 2]" :key="i">
@@ -159,6 +202,25 @@
</div>
</div>
<!-- Confirmation Checkbox -->
<div class="px-4">
<label class="relative flex items-center group cursor-pointer w-fit">
<input type="checkbox" name="is_confirmed" value="1" class="peer h-6 w-6 rounded-lg border-2 {{ $errors->has('is_confirmed') ? 'border-rose-500/50' : 'border-slate-200 dark:border-slate-800' }} text-cyan-500 focus:ring-4 focus:ring-cyan-500/10 transition-all cursor-pointer bg-white dark:bg-slate-900 appearance-none checked:bg-cyan-500 checked:border-cyan-500">
<svg class="absolute h-4 w-4 text-white left-1 opacity-0 peer-checked:opacity-100 transition-opacity pointer-events-none stroke-[3]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
<div class="ml-4">
<span class="text-sm font-black {{ $errors->has('is_confirmed') ? 'text-rose-500' : 'text-slate-700 dark:text-slate-200' }} uppercase tracking-widest group-hover:text-cyan-600 transition-colors">
{{ __('已確認告知客戶維修內容並取得現場簽名確認') }}
</span>
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-[0.2em] mt-0.5">{{ __('Confirmed notification to customer and obtained signature') }}</p>
</div>
</label>
@error('is_confirmed')
<p class="text-xs font-bold text-rose-500 mt-2 uppercase tracking-widest ml-10">{{ $message }}</p>
@enderror
</div>
<div class="flex items-center justify-end gap-3 px-4">
<button type="button" onclick="history.back()" class="btn-luxury-ghost px-10">{{ __('Cancel') }}</button>
<button type="submit" class="btn-luxury-primary px-16 py-4 text-base">{{ __('Submit Record') }}</button>