diff --git a/app/Enums/TextAdColors.php b/app/Enums/TextAdColors.php
index 3c4f2aa..4fbd805 100644
--- a/app/Enums/TextAdColors.php
+++ b/app/Enums/TextAdColors.php
@@ -8,7 +8,6 @@ enum TextAdColors: string
{
use HasLabels;
- case Black = 'black';
case White = 'white';
case Red = 'red';
case Green = 'green';
@@ -17,7 +16,6 @@ enum TextAdColors: string
public function labels(): string
{
return match ($this) {
- self::Black => '黑色',
self::White => '白色',
self::Red => '紅色',
self::Green => '綠色',
diff --git a/app/Jobs/ExportSqliteBroadcastJob.php b/app/Jobs/ExportSqliteBroadcastJob.php
new file mode 100644
index 0000000..234bc6a
--- /dev/null
+++ b/app/Jobs/ExportSqliteBroadcastJob.php
@@ -0,0 +1,67 @@
+branchId = $branchId;
+ }
+
+ public function handle()
+ {
+ $sqlitePath = storage_path('app/database/tempBroadcastTemplate.sqlite');
+
+ if (!file_exists(dirname($sqlitePath))) {
+ mkdir(dirname($sqlitePath), 0755, true);
+ }
+
+ if (!file_exists($sqlitePath)) {
+ file_put_contents($sqlitePath, '');
+ }
+ $connectionName = 'tempsqlite_' . md5($sqlitePath . microtime());
+ config(["database.connections.{$connectionName}" => [
+ 'driver' => 'sqlite',
+ 'database' => $sqlitePath,
+ 'prefix' => '',
+ ]]);
+
+ $exporter = new SqliteExportService($connectionName);
+ $exporter->exportMultiple([
+ 'broadcast_templates' => [
+ 'query' => fn () => DB::table('broadcast_templates'),
+ 'tableSchema' => function (Blueprint $table) {
+ $table->id();
+ $table->string('content')->comment('廣告公告');
+ $table->enum('color', ['white', 'red', 'green','blue'])->default('white')->comment('顯示顏色');
+ $table->boolean('is_active')->default(true); // 啟用狀態
+ $table->timestamps();
+ },
+ 'transformer' => fn ($row) => [
+ 'content' => $row->content,
+ 'color' => $row->color,
+ 'is_active' => $row->is_active,
+ 'created_at' => $row->created_at,
+ 'updated_at' => $row->updated_at,
+ ],
+ ],
+
+ ]);
+ SendSqliteFileJob::dispatch($sqlitePath, $this->branchId);
+ }
+}
diff --git a/app/Livewire/Forms/BroadcastTemplateForm.php b/app/Livewire/Forms/BroadcastTemplateForm.php
new file mode 100644
index 0000000..ebbefe0
--- /dev/null
+++ b/app/Livewire/Forms/BroadcastTemplateForm.php
@@ -0,0 +1,136 @@
+'',
+ 'color' => 'white',
+ 'is_active' => true,
+ ];
+ public function rules()
+ {
+ return [
+ 'fields.content' => 'required|string|max:255',
+ 'fields.color' => ['required',new Enum(TextAdColors::class)],
+ 'fields.is_active' => 'required|boolean',
+ ];
+ }
+
+
+ public function mount()
+ {
+ $this->colorOptions = collect(TextAdColors::cases())->map(fn ($color) => [
+ 'name' => $color->labels(),
+ 'value' => $color->value,
+ ])->toArray();
+ $this->canCreate = Auth::user()?->can('broadcast-edit') ?? false;
+ $this->canEdit = Auth::user()?->can('broadcast-edit') ?? false;
+ $this->canDelect = Auth::user()?->can('broadcast-delete') ?? false;
+ }
+
+ public function openModal($id = null)
+ {
+ $this->resetFields();
+
+ if ($id) {
+ $text = BroadcastTemplate::findOrFail($id);
+ $this->textId = $text->id;
+ $this->fields = $text->only(array_keys($this->fields));
+ }
+
+ $this->showModal = true;
+ }
+
+ public function closeModal()
+ {
+ $this->resetFields();
+ $this->showModal = false;
+ }
+
+ public function save()
+ {
+ $validated = $this->validate($this->rules());
+ if ($this->textId) {
+ if ($this->canEdit) {
+ $text = BroadcastTemplate::findOrFail($this->textId);
+ $text->update($this->fields);
+ $this->notification()->send([
+ 'icon' => 'success',
+ 'title' => '成功',
+ 'description' => '文字公告已更新',
+ ]);
+ }
+ } else {
+ if ($this->canCreate) {
+ $text = BroadcastTemplate::create($this->fields);
+ $this->notification()->send([
+ 'icon' => 'success',
+ 'title' => '成功',
+ 'description' => '文字公告已新增',
+ ]);
+ }
+ }
+ $this->resetFields();
+ $this->showModal = false;
+ $this->dispatch('pg:eventRefresh-broadcast-table');
+ }
+
+ public function deleteBroadcast($id)
+ {
+ if ($this->canDelect) {
+ BroadcastTemplate::findOrFail($id)->delete();
+ $this->notification()->send([
+ 'icon' => 'success',
+ 'title' => '成功',
+ 'description' => '文字公告已刪除',
+ ]);
+
+ $this->dispatch('pg:eventRefresh-broadcast-table');
+ }
+ }
+
+ public function resetFields()
+ {
+ foreach ($this->fields as $key => $value) {
+ if ($key == 'color') {
+ $this->fields[$key] = 'white';
+ } else if($key == 'is_active'){
+ $this->fields[$key] = true;
+ } else {
+ $this->fields[$key] = '';
+ }
+ }
+ $this->textId = null;
+ }
+
+ public function render()
+ {
+ return view('livewire.forms.broadcast-template-form');
+ }
+}
diff --git a/app/Livewire/Tables/BroadcastTemplateTable.php b/app/Livewire/Tables/BroadcastTemplateTable.php
new file mode 100644
index 0000000..46933ed
--- /dev/null
+++ b/app/Livewire/Tables/BroadcastTemplateTable.php
@@ -0,0 +1,212 @@
+ 'outside']);
+ $this->canCreate = Auth::user()?->can('broadcast-create') ?? false;
+ $this->canEdit = Auth::user()?->can('broadcast-edit') ?? false;
+ $this->canDownload=Auth::user()?->can('broadcast-delete') ?? false;
+ $this->canDelect = Auth::user()?->can('broadcast-delete') ?? false;
+ }
+
+ public function setUp(): array
+ {
+ if($this->canDownload || $this->canDelect){
+ $this->showCheckBox();
+ }
+ $actions = [];
+ if($this->canDownload){
+ $actions[]=PowerGrid::exportable(fileName: $this->tableName.'-file')
+ ->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV);
+ }
+ $header = PowerGrid::header()->showSoftDeletes()->showToggleColumns();
+ $header->includeViewOnTop('livewire.headers.broadcast-template');
+ $actions[]=$header;
+ $actions[]=PowerGrid::footer()->showPerPage()->showRecordCount();
+
+ return $actions;
+ }
+ public function header(): array
+ {
+ $actions = [];
+ if ($this->canDelect) {
+ $actions[]=Button::add('bulk-delete')
+ ->slot('Bulk delete ()')
+ ->icon('solid-trash',['id' => 'my-custom-icon-id', 'class' => 'font-bold'])
+ ->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
+ ->dispatch('bulkDelete.' . $this->tableName, []);
+ }
+ return $actions;
+ }
+
+ public function datasource(): Builder
+ {
+ return BroadcastTemplate::query();
+ }
+
+ public function relationSearch(): array
+ {
+ return [];
+ }
+
+ public function fields(): PowerGridFields
+ {
+ return PowerGrid::fields()
+ ->add('id')
+ ->add('content')
+ ->add(
+ 'content_short',
+ fn (BroadcastTemplate $model) =>
+ '' . e(Str::limit($model->content, 50)) . ''
+ )
+ ->add('color')
+ ->add('color_str', function (BroadcastTemplate $model) {
+ if ($this->canEdit) {
+ return Blade::render(
+ '',
+ [
+ 'options' => TextAdColors::options(),
+ 'modelId' => intval($model->id),
+ 'fieldName'=>'color',
+ 'selected' => $model->color->value
+ ]
+ );
+ }
+ // 沒有權限就顯示對應的文字
+
+ return $model->color->labelPowergridFilter(); // 假設 label() 會回傳顯示文字
+ } )
+ ->add('is_active')
+ ->add('created_at');
+ }
+
+ public function columns(): array
+ {
+ $column=[];
+ $column[] = Column::make(__('broadcast-templates.id'), 'id');
+ $column[] = Column::make(__('broadcast-templates.content'), 'content_short', 'broadcast-templates.content')->sortable()->searchable();
+ $column[] = Column::make(__('broadcast-templates.color'),'color_str', 'text-ads.color')->searchable();
+ $column[] = Column::make(__('broadcast-templates.is_active'), 'is_active')->toggleable(hasPermission: $this->canEdit, trueLabel: 'yes', falseLabel: 'no');
+ $column[] = Column::make(__('broadcast-templates.created_at'), 'created_at')->sortable()->searchable();
+ $column[] = Column::action(__('broadcast-templates.actions'));
+ return $column;
+ }
+
+ public function filters(): array
+ {
+ return [
+ ];
+ }
+ #[On('bulkDelete.{tableName}')]
+ public function bulkDelete(): void
+ {
+ $this->js('alert(window.pgBulkActions.get(\'' . $this->tableName . '\'))');
+ if($this->checkboxValues){
+ foreach ($this->checkboxValues as $id) {
+ $template = BroadcastTemplate::find($id);
+ if ($template) {
+ $template->delete();
+ }
+ }
+ $this->js('window.pgBulkActions.clearAll()'); // clear the count on the interface.
+ }
+ }
+ #[On('selectChanged')]
+ public function selectChanged($value,$fieldName, $modelId): void
+ {
+ //dd($value,$fieldName, $modelId);
+ if (in_array($fieldName, ['color'])) {
+ $this->noUpdated($modelId,$fieldName,$value);
+ }
+ }
+ #[On('onUpdatedEditable')]
+ public function onUpdatedEditable($id, $field, $value): void
+ {
+ if (in_array($field,[
+ ''
+ ]) && $this->canEdit) {
+ $this->noUpdated($id,$field,$value);
+ }
+ }
+ #[On('onUpdatedToggleable')]
+ public function onUpdatedToggleable($id, $field, $value): void
+ {
+ if (in_array($field,['is_active']) && $this->canEdit) {
+ $this->noUpdated($id,$field,$value);
+ }
+ }
+ private function noUpdated($id,$field,$value){
+ $template = BroadcastTemplate::find($id);
+ if ($template) {
+ $template->{$field} = $value;
+ $template->save(); // 明確觸發 saving
+ }
+ $this->notification()->send([
+ 'icon' => 'success',
+ 'title' => $id.'.'.__('broadcast.'.$field).':'.$value,
+ 'description' => '已經寫入',
+ ]);
+ }
+
+ public function actions(BroadcastTemplate $row): array
+ {
+ $actions = [];
+ if ($this->canEdit) {
+ $actions[]=Button::add('edit')
+ ->slot(__('broadcast-templates.edit'))
+ ->icon('solid-pencil-square')
+ ->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
+ ->dispatchTo('forms.broadcast-template-form', 'openModal', ['id' => $row->id]);
+ }
+ if($this->canDelect){
+ $actions[]=Button::add('delete')
+ ->slot(__('broadcast-templates.delete'))
+ ->icon('solid-trash')
+ ->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
+ ->dispatchTo('forms.broadcast-template-form', 'deleteBroadcast', ['id' => $row->id]);
+ }
+ return $actions;
+ }
+
+ /*
+ public function actionRules($row): array
+ {
+ return [
+ // Hide button edit for ID 1
+ Rule::button('edit')
+ ->when(fn($row) => $row->id === 1)
+ ->hide(),
+ ];
+ }
+ */
+}
diff --git a/app/Models/BroadcastTemplate.php b/app/Models/BroadcastTemplate.php
new file mode 100644
index 0000000..a53b78f
--- /dev/null
+++ b/app/Models/BroadcastTemplate.php
@@ -0,0 +1,18 @@
+ \App\Enums\TextAdColors::class,
+ 'is_active' => 'boolean',
+ ];
+}
diff --git a/app/Models/TextBroadcast.php b/app/Models/TextBroadcast.php
new file mode 100644
index 0000000..94da1fe
--- /dev/null
+++ b/app/Models/TextBroadcast.php
@@ -0,0 +1,20 @@
+belongsToMany(Room::class, 'text_broadcast_room');
+ }
+
+ public function template()
+ {
+ return $this->belongsTo(BroadcastTemplate::class);
+ }
+}
diff --git a/database/migrations/2025_06_26_114551_create_text_ads_table.php b/database/migrations/2025_06_26_114551_create_text_ads_table.php
index 777d8a0..7d17046 100644
--- a/database/migrations/2025_06_26_114551_create_text_ads_table.php
+++ b/database/migrations/2025_06_26_114551_create_text_ads_table.php
@@ -14,7 +14,7 @@ return new class extends Migration
Schema::create('text_ads', function (Blueprint $table) {
$table->id();
$table->string('content')->comment('廣告內容');
- $table->enum('color', ['black','white', 'red', 'green','blue'])->default('black')->comment('顯示顏色');
+ $table->enum('color', ['white', 'red', 'green','blue'])->default('white')->comment('顯示顏色');
$table->integer('duration')->default(1)->comment('播放間隔時間(分鐘)');
$table->boolean('is_active')->default(true); // 啟用狀態
$table->timestamps();
diff --git a/database/migrations/2025_08_03_162846_create_broadcast_table.php b/database/migrations/2025_08_03_162846_create_broadcast_table.php
new file mode 100644
index 0000000..8174571
--- /dev/null
+++ b/database/migrations/2025_08_03_162846_create_broadcast_table.php
@@ -0,0 +1,44 @@
+id();
+ $table->text('content')->comment('公告內容'); // e.g., "親愛的 **name**,歡迎光臨!"
+ $table->enum('color', ['white', 'red', 'green','blue'])->default('white')->comment('顯示顏色');
+ $table->boolean('is_active')->default(true); // 啟用狀態
+ $table->timestamps();
+ });
+ Schema::create('text_broadcasts', function (Blueprint $table) {
+ $table->id();
+ $table->foreignId('template_id')->nullable()->constrained('broadcast_templates')->nullOnDelete();
+ $table->text('content'); // Final content with variables replaced
+ $table->enum('target_type', ['all', 'rooms'])->default('all');
+ $table->timestamps();
+ });
+ Schema::create('text_broadcast_room', function (Blueprint $table) {
+ $table->id();
+ $table->foreignId('text_broadcast_id')->constrained()->onDelete('cascade');
+ $table->foreignId('room_id')->constrained()->onDelete('cascade');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('text_broadcast_room');
+ Schema::dropIfExists('text_broadcasts');
+ Schema::dropIfExists('broadcast_templates');
+ }
+};
diff --git a/database/seeders/BroadcastTemplateSeeder.php b/database/seeders/BroadcastTemplateSeeder.php
new file mode 100644
index 0000000..691456a
--- /dev/null
+++ b/database/seeders/BroadcastTemplateSeeder.php
@@ -0,0 +1,58 @@
+ '親愛的 **name**,祝你生日快樂!',
+ 'color' => TextAdColors::White,
+ ],
+ [
+ 'content' => '**room_name** 歡迎您蒞臨本店,祝您有美好的一天!',
+ 'color' => TextAdColors::White,
+ ],
+ [
+ 'content' => '**name** 拾獲您的證件,請至櫃台領取',
+ 'color' => TextAdColors::White,
+ ],
+ [
+ 'content' => '**name** 請移動你的愛車,謝謝!!',
+ 'color' => TextAdColors::White,
+ ],
+ [
+ 'content' => '**name** 祝你生日快樂!!',
+ 'color' => TextAdColors::White,
+ ],
+ [
+ 'content' => '**name** 櫃台有您訪客!!',
+ 'color' => TextAdColors::White,
+ ],
+ [
+ 'content' => '**room_name** 警察臨檢不便之處敬請原諒!!',
+ 'color' => TextAdColors::White,
+ ],
+ [
+ 'content' => '**name** 您的愛車被拖吊了!!',
+ 'color' => TextAdColors::White,
+ ],
+ [
+ 'content' => '**name** 請移動您的愛車,謝謝',
+ 'color' => TextAdColors::White,
+ ],
+ ];
+ foreach ($txs as $tx) {
+ BroadcastTemplate::create([
+ 'content' => $tx['content'],
+ 'color' => $tx['color'],
+ 'is_active' => true,
+ ]);
+ }
+ }
+}
diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php
index e1a0b50..a232c84 100644
--- a/database/seeders/DatabaseSeeder.php
+++ b/database/seeders/DatabaseSeeder.php
@@ -20,6 +20,7 @@ class DatabaseSeeder extends Seeder
CreateAdminUserSeeder::class,
TextAdPermissionSeeder::class,
TextAdSeeder::class,
+ BroadcastTemplateSeeder::class,
]);
}
}
diff --git a/database/seeders/TextAdPermissionSeeder.php b/database/seeders/TextAdPermissionSeeder.php
index f418ace..ada1125 100644
--- a/database/seeders/TextAdPermissionSeeder.php
+++ b/database/seeders/TextAdPermissionSeeder.php
@@ -18,6 +18,10 @@ class TextAdPermissionSeeder extends Seeder
'text-ad-create',
'text-ad-edit',
'text-ad-delete',
+ 'broadcast-list',
+ 'broadcast-create',
+ 'broadcast-edit',
+ 'broadcast-delete',
];
foreach ($permissions as $permission) {
diff --git a/resources/lang/zh-tw/broadcast-templates.php b/resources/lang/zh-tw/broadcast-templates.php
new file mode 100644
index 0000000..c7d2d68
--- /dev/null
+++ b/resources/lang/zh-tw/broadcast-templates.php
@@ -0,0 +1,27 @@
+ '文字公告管理',
+ 'list' => '文字公告列表',
+ 'CreateNew' => '新增文字公告',
+ 'EditArtist' => '編輯文字公告',
+ 'ImportData' => '滙入文字公告',
+ 'create_edit' => '新增 / 編輯',
+ 'create' => '新增',
+ 'edit' => '編輯',
+ 'delete' => '刪除',
+ 'id' => '編號',
+ 'name' => '主題',
+ 'content' => '內容',
+ 'description' => '描述',
+ 'color' => '顏色',
+ 'duration' => '播放間隔時間(分鐘)',
+ 'is_active' => '是否推播',
+ 'created_at' =>'建立於',
+
+
+ 'actions' => '操作',
+ 'view' => '查看',
+ 'submit' => '提交',
+ 'cancel' => '取消',
+];
\ No newline at end of file
diff --git a/resources/views/livewire/admin/broadcast-templates.blade.php b/resources/views/livewire/admin/broadcast-templates.blade.php
new file mode 100644
index 0000000..b4151ad
--- /dev/null
+++ b/resources/views/livewire/admin/broadcast-templates.blade.php
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/views/livewire/forms/broadcast-template-form.blade.php b/resources/views/livewire/forms/broadcast-template-form.blade.php
new file mode 100644
index 0000000..3ffb545
--- /dev/null
+++ b/resources/views/livewire/forms/broadcast-template-form.blade.php
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/views/livewire/headers/broadcast-template.blade.php b/resources/views/livewire/headers/broadcast-template.blade.php
new file mode 100644
index 0000000..a22d6cf
--- /dev/null
+++ b/resources/views/livewire/headers/broadcast-template.blade.php
@@ -0,0 +1,10 @@
+
+ @if ($canCreate)
+
+ @endif
+
\ No newline at end of file
diff --git a/resources/views/livewire/headers/broadcast.blade.php b/resources/views/livewire/headers/broadcast.blade.php
deleted file mode 100644
index 974a4d1..0000000
--- a/resources/views/livewire/headers/broadcast.blade.php
+++ /dev/null
@@ -1,10 +0,0 @@
-
- @if ($canCreate)
-
- @endif
-
\ No newline at end of file
diff --git a/resources/views/livewire/layout/admin/sidebar.blade.php b/resources/views/livewire/layout/admin/sidebar.blade.php
index 08b5f92..fe9d43e 100644
--- a/resources/views/livewire/layout/admin/sidebar.blade.php
+++ b/resources/views/livewire/layout/admin/sidebar.blade.php
@@ -17,6 +17,7 @@ new class extends Component
['label' => 'Room', 'route' => 'admin.rooms', 'icon' => 'building-library', 'permission' => 'room-list'],
['label' => 'RoomGrid', 'route' => 'admin.room-grids', 'icon' => 'film', 'permission' => 'room-list'],
['label' => 'TextAd', 'route' => 'admin.text-ads', 'icon' => 'megaphone', 'permission' => 'text-ad-list'],
+ ['label' => 'BroadcastTemplate', 'route' => 'admin.broadcast-templates', 'icon' => 'megaphone', 'permission' => 'broadcast-list'],
];
/**
diff --git a/routes/web.php b/routes/web.php
index b607e28..732cf6d 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -31,4 +31,5 @@ Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function ()
Route::get('/rooms', function () {return view('livewire.admin.rooms');})->name('rooms');
Route::get('/room-grids', function () {return view('livewire.admin.room-grids');})->name('room-grids');
Route::get('/text-ads', function () {return view('livewire.admin.text-ads');})->name('text-ads');
+ Route::get('/broadcast-templates', function () {return view('livewire.admin.broadcast-templates');})->name('broadcast-templates');
});
\ No newline at end of file