[FEAT] 完善帳號管理狀態切換功能、優化多語系提示與 UI 樣式一致性
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 42s
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 42s
This commit is contained in:
@@ -25,7 +25,7 @@ class MachineSettingController extends AdminController
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$tab = $request->input('tab', 'machines');
|
||||
$per_page = $request->input('per_page', 20);
|
||||
$per_page = $request->input('per_page', 10);
|
||||
$search = $request->input('search');
|
||||
|
||||
// 1. 處理機台清單 (Machines Tab)
|
||||
@@ -36,14 +36,14 @@ class MachineSettingController extends AdminController
|
||||
->orWhere('serial_no', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
$machines = $machineQuery->latest()->paginate($per_page, ['*'], 'machines_page')->withQueryString();
|
||||
$machines = $machineQuery->latest()->paginate($per_page)->withQueryString();
|
||||
|
||||
// 2. 處理型號清單 (Models Tab)
|
||||
$modelQuery = MachineModel::query()->withCount('machines');
|
||||
if ($tab === 'models' && $search) {
|
||||
$modelQuery->where('name', 'like', "%{$search}%");
|
||||
}
|
||||
$models_list = $modelQuery->latest()->paginate($per_page, ['*'], 'models_page')->withQueryString();
|
||||
$models_list = $modelQuery->latest()->paginate($per_page)->withQueryString();
|
||||
|
||||
// 3. 基礎下拉資料 (用於新增/編輯機台的彈窗)
|
||||
$models = MachineModel::select('id', 'name')->get();
|
||||
|
||||
@@ -133,10 +133,32 @@ class CompanyController extends Controller
|
||||
]);
|
||||
|
||||
$company->update($validated);
|
||||
|
||||
// 分支邏輯:若停用客戶,連帶停用其所有帳號
|
||||
if ($validated['status'] == 0) {
|
||||
$company->users()->update(['status' => 0]);
|
||||
}
|
||||
|
||||
return redirect()->back()->with('success', __('Customer updated successfully.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 切換客戶狀態
|
||||
*/
|
||||
public function toggleStatus(Company $company)
|
||||
{
|
||||
$newStatus = $company->status == 1 ? 0 : 1;
|
||||
$company->update(['status' => $newStatus]);
|
||||
|
||||
// 若切換為停用,同步更新所有旗下帳號
|
||||
if ($newStatus == 0) {
|
||||
$company->users()->update(['status' => 0]);
|
||||
return redirect()->back()->with('success', __('Customer and associated accounts disabled successfully.'));
|
||||
}
|
||||
|
||||
return redirect()->back()->with('success', __('Customer enabled successfully.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
|
||||
@@ -14,7 +14,7 @@ class MachineController extends AdminController
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$tab = $request->input('tab', 'list');
|
||||
$per_page = $tab === 'list' ? $request->input('per_page', 10) : $request->input('per_page', 12);
|
||||
$per_page = $request->input('per_page', 10);
|
||||
|
||||
$query = Machine::query();
|
||||
|
||||
@@ -73,7 +73,7 @@ class MachineController extends AdminController
|
||||
*/
|
||||
public function logsAjax(Request $request, Machine $machine)
|
||||
{
|
||||
$per_page = $request->input('per_page', 20);
|
||||
$per_page = $request->input('per_page', 10);
|
||||
|
||||
$startDate = $request->get('start_date', now()->format('Y-m-d'));
|
||||
$endDate = $request->get('end_date', now()->format('Y-m-d'));
|
||||
|
||||
@@ -73,6 +73,7 @@ class MaintenanceController extends Controller
|
||||
'content' => 'nullable|string',
|
||||
'maintenance_at' => 'required|date',
|
||||
'photos.*' => 'nullable|image|max:5120', // 每張上限 5MB
|
||||
'is_confirmed' => 'required|accepted',
|
||||
]);
|
||||
|
||||
$machine = Machine::findOrFail($validated['machine_id']);
|
||||
@@ -97,6 +98,7 @@ class MaintenanceController extends Controller
|
||||
'content' => $validated['content'],
|
||||
'photos' => $photoPaths,
|
||||
'maintenance_at' => $validated['maintenance_at'],
|
||||
'is_confirmed' => true, // 既然通過驗證(accepted),則存為 true
|
||||
]);
|
||||
|
||||
return redirect()->route('admin.maintenance.index')
|
||||
|
||||
@@ -503,4 +503,20 @@ class PermissionController extends Controller
|
||||
|
||||
return redirect()->back()->with('success', __('Account deleted successfully.'));
|
||||
}
|
||||
|
||||
public function toggleAccountStatus($id)
|
||||
{
|
||||
$user = \App\Models\System\User::findOrFail($id);
|
||||
|
||||
// 禁止切換 Super Admin 狀態
|
||||
if ($user->hasRole('super-admin')) {
|
||||
return back()->with('error', __('Cannot change Super Admin status.'));
|
||||
}
|
||||
|
||||
$user->status = $user->status ? 0 : 1;
|
||||
$user->save();
|
||||
|
||||
$statusText = $user->status ? __('Enabled') : __('Disabled');
|
||||
return back()->with('success', __('Account :name status has been changed to :status.', ['name' => $user->name, 'status' => $statusText]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,13 @@ class MaintenanceRecord extends Model
|
||||
'content',
|
||||
'photos',
|
||||
'maintenance_at',
|
||||
'is_confirmed',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'photos' => 'array',
|
||||
'maintenance_at' => 'datetime',
|
||||
'is_confirmed' => 'boolean',
|
||||
];
|
||||
|
||||
public function machine()
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('maintenance_records', function (Blueprint $table) {
|
||||
$table->boolean('is_confirmed')->default(false)->after('photos')->comment('已確認告知客戶並簽名');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('maintenance_records', function (Blueprint $table) {
|
||||
$table->dropColumn('is_confirmed');
|
||||
});
|
||||
}
|
||||
};
|
||||
13
lang/en.json
13
lang/en.json
@@ -691,5 +691,14 @@
|
||||
"user": "一般用戶",
|
||||
"vs Yesterday": "vs Yesterday",
|
||||
"warehouses": "Warehouse Management",
|
||||
"待填寫": "Pending"
|
||||
}
|
||||
"待填寫": "Pending",
|
||||
"Enabled": "Enabled",
|
||||
"Account :name status has been changed to :status.": "Account :name status has been changed to :status.",
|
||||
"Cannot change Super Admin status.": "Cannot change Super Admin status.",
|
||||
"Confirm Status Change": "Confirm Status Change",
|
||||
"Are you sure you want to change the status? This may affect associated accounts.": "Are you sure you want to change the status? This may affect associated accounts.",
|
||||
"Confirm Account Status Change": "Confirm Account Status Change",
|
||||
"Are you sure you want to change the status? After disabling, this account will no longer be able to log in to the system.": "Are you sure you want to change the status? After disabling, this account will no longer be able to log in to the system.",
|
||||
"Confirm Account Deactivation": "Confirm Deactivation",
|
||||
"Are you sure you want to deactivate this account? After deactivating, this account will no longer be able to log in to the system.": "Are you sure you want to deactivate this account? After deactivating, this account will no longer be able to log in to the system."
|
||||
}
|
||||
|
||||
13
lang/ja.json
13
lang/ja.json
@@ -692,5 +692,14 @@
|
||||
"user": "一般用戶",
|
||||
"vs Yesterday": "前日比",
|
||||
"warehouses": "倉庫管理",
|
||||
"待填寫": "待填寫"
|
||||
}
|
||||
"待填寫": "待填寫",
|
||||
"Enabled": "有効中",
|
||||
"Account :name status has been changed to :status.": "アカウント :name のステータスが :status に変更されました。",
|
||||
"Cannot change Super Admin status.": "スーパー管理者のステータスは変更できません。",
|
||||
"Confirm Status Change": "ステータス変更の確認",
|
||||
"Are you sure you want to change the status? This may affect associated accounts.": "ステータスを変更してもよろしいですか?関連するアカウントに影響する可能性があります。",
|
||||
"Confirm Account Status Change": "アカウントステータス変更の確認",
|
||||
"Are you sure you want to change the status? After disabling, this account will no longer be able to log in to the system.": "ステータスを変更してもよろしいですか?無効化後、このアカウントはシステムにログインできなくなります。",
|
||||
"Confirm Account Deactivation": "アカウント停止の確認",
|
||||
"Are you sure you want to deactivate this account? After deactivating, this account will no longer be able to log in to the system.": "アカウントを停止してもよろしいですか?一旦停止するとシステムにログインできなくなります。"
|
||||
}
|
||||
|
||||
@@ -688,5 +688,14 @@
|
||||
"user": "一般用戶",
|
||||
"vs Yesterday": "較昨日",
|
||||
"warehouses": "倉庫管理",
|
||||
"待填寫": "待填寫"
|
||||
}
|
||||
"待填寫": "待填寫",
|
||||
"Enabled": "已啟用",
|
||||
"Account :name status has been changed to :status.": "帳號 :name 的狀態已變更為 :status。",
|
||||
"Cannot change Super Admin status.": "無法變更超級管理員的狀態。",
|
||||
"Confirm Status Change": "確認變更狀態",
|
||||
"Are you sure you want to change the status? This may affect associated accounts.": "您確定要變更狀態嗎?這可能會影響相關帳號的權限效力。",
|
||||
"Confirm Account Status Change": "帳號狀態變更確認",
|
||||
"Are you sure you want to change the status? After disabling, this account will no longer be able to log in to the system.": "您確定要變更狀態嗎?停用之後,該帳號將會立即被登出且無法再登入系統。",
|
||||
"Confirm Account Deactivation": "停用帳號確認",
|
||||
"Are you sure you want to deactivate this account? After deactivating, this account will no longer be able to log in to the system.": "確定要停用此帳號嗎?停用後將無法登入系統。"
|
||||
}
|
||||
|
||||
@@ -179,8 +179,8 @@ return [
|
||||
*/
|
||||
|
||||
'custom' => [
|
||||
'attribute-name' => [
|
||||
'rule-name' => 'custom-message',
|
||||
'is_confirmed' => [
|
||||
'accepted' => '您必須勾選確認已告知客戶並取得簽名。',
|
||||
],
|
||||
],
|
||||
|
||||
@@ -202,6 +202,11 @@ return [
|
||||
'current_password' => '目前密碼',
|
||||
'password_confirmation' => '確認密碼',
|
||||
'phone' => '電話',
|
||||
'machine_id' => '機台',
|
||||
'category' => '類別',
|
||||
'maintenance_at' => '維修日期',
|
||||
'content' => '維修內容',
|
||||
'is_confirmed' => '確認勾選框',
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -24,10 +24,22 @@
|
||||
openEditModal(company) {
|
||||
this.editing = true;
|
||||
this.currentCompany = { ...company };
|
||||
this.originalStatus = company.status;
|
||||
this.showModal = true;
|
||||
},
|
||||
isDeleteConfirmOpen: false,
|
||||
deleteFormAction: ''
|
||||
deleteFormAction: '',
|
||||
isStatusConfirmOpen: false,
|
||||
originalStatus: 1,
|
||||
toggleFormAction: '',
|
||||
statusToggleSource: 'edit',
|
||||
submitConfirmedForm() {
|
||||
if (this.statusToggleSource === 'list') {
|
||||
this.$refs.statusToggleForm.submit();
|
||||
} else {
|
||||
this.$refs.companyForm.submit();
|
||||
}
|
||||
}
|
||||
}">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||
@@ -148,7 +160,7 @@
|
||||
</span>
|
||||
@else
|
||||
<span
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-black bg-slate-100 dark:bg-slate-800 text-slate-400 dark:text-slate-500 border border-slate-200 dark:border-slate-700 tracking-widest uppercase">
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-black bg-rose-500/10 text-rose-500 border border-rose-500/20 tracking-widest uppercase">
|
||||
{{ __('Disabled') }}
|
||||
</span>
|
||||
@endif
|
||||
@@ -174,7 +186,26 @@
|
||||
</td>
|
||||
<td class="px-6 py-6 text-right">
|
||||
<div class="flex items-center justify-end gap-x-2">
|
||||
<button @click="openEditModal({{ json_encode($company) }})"
|
||||
@if($company->status)
|
||||
<button type="button"
|
||||
@click="toggleFormAction = '{{ route('admin.permission.companies.status.toggle', $company->id) }}'; statusToggleSource = 'list'; isStatusConfirmOpen = true"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-amber-500 hover:bg-amber-500/5 transition-all border border-transparent hover:border-amber-500/20"
|
||||
title="{{ __('Disable') }}">
|
||||
<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="M15.75 5.25v13.5m-7.5-13.5v13.5" />
|
||||
</svg>
|
||||
</button>
|
||||
@else
|
||||
<button type="button"
|
||||
@click="toggleFormAction = '{{ route('admin.permission.companies.status.toggle', $company->id) }}'; $nextTick(() => $refs.statusToggleForm.submit())"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 transition-all border border-transparent hover:border-emerald-500/20"
|
||||
title="{{ __('Enable') }}">
|
||||
<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="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347c-.75.412-1.667-.13-1.667-.986V5.653z" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
<button @click="statusToggleSource = 'edit'; openEditModal({{ json_encode($company) }})"
|
||||
class="p-2 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">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
stroke-width="2.5">
|
||||
@@ -251,8 +282,16 @@
|
||||
</div>
|
||||
|
||||
<form
|
||||
x-ref="companyForm"
|
||||
:action="editing ? '{{ url('admin/permission/companies') }}/' + currentCompany.id : '{{ route('admin.permission.companies.store') }}'"
|
||||
method="POST" class="space-y-6">
|
||||
method="POST" class="space-y-6"
|
||||
@submit.prevent="
|
||||
if (editing && currentCompany.status == '0' && originalStatus == '1') {
|
||||
isStatusConfirmOpen = true;
|
||||
} else {
|
||||
$el.submit();
|
||||
}
|
||||
">
|
||||
@csrf
|
||||
<input type="hidden" name="_method" :value="editing ? 'PUT' : 'POST'">
|
||||
|
||||
@@ -405,6 +444,12 @@
|
||||
</div>
|
||||
|
||||
<x-delete-confirm-modal :message="__('Are you sure to delete this customer?')" />
|
||||
<x-status-confirm-modal :title="__('停用客戶確認')" :message="__('停用此客戶將會連帶停用該客戶下的所有帳號,確定要繼續嗎?')" />
|
||||
|
||||
<form x-ref="statusToggleForm" :action="toggleFormAction" method="POST" class="hidden">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@@ -182,13 +182,12 @@ $roleSelectConfig = [
|
||||
<td class="px-6 py-6 text-center">
|
||||
@if($user->status)
|
||||
<span
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20 tracking-widest uppercase">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 mr-2 animate-pulse"></span>
|
||||
class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-bold bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20 tracking-widest uppercase">
|
||||
{{ __('Active') }}
|
||||
</span>
|
||||
@else
|
||||
<span
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 border border-slate-200 dark:border-slate-700 tracking-widest uppercase">
|
||||
class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-bold bg-rose-500/10 text-rose-600 dark:text-rose-400 border border-rose-500/20 tracking-widest uppercase">
|
||||
{{ __('Disabled') }}
|
||||
</span>
|
||||
@endif
|
||||
@@ -196,6 +195,25 @@ $roleSelectConfig = [
|
||||
<td class="px-6 py-6 text-right">
|
||||
<div class="flex justify-end items-center gap-2">
|
||||
@if(!$user->hasRole('super-admin'))
|
||||
@if($user->status)
|
||||
<button type="button"
|
||||
@click="toggleFormAction = '{{ route($baseRoute . '.status.toggle', $user->id) }}'; statusToggleSource = 'list'; isStatusConfirmOpen = true"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-amber-500 hover:bg-amber-500/5 transition-all border border-transparent hover:border-amber-500/20"
|
||||
title="{{ __('Disable') }}">
|
||||
<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="M15.75 5.25v13.5m-7.5-13.5v13.5" />
|
||||
</svg>
|
||||
</button>
|
||||
@else
|
||||
<button type="button"
|
||||
@click="toggleFormAction = '{{ route($baseRoute . '.status.toggle', $user->id) }}'; $nextTick(() => $refs.statusToggleForm.submit())"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 transition-all border border-transparent hover:border-emerald-500/20"
|
||||
title="{{ __('Enable') }}">
|
||||
<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="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347c-.75.412-1.667-.13-1.667-.986V5.653z" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
<button @click="openEditModal(@js($user))"
|
||||
class="p-2 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') }}">
|
||||
@@ -396,7 +414,7 @@ $roleSelectConfig = [
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form
|
||||
<form x-ref="accountForm"
|
||||
:action="!editing ? '{{ route($baseRoute . '.store') }}' : '{{ route($baseRoute) }}/' + currentUser.id"
|
||||
method="POST" class="space-y-6">
|
||||
@csrf
|
||||
@@ -594,6 +612,13 @@ $roleSelectConfig = [
|
||||
<x-delete-confirm-modal
|
||||
:message="__('Are you sure you want to delete this account? This action cannot be undone.')" />
|
||||
|
||||
<!-- Status Change Confirm Modal -->
|
||||
<x-status-confirm-modal :title="__('Confirm Account Deactivation')" :message="__('Are you sure you want to deactivate this account? After deactivating, this account will no longer be able to log in to the system.')" />
|
||||
|
||||
<form x-ref="statusToggleForm" :action="toggleFormAction" method="POST" class="hidden">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@@ -617,6 +642,9 @@ $roleSelectConfig = [
|
||||
roleSelectConfig: initData.roleSelectConfig,
|
||||
isDeleteConfirmOpen: false,
|
||||
deleteFormAction: '',
|
||||
isStatusConfirmOpen: false,
|
||||
toggleFormAction: '',
|
||||
statusToggleSource: 'list',
|
||||
confirmDelete(action) {
|
||||
this.deleteFormAction = action;
|
||||
this.isDeleteConfirmOpen = true;
|
||||
@@ -701,6 +729,13 @@ $roleSelectConfig = [
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
submitConfirmedForm() {
|
||||
if (this.statusToggleSource === 'list') {
|
||||
this.$refs.statusToggleForm.submit();
|
||||
} else {
|
||||
this.$refs.accountForm.submit();
|
||||
}
|
||||
},
|
||||
get filteredRoles() {
|
||||
const companyId = this.currentUser.company_id;
|
||||
if (!companyId || companyId.toString().trim() === '') {
|
||||
|
||||
@@ -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>
|
||||
|
||||
56
resources/views/components/status-confirm-modal.blade.php
Normal file
56
resources/views/components/status-confirm-modal.blade.php
Normal file
@@ -0,0 +1,56 @@
|
||||
@props([
|
||||
'message' => __('Are you sure you want to change the status? This may affect associated accounts.'),
|
||||
'title' => __('Confirm Status Change')
|
||||
])
|
||||
|
||||
<template x-teleport="body">
|
||||
<div x-show="isStatusConfirmOpen" class="fixed inset-0 z-[200] 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="isStatusConfirmOpen" 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="isStatusConfirmOpen = false"></div>
|
||||
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
||||
|
||||
<div x-show="isStatusConfirmOpen" 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 px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white dark:bg-slate-900 rounded-3xl shadow-2xl sm:my-8 sm:align-middle sm:max-w-md sm:w-full sm:p-8 border border-slate-100 dark:border-slate-800">
|
||||
|
||||
<div class="sm:flex sm:items-start text-center sm:text-left">
|
||||
<div
|
||||
class="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto bg-amber-100 dark:bg-amber-500/10 rounded-2xl sm:mx-0 sm:h-12 sm:w-12 text-amber-600 dark:text-amber-400">
|
||||
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mt-3 sm:mt-0 sm:ml-6">
|
||||
<h3 class="text-xl font-black text-slate-800 dark:text-white leading-6 tracking-tight font-display uppercase">
|
||||
{{ $title }}
|
||||
</h3>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 leading-relaxed">
|
||||
{{ $message }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 sm:mt-10 sm:flex sm:flex-row-reverse gap-3">
|
||||
<button type="button" @click="submitConfirmedForm()"
|
||||
class="inline-flex justify-center w-full px-6 py-3 text-sm font-black text-white transition-all bg-amber-500 rounded-xl hover:bg-amber-600 shadow-lg shadow-amber-200 dark:shadow-none hover:scale-[1.02] active:scale-[0.98] sm:w-auto uppercase tracking-widest font-display">
|
||||
{{ __('Confirm') }}
|
||||
</button>
|
||||
<button type="button" @click="isStatusConfirmOpen = false"
|
||||
class="inline-flex justify-center w-full px-6 py-3 mt-3 text-sm font-black text-slate-700 dark:text-slate-200 transition-all bg-slate-100 dark:bg-slate-800 rounded-xl hover:bg-slate-200 dark:hover:bg-slate-700 sm:mt-0 sm:w-auto uppercase tracking-widest font-display">
|
||||
{{ __('Cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -7,7 +7,7 @@
|
||||
<span class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest pl-1 leading-none">{{ __('Show') }}</span>
|
||||
<div class="relative group flex items-center">
|
||||
@php
|
||||
$currentLimit = request('per_page', 10);
|
||||
$currentLimit = $paginator->perPage();
|
||||
$limits = [10, 25, 50, 100];
|
||||
@endphp
|
||||
<select onchange="const params = new URLSearchParams(window.location.search); params.set('per_page', this.value); params.delete('page'); window.location.href = window.location.pathname + '?' + params.toString();"
|
||||
|
||||
@@ -117,6 +117,7 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
|
||||
Route::get('/advertisements', [App\Http\Controllers\Admin\DataConfigController::class , 'advertisements'])->name('advertisements');
|
||||
Route::get('/admin-products', [App\Http\Controllers\Admin\DataConfigController::class , 'adminProducts'])->name('admin-products');
|
||||
Route::get('/sub-accounts', [App\Http\Controllers\Admin\PermissionController::class , 'accounts'])->name('sub-accounts')->middleware('can:menu.data-config.sub-accounts');
|
||||
Route::patch('/sub-accounts/{id}/toggle-status', [App\Http\Controllers\Admin\PermissionController::class, 'toggleAccountStatus'])->name('sub-accounts.status.toggle')->middleware('can:menu.data-config.sub-accounts');
|
||||
Route::post('/sub-accounts', [App\Http\Controllers\Admin\PermissionController::class , 'storeAccount'])->name('sub-accounts.store')->middleware('can:menu.data-config.sub-accounts');
|
||||
Route::put('/sub-accounts/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'updateAccount'])->name('sub-accounts.update')->middleware('can:menu.data-config.sub-accounts');
|
||||
Route::delete('/sub-accounts/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'destroyAccount'])->name('sub-accounts.destroy')->middleware('can:menu.data-config.sub-accounts');
|
||||
@@ -196,8 +197,10 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
|
||||
|
||||
// 15. 權限設定
|
||||
Route::prefix('permission')->name('permission.')->group(function () {
|
||||
Route::patch('companies/{company}/toggle-status', [App\Http\Controllers\Admin\CompanyController::class, 'toggleStatus'])->name('companies.status.toggle')->middleware('can:menu.permissions.companies');
|
||||
Route::resource('companies', App\Http\Controllers\Admin\CompanyController::class)->except(['show', 'create', 'edit'])->middleware('can:menu.permissions.companies');
|
||||
Route::get('/accounts', [App\Http\Controllers\Admin\PermissionController::class , 'accounts'])->name('accounts')->middleware('can:menu.permissions.accounts');
|
||||
Route::patch('/accounts/{id}/toggle-status', [App\Http\Controllers\Admin\PermissionController::class, 'toggleAccountStatus'])->name('accounts.status.toggle')->middleware('can:menu.permissions.accounts');
|
||||
Route::post('/accounts', [App\Http\Controllers\Admin\PermissionController::class , 'storeAccount'])->name('accounts.store')->middleware('can:menu.permissions.accounts');
|
||||
Route::put('/accounts/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'updateAccount'])->name('accounts.update')->middleware('can:menu.permissions.accounts');
|
||||
Route::delete('/accounts/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'destroyAccount'])->name('accounts.destroy')->middleware('can:menu.permissions.accounts');
|
||||
|
||||
Reference in New Issue
Block a user