diff --git a/app/Http/Controllers/Admin/AdvertisementController.php b/app/Http/Controllers/Admin/AdvertisementController.php new file mode 100644 index 0000000..7a17c50 --- /dev/null +++ b/app/Http/Controllers/Admin/AdvertisementController.php @@ -0,0 +1,221 @@ +user(); + $tab = $request->input('tab', 'list'); + + // Tab 1: 廣告列表 + $advertisements = Advertisement::with('company')->latest()->paginate(10); + $allAds = Advertisement::active()->get(); + + // Tab 2: 機台廣告設置 (所需資料) + // 取得使用者有權限的機台列表 (已透過 Global Scope 過濾) + $machines = Machine::select('id', 'name', 'serial_no', 'company_id')->get(); + + $companies = $user->isSystemAdmin() ? Company::orderBy('name')->get() : collect(); + + return view('admin.ads.index', [ + 'advertisements' => $advertisements, + 'machines' => $machines, + 'tab' => $tab, + 'allAds' => $allAds, + 'companies' => $companies, + ]); + } + + /** + * 素材 CRUD: 儲存廣告 + */ + public function store(Request $request) + { + $request->validate([ + 'name' => 'required|string|max:255', + 'type' => 'required|in:image,video', + 'duration' => 'required|in:15,30,60', + 'file' => [ + 'required', + 'file', + 'mimes:jpeg,png,jpg,gif,webp,mp4,mov,avi', + $request->type === 'image' ? 'max:5120' : 'max:51200', // Image 5MB, Video 50MB + ], + 'company_id' => 'nullable|exists:companies,id', + ]); + + $user = auth()->user(); + $path = $request->file('file')->store('ads', 'public'); + + if ($user->isSystemAdmin()) { + $companyId = $request->filled('company_id') ? $request->company_id : null; + } else { + $companyId = $user->company_id; + } + + Advertisement::create([ + 'company_id' => $companyId, + 'name' => $request->name, + 'type' => $request->type, + 'duration' => (int) $request->duration, + 'url' => Storage::disk('public')->url($path), + 'is_active' => true, + ]); + + return redirect()->back()->with('success', __('Advertisement created successfully.')); + } + + public function update(Request $request, Advertisement $advertisement) + { + $rules = [ + 'name' => 'required|string|max:255', + 'type' => 'required|in:image,video', + 'duration' => 'required|in:15,30,60', + 'is_active' => 'boolean', + 'company_id' => 'nullable|exists:companies,id', + ]; + + if ($request->hasFile('file')) { + $rules['file'] = [ + 'file', + 'mimes:jpeg,png,jpg,gif,webp,mp4,mov,avi', + $request->type === 'image' ? 'max:5120' : 'max:51200', + ]; + } + + $request->validate($rules); + + $data = $request->only(['name', 'type', 'duration']); + $data['is_active'] = $request->has('is_active'); + + $user = auth()->user(); + if ($user->isSystemAdmin()) { + $data['company_id'] = $request->filled('company_id') ? $request->company_id : null; + } + + if ($request->hasFile('file')) { + // 刪除舊檔案 + $oldPath = str_replace(Storage::disk('public')->url(''), '', $advertisement->url); + Storage::disk('public')->delete($oldPath); + + // 存入新檔案 + $path = $request->file('file')->store('ads', 'public'); + $data['url'] = Storage::disk('public')->url($path); + } + + $advertisement->update($data); + + return redirect()->back()->with('success', __('Advertisement updated successfully.')); + } + + public function destroy(Advertisement $advertisement) + { + // 檢查是否有機台正投放中 + if ($advertisement->machineAdvertisements()->exists()) { + return redirect()->back()->with('error', __('Cannot delete advertisement being used by machines.')); + } + + // 刪除實體檔案 + $path = str_replace(Storage::disk('public')->url(''), '', $advertisement->url); + Storage::disk('public')->delete($path); + + $advertisement->delete(); + return redirect()->back()->with('success', __('Advertisement deleted successfully.')); + } + + /** + * AJAX: 取得特定機台的廣告投放清單 + */ + public function getMachineAds(Machine $machine) + { + $assignments = MachineAdvertisement::where('machine_id', $machine->id) + ->with('advertisement') + ->get() + ->groupBy('position'); + + return response()->json([ + 'success' => true, + 'data' => $assignments + ]); + } + + /** + * 投放廣告至機台 + */ + public function assign(Request $request) + { + $request->validate([ + 'machine_id' => 'required|exists:machines,id', + 'advertisement_id' => 'required|exists:advertisements,id', + 'position' => 'required|in:vending,visit_gift,standby', + 'sort_order' => 'nullable|integer', + ]); + + // If sort_order is not provided, append to the end of the current position list + $newSortOrder = $request->sort_order; + if (is_null($newSortOrder)) { + $newSortOrder = MachineAdvertisement::where('machine_id', $request->machine_id) + ->where('position', $request->position) + ->max('sort_order') + 1; + } + + MachineAdvertisement::updateOrCreate( + [ + 'machine_id' => $request->machine_id, + 'position' => $request->position, + 'advertisement_id' => $request->advertisement_id, + ], + [ + 'sort_order' => $newSortOrder, + ] + ); + + return response()->json([ + 'success' => true, + 'message' => __('Advertisement assigned successfully.') + ]); + } + + /** + * 重新排序廣告播放順序 + */ + public function reorderAssignments(Request $request) + { + $request->validate([ + 'assignment_ids' => 'required|array', + 'assignment_ids.*' => 'exists:machine_advertisements,id' + ]); + + foreach ($request->assignment_ids as $index => $id) { + MachineAdvertisement::where('id', $id)->update(['sort_order' => $index]); + } + + return response()->json([ + 'success' => true, + 'message' => __('Order updated successfully.') + ]); + } + + /** + * 移除廣告投放 + */ + public function removeAssignment($id) + { + $assignment = MachineAdvertisement::findOrFail($id); + $assignment->delete(); + + return response()->json([ + 'success' => true, + 'message' => __('Assignment removed successfully.') + ]); + } +} diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 70795cf..049ddb1 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -48,7 +48,7 @@ class ProfileController extends Controller public function updateAvatar(Request $request): \Illuminate\Http\JsonResponse { $request->validate([ - 'avatar' => ['required', 'image', 'mimes:jpeg,png,jpg,gif', 'max:1024'], + 'avatar' => ['required', 'image', 'mimes:jpeg,png,jpg,gif,webp', 'max:1024'], ]); $user = $request->user(); diff --git a/app/Models/Machine/MachineAdvertisement.php b/app/Models/Machine/MachineAdvertisement.php new file mode 100644 index 0000000..a8e3689 --- /dev/null +++ b/app/Models/Machine/MachineAdvertisement.php @@ -0,0 +1,43 @@ + 'integer', + 'start_at' => 'datetime', + 'end_at' => 'datetime', + ]; + + /** + * Get the advertisement associated with this assignment. + */ + public function advertisement() + { + return $this->belongsTo(Advertisement::class); + } + + /** + * Get the machine associated with this assignment. + */ + public function machine() + { + return $this->belongsTo(Machine::class); + } +} diff --git a/app/Models/System/Advertisement.php b/app/Models/System/Advertisement.php new file mode 100644 index 0000000..b887c96 --- /dev/null +++ b/app/Models/System/Advertisement.php @@ -0,0 +1,51 @@ + 'integer', + 'is_active' => 'boolean', + ]; + + /** + * Get the machine assignments for this advertisement. + */ + public function machineAdvertisements() + { + return $this->hasMany(MachineAdvertisement::class); + } + + /** + * Get the company that owns the advertisement. + */ + public function company() + { + return $this->belongsTo(Company::class); + } + + /** + * Scope a query to only include active advertisements. + */ + public function scopeActive($query) + { + return $query->where('is_active', true); + } +} diff --git a/database/migrations/2026_03_31_090739_create_advertisements_table.php b/database/migrations/2026_03_31_090739_create_advertisements_table.php new file mode 100644 index 0000000..f7e5c76 --- /dev/null +++ b/database/migrations/2026_03_31_090739_create_advertisements_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('company_id')->nullable()->constrained()->onDelete('cascade'); + $table->string('name'); + $table->string('type')->default('image'); // image, video + $table->integer('duration')->default(15); // 15, 30, 60 + $table->string('url'); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('advertisements'); + } +}; diff --git a/database/migrations/2026_03_31_090740_create_machine_advertisements_table.php b/database/migrations/2026_03_31_090740_create_machine_advertisements_table.php new file mode 100644 index 0000000..6d1a6fc --- /dev/null +++ b/database/migrations/2026_03_31_090740_create_machine_advertisements_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('machine_id')->constrained()->onDelete('cascade'); + $table->foreignId('advertisement_id')->constrained()->onDelete('cascade'); + $table->string('position')->comment('vending, visit_gift, standby'); + $table->integer('sort_order')->default(0); + $table->dateTime('start_at')->nullable(); + $table->dateTime('end_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('machine_advertisements'); + } +}; diff --git a/lang/en.json b/lang/en.json index 6b15b87..48bda6f 100644 --- a/lang/en.json +++ b/lang/en.json @@ -800,5 +800,72 @@ "user": "一般用戶", "vs Yesterday": "vs Yesterday", "warehouses": "Warehouse Management", - "待填寫": "Pending" + "待填寫": "Pending", + "Advertisement List": "Advertisement List", + "Machine Advertisement Settings": "Machine Advertisement Settings", + "Add Advertisement": "Add Advertisement", + "Edit Advertisement": "Edit Advertisement", + "Delete Advertisement": "Delete Advertisement", + "Duration": "Duration", + "15 Seconds": "15 Seconds", + "30 Seconds": "30 Seconds", + "60 Seconds": "60 Seconds", + "Position": "Position", + "Standby Ad": "Standby Ad", + "Assign Advertisement": "Assign Advertisement", + "Please select a machine first": "Please select a machine first", + "Advertisement created successfully": "Ad created successfully", + "Advertisement updated successfully": "Ad updated successfully", + "Advertisement deleted successfully": "Ad deleted successfully", + "Advertisement assigned successfully": "Ad assigned successfully", + "Vending": "Vending", + "Visit Gift": "Visit Gift", + "Standby": "Standby", + "Advertisement Video/Image": "Ad Video/Image", + "Sort Order": "Sort Order", + "Date Range": "Date Range", + "Manage ad materials and machine playback settings": "Manage ad materials and machine playback settings", + "Preview": "Preview", + "No advertisements found.": "No advertisements found.", + "vending": "Vending Page", + "visit_gift": "Visit Gift", + "standby": "Standby AD", + "No assignments": "No assignments", + "Please select a machine to view and manage its advertisements.": "Please select a machine to view and manage its advertisements.", + "Delete Advertisement Confirmation": "Delete Advertisement Confirmation", + "Are you sure you want to delete this advertisement? This will also remove all assignments to machines.": "Are you sure you want to delete this advertisement? This will also remove all assignments to machines.", + "Manage your ad material details": "Manage your ad material details", + "Material Name": "Material Name", + "Enter ad material name": "Enter ad material name", + "Material Type": "Material Type", + "Duration (Seconds)": "Duration (Seconds)", + "Seconds": "Seconds", + "Upload Image": "Upload Image", + "Upload Video": "Upload Video", + "Active Status": "Active Status", + "Save Material": "Save Material", + "Select a material to play on this machine": "Select a material to play on this machine", + "Target Position": "Target Position", + "Select Material": "Select Material", + "Please select a material": "Please select a material", + "Playback Order": "Playback Order", + "Smallest number plays first.": "Smallest number plays first.", + "Confirm Assignment": "Confirm Assignment", + "Are you sure you want to remove this assignment?": "Are you sure you want to remove this assignment?", + "image": "Image", + "video": "Video", + "Search Machine...": "Search Machine...", + "Advertisement created successfully.": "Advertisement created successfully.", + "Advertisement updated successfully.": "Advertisement updated successfully.", + "Advertisement deleted successfully.": "Advertisement deleted successfully.", + "Cannot delete advertisement being used by machines.": "Cannot delete advertisement being used by machines.", + "Advertisement assigned successfully.": "Advertisement assigned successfully.", + "Assignment removed successfully.": "Assignment removed successfully.", + "Max 5MB": "Max 5MB", + "Max 50MB": "Max 50MB", + "Select...": "Select...", + "Ad Settings": "Ad Settings", + "System Default (All Companies)": "System Default (All Companies)", + "No materials available": "No materials available", + "Search...": "Search..." } \ No newline at end of file diff --git a/lang/ja.json b/lang/ja.json index 0a32498..bf0dc40 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -36,7 +36,7 @@ "Admin Sellable Products": "管理者販売可能商品", "Admin display name": "管理者表示名", "Administrator": "管理者", - "Advertisement Management": "廣告管理", + "Advertisement Management": "広告管理", "Affiliated Unit": "会社名", "Affiliation": "会社名", "Alert Summary": "アラート概要", @@ -803,5 +803,72 @@ "user": "一般用戶", "vs Yesterday": "前日比", "warehouses": "倉庫管理", - "待填寫": "待填寫" + "待填寫": "待填寫", + "Advertisement List": "広告リスト", + "Machine Advertisement Settings": "機台広告設定", + "Add Advertisement": "広告を追加", + "Edit Advertisement": "広告を編集", + "Delete Advertisement": "広告を削除", + "Duration": "再生時間", + "15 Seconds": "15秒", + "30 Seconds": "30秒", + "60 Seconds": "60秒", + "Position": "配信位置", + "Standby Ad": "待機広告", + "Assign Advertisement": "広告を配信", + "Please select a machine first": "まず機台を選択してください", + "Advertisement created successfully": "広告が正常に作成されました", + "Advertisement updated successfully": "広告が正常に更新されました", + "Advertisement deleted successfully": "広告が正常に削除されました", + "Advertisement assigned successfully": "広告配信が完了しました", + "Vending": "販売画面", + "Visit Gift": "来店特典", + "Standby": "待機広告", + "Advertisement Video/Image": "広告動画/画像", + "Sort Order": "並べ替え順序", + "Date Range": "日時範囲", + "Manage ad materials and machine playback settings": "広告素材と機台再生設定を管理します", + "Preview": "プレビュー", + "No advertisements found.": "広告が見つかりませんでした。", + "vending": "販売画面", + "visit_gift": "来店特典", + "standby": "待機広告", + "No assignments": "配信設定なし", + "Please select a machine to view and manage its advertisements.": "広告を表示および管理する機台を選択してください。", + "Delete Advertisement Confirmation": "広告削除の確認", + "Are you sure you want to delete this advertisement? This will also remove all assignments to machines.": "この広告を削除してもよろしいですか?すべての機台への配信設定も削除されます。", + "Manage your ad material details": "広告素材の詳細を管理します", + "Material Name": "素材名", + "Enter ad material name": "素材名を入力してください", + "Material Type": "素材タイプ", + "Duration (Seconds)": "再生時間(秒)", + "Seconds": "秒", + "Upload Image": "画像をアップロード", + "Upload Video": "動画をアップロード", + "Active Status": "有効ステータス", + "Save Material": "素材を保存", + "Select a material to play on this machine": "この機台で再生する素材を選択してください", + "Target Position": "配信位置", + "Select Material": "素材を選択", + "Please select a material": "素材を選択してください", + "Playback Order": "再生順序", + "Smallest number plays first.": "数字が小さいほど先に再生されます。", + "Confirm Assignment": "配信を確定", + "Are you sure you want to remove this assignment?": "この配信設定を削除してもよろしいですか?", + "image": "画像", + "video": "動画", + "Search Machine...": "機台を検索...", + "Advertisement created successfully.": "広告が正常に作成されました。", + "Advertisement updated successfully.": "広告が正常に更新されました。", + "Advertisement deleted successfully.": "広告が正常に削除されました。", + "Cannot delete advertisement being used by machines.": "機台で使用中の広告は削除できません。", + "Advertisement assigned successfully.": "広告の配信設定が完了しました。", + "Assignment removed successfully.": "配信設定が解除されました。", + "Max 5MB": "最大 5MB", + "Max 50MB": "最大 50MB", + "Select...": "選択してください...", + "Ad Settings": "広告設定", + "System Default (All Companies)": "システムデフォルト(すべての会社)", + "No materials available": "利用可能な素材がありません", + "Search...": "検索..." } \ No newline at end of file diff --git a/lang/zh_TW.json b/lang/zh_TW.json index bed85a6..90f76a8 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -828,5 +828,71 @@ "user": "一般用戶", "vs Yesterday": "較昨日", "warehouses": "倉庫管理", - "待填寫": "待填寫" + "待填寫": "待填寫", + "Advertisement List": "廣告列表", + "Machine Advertisement Settings": "機台廣告設置", + "Add Advertisement": "新增廣告", + "Edit Advertisement": "編輯廣告", + "Delete Advertisement": "刪除廣告", + "Duration": "時長", + "15 Seconds": "15 秒", + "30 Seconds": "30 秒", + "60 Seconds": "60 秒", + "Position": "投放位置", + "Standby Ad": "待機廣告", + "Assign Advertisement": "投放廣告", + "Please select a machine first": "請先選擇機台", + "Advertisement created successfully": "廣告建立成功", + "Advertisement updated successfully": "廣告更新成功", + "Advertisement deleted successfully": "廣告刪除成功", + "Advertisement assigned successfully": "廣告投放完成", + "Vending": "販賣頁", + "Visit Gift": "來店禮", + "Standby": "待機廣告", + "Advertisement Video/Image": "廣告影片/圖片", + "Sort Order": "排序", + "Date Range": "日期區間", + "Manage ad materials and machine playback settings": "管理廣告素材與機台播放設定", + "Preview": "預覽", + "No advertisements found.": "未找到廣告素材。", + "vending": "販賣頁", + "visit_gift": "來店禮", + "standby": "待機廣告", + "No assignments": "尚未投放", + "Please select a machine to view and manage its advertisements.": "請選擇一個機台以查看並管理其廣告。", + "Delete Advertisement Confirmation": "刪除廣告確認", + "Are you sure you want to delete this advertisement? This will also remove all assignments to machines.": "您確定要刪除此廣告嗎?這也將移除所有對機台的投放。", + "Manage your ad material details": "管理您的廣告素材詳情", + "Material Name": "素材名稱", + "Enter ad material name": "輸入廣告素材名稱", + "Material Type": "素材類型", + "Duration (Seconds)": "播放秒數", + "Seconds": "秒", + "Upload Image": "上傳圖片", + "Upload Video": "上傳影片", + "Save Material": "儲存素材", + "Select a material to play on this machine": "選擇要在此機台播放的素材", + "Target Position": "投放位置", + "Select Material": "選擇素材", + "Please select a material": "請選擇素材", + "Playback Order": "播放順序", + "Smallest number plays first.": "數字愈小愈先播放。", + "Confirm Assignment": "確認投放", + "Are you sure you want to remove this assignment?": "您確定要移除此投放嗎?", + "image": "圖片", + "video": "影片", + "Search Machine...": "搜尋機台...", + "Advertisement created successfully.": "廣告建立成功。", + "Advertisement updated successfully.": "廣告更新成功。", + "Advertisement deleted successfully.": "廣告刪除成功。", + "Cannot delete advertisement being used by machines.": "無法刪除正在使用的廣告素材。", + "Advertisement assigned successfully.": "廣告投放完成。", + "Assignment removed successfully.": "廣告投放已移除。", + "Max 5MB": "最大 5MB", + "Max 50MB": "最大 50MB", + "Select...": "請選擇...", + "Ad Settings": "廣告設置", + "System Default (All Companies)": "系統預設 (所有公司)", + "No materials available": "沒有可用的素材", + "Search...": "搜尋..." } \ No newline at end of file diff --git a/resources/views/admin/ads/index.blade.php b/resources/views/admin/ads/index.blade.php new file mode 100644 index 0000000..647e681 --- /dev/null +++ b/resources/views/admin/ads/index.blade.php @@ -0,0 +1,720 @@ +@extends('layouts.admin') + +@php +$routeName = request()->route()->getName(); +$baseRoute = 'admin.data-config.advertisements'; +@endphp + +@section('content') +
+ + +
+
+

{{ __('Advertisement Management') }}

+

+ {{ __('Manage ad materials and machine playback settings') }} +

+
+
+ +
+
+ + +
+ + +
+ + +
+ +
+
+ + + + + + @if(auth()->user()->isSystemAdmin()) + + @endif + + + + + + + + @forelse($advertisements as $ad) + + + + @if(auth()->user()->isSystemAdmin()) + + @endif + + + + + + @empty + + + + @endforelse + +
{{ __('Preview') }}{{ __('Name') }}{{ __('Company Name') }}{{ __('Type') }}{{ __('Duration') }}{{ __('Status') }}{{ __('Actions') }}
+
+ @if($ad->type === 'image') + + @else +
+ +
+ @endif +
+
+ {{ $ad->name }} + + {{ $ad->company->name ?? __('System Default') }} + + + {{ __($ad->type) }} + + + {{ $ad->duration }}s + + @if($ad->is_active) + {{ __('Active') }} + @else + {{ __('Disabled') }} + @endif + +
+ + +
+
{{ __('No advertisements found.') }}
+
+
+ {{ $advertisements->links('vendor.pagination.luxury') }} +
+
+ + +
+
+ +
+ + +
+ +
+ +
+ @foreach(['vending', 'visit_gift', 'standby'] as $pos) +
+
+

+ + {{ __($pos) }} +

+
+ + +
+
+ +
+ + + +
+
+ @endforeach +
+
+ +
+ {{ __('Please select a machine to view and manage its advertisements.') }} +
+
+
+
+ + + @include('admin.ads.partials.ad-modal') + @include('admin.ads.partials.assign-modal') + + +
+ +
+ +
+ + + + +
+ + +
+ + +
+

+

+
+
+
+ + +
+ +
+ +
+ + + + + +
+ + + + + + +
+
+ + +
+
+ + + + + +
+ +
+ + + + +
+
+ +
+
+ + +
+@endsection + +@section('scripts') + +@endsection diff --git a/resources/views/admin/ads/partials/ad-modal.blade.php b/resources/views/admin/ads/partials/ad-modal.blade.php new file mode 100644 index 0000000..6bcf68c --- /dev/null +++ b/resources/views/admin/ads/partials/ad-modal.blade.php @@ -0,0 +1,194 @@ +
+ +
+ +
+
+ + +
+
+

+

{{ __('Manage your ad material details') }}

+
+ +
+ +
+ @csrf + + + @if(auth()->user()->isSystemAdmin()) +
+ + +
+ @endif + +
+ + +
+ +
+
+ + +
+ + +
+ + +
+
+ + +
+ +
+
+ +
+ +

+
+ + +
+ + {{ __('Active Status') }} +
+ + +
+ + +
+
+
+
+
+ + diff --git a/resources/views/admin/ads/partials/assign-modal.blade.php b/resources/views/admin/ads/partials/assign-modal.blade.php new file mode 100644 index 0000000..1e2a8e2 --- /dev/null +++ b/resources/views/admin/ads/partials/assign-modal.blade.php @@ -0,0 +1,85 @@ +
+ +
+ +
+
+ + +
+
+

{{ __('Assign Advertisement') }}

+

{{ __('Select a material to play on this machine') }}

+
+ +
+ +
+ +
+
+

{{ __('Target Position') }}

+

+
+
+ + +
+ +
+ +
+
+ + + +
+ + +
+
+
+
+
+ + diff --git a/resources/views/components/breadcrumbs.blade.php b/resources/views/components/breadcrumbs.blade.php index 084b0f0..aef816c 100644 --- a/resources/views/components/breadcrumbs.blade.php +++ b/resources/views/components/breadcrumbs.blade.php @@ -114,6 +114,11 @@ 'analysis' => __('Analysis Management'), 'audit' => __('Audit Management'), 'maintenance' => __('Maintenance Records'), + 'data-config' => match($segments[2] ?? '') { + 'products' => __('Product Management'), + 'advertisements' => __('Advertisement Management'), + default => null, + }, default => null, }, 'edit' => str_starts_with($routeName, 'profile') ? null : __('Edit'), diff --git a/resources/views/components/searchable-select.blade.php b/resources/views/components/searchable-select.blade.php index 6de0177..36211d5 100644 --- a/resources/views/components/searchable-select.blade.php +++ b/resources/views/components/searchable-select.blade.php @@ -15,6 +15,7 @@ $isEmptySelected = (is_null($selected) || (string)$selected === '' || (string)$selected === ' '); $config = [ + "placeholder" => $placeholder ?: __('Select...'), "hasSearch" => (bool)$hasSearch, "searchPlaceholder" => $placeholder ?: __('Search...'), "isHidePlaceholder" => false, diff --git a/resources/views/layouts/partials/sidebar-menu.blade.php b/resources/views/layouts/partials/sidebar-menu.blade.php index 0c2efe8..a766bce 100644 --- a/resources/views/layouts/partials/sidebar-menu.blade.php +++ b/resources/views/layouts/partials/sidebar-menu.blade.php @@ -215,7 +215,7 @@ @endcan @can('menu.data-config.advertisements') -
  • +
  • {{ __('Advertisement Management') }}
  • diff --git a/routes/web.php b/routes/web.php index 128cf31..45f0910 100644 --- a/routes/web.php +++ b/routes/web.php @@ -114,9 +114,20 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name( // 9. 資料設定 Route::prefix('data-config')->name('data-config.')->group(function () { - Route::resource('products', App\Http\Controllers\Admin\ProductController::class)->except(['show']); - Route::patch('/products/{id}/toggle-status', [App\Http\Controllers\Admin\ProductController::class, 'toggleStatus'])->name('products.status.toggle'); - Route::get('/advertisements', [App\Http\Controllers\Admin\DataConfigController::class , 'advertisements'])->name('advertisements'); + Route::middleware('can:menu.data-config.products')->group(function () { + Route::resource('products', App\Http\Controllers\Admin\ProductController::class)->except(['show']); + Route::patch('/products/{id}/toggle-status', [App\Http\Controllers\Admin\ProductController::class, 'toggleStatus'])->name('products.status.toggle'); + }); + + // 廣告管理 (Advertisement Management) + Route::middleware('can:menu.data-config.advertisements')->group(function () { + Route::resource('advertisements', App\Http\Controllers\Admin\AdvertisementController::class)->except(['show', 'create', 'edit']); + Route::get('/advertisements/machine/{machine}', [App\Http\Controllers\Admin\AdvertisementController::class, 'getMachineAds'])->name('advertisements.machine.get'); + Route::post('/advertisements/assign', [App\Http\Controllers\Admin\AdvertisementController::class, 'assign'])->name('advertisements.assign'); + Route::post('/advertisements/assignments/reorder', [App\Http\Controllers\Admin\AdvertisementController::class, 'reorderAssignments'])->name('advertisements.assignments.reorder'); + Route::delete('/advertisements/assignment/{id}', [App\Http\Controllers\Admin\AdvertisementController::class, 'removeAssignment'])->name('advertisements.assignment.remove'); + }); + 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');