加入分店列表 20250510

This commit is contained in:
allen.yan 2025-05-10 20:08:05 +08:00
parent aeb14074d2
commit 7092566c8d
12 changed files with 574 additions and 117 deletions

View File

@ -0,0 +1,116 @@
<?php
namespace App\Livewire\Admin;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use WireUi\Traits\WireUiActions;
use App\Models\Branch;
class BranchForm extends Component
{
use WireUiActions;
protected $listeners = ['openModal','closeModal', 'deleteArtist'];
public bool $canCreate;
public bool $canEdit;
public bool $canDelect;
public bool $showModal = false;
public ?int $branchId = null;
public array $fields = [
'name' =>'',
'external_ip' =>'',
'enable' => true,
];
public function mount()
{
$this->canCreate = Auth::user()?->can('room-edit') ?? false;
$this->canEdit = Auth::user()?->can('room-edit') ?? false;
$this->canDelect = Auth::user()?->can('room-delete') ?? false;
}
public function openModal($id = null)
{
$this->resetFields();
if ($id) {
$branch = Branch::findOrFail($id);
$this->branchId = $branch->id;
$this->fields = $branch->only(array_keys($this->fields));
}
$this->showModal = true;
}
public function closeModal()
{
$this->resetFields();
$this->showModal = false;
}
public function save()
{
if ($this->branchId) {
if ($this->canEdit) {
$branch = Branch::findOrFail($this->branchId);
$branch->update($this->fields);
$this->notification()->send([
'icon' => 'success',
'title' => '成功',
'description' => '分店已更新',
]);
}
} else {
if ($this->canCreate) {
$branch = Branch::create($this->fields);
$this->notification()->send([
'icon' => 'success',
'title' => '成功',
'description' => '分店已新增',
]);
}
}
$this->resetFields();
$this->showModal = false;
$this->dispatch('pg:eventRefresh-branch-table');
}
public function deleteArtist($id)
{
if ($this->canDelect) {
Branch::findOrFail($id)->delete();
$this->notification()->send([
'icon' => 'success',
'title' => '成功',
'description' => '分店已刪除',
]);
$this->dispatch('pg:eventRefresh-branch-table');
}
}
public function resetFields()
{
foreach ($this->fields as $key => $value) {
if ($key == 'enable') {
$this->fields[$key] = true;
} else {
$this->fields[$key] = '';
}
}
$this->branchId = null;
}
public function render()
{
return view('livewire.admin.branch-form');
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace App\Livewire\Admin;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\File;
use WireUi\Traits\WireUiActions;
use Livewire\Component;
use Livewire\WithFileUploads;
use App\Jobs\ImportJob;
class BranchImportData extends Component
{
use WithFileUploads, WireUiActions;
protected $listeners = ['openModal','closeModal'];
public bool $canCreate;
public bool $showModal = false;
public $file;
public string $maxUploadSize;
public function mount()
{
$this->canCreate = Auth::user()?->can('room-edit') ?? false;
$this->maxUploadSize = $this->getMaxUploadSize();
}
public function openModal()
{
$this->showModal = true;
}
public function closeModal()
{
$this->deleteTmpFile(); // 關閉 modal 時刪除暫存檔案
$this->reset(['file']);
$this->showModal = false;
}
public function import()
{
// 檢查檔案是否有上傳
$this->validate([
'file' => 'required|file|mimes:csv,xlsx,xls'
]);
if ($this->canCreate) {
// 儲存檔案至 storage
$path = $this->file->storeAs('imports', uniqid() . '_' . $this->file->getClientOriginalName());
// 丟到 queue 執行
ImportJob::dispatch($path,'Branch');
$this->notification()->send([
'icon' => 'info',
'title' => $this->file->getClientOriginalName(),
'description' => '已排入背景匯入作業,請稍候查看結果',
]);
$this->deleteTmpFile(); // 匯入後也順便刪除 tmp 檔
$this->reset(['file']);
$this->showModal = false;
}
}
protected function deleteTmpFile()
{
$Path = $this->file->getRealPath();
if ($Path && File::exists($Path)) {
File::delete($Path);
}
}
private function getMaxUploadSize(): string
{
$uploadMax = $this->convertPHPSizeToBytes(ini_get('upload_max_filesize'));
$postMax = $this->convertPHPSizeToBytes(ini_get('post_max_size'));
$max = min($uploadMax, $postMax);
return $this->humanFileSize($max);
}
private function convertPHPSizeToBytes(string $s): int
{
$s = trim($s);
$unit = strtolower($s[strlen($s) - 1]);
$bytes = (int) $s;
switch ($unit) {
case 'g':
$bytes *= 1024;
case 'm':
$bytes *= 1024;
case 'k':
$bytes *= 1024;
}
return $bytes;
}
private function humanFileSize(int $bytes, int $decimals = 2): string
{
$sizes = ['B', 'KB', 'MB', 'GB'];
$factor = floor((strlen((string) $bytes) - 1) / 3);
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . $sizes[$factor];
}
public function render()
{
return view('livewire.admin.branch-import-data');
}
}

View File

@ -0,0 +1,213 @@
<?php
namespace App\Livewire\Admin;
use App\Models\Branch;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Blade;
use Illuminate\Database\Eloquent\Builder;
use PowerComponents\LivewirePowerGrid\Button;
use PowerComponents\LivewirePowerGrid\Column;
use PowerComponents\LivewirePowerGrid\Facades\Filter;
use PowerComponents\LivewirePowerGrid\Facades\PowerGrid;
use PowerComponents\LivewirePowerGrid\PowerGridFields;
use PowerComponents\LivewirePowerGrid\PowerGridComponent;
use PowerComponents\LivewirePowerGrid\Traits\WithExport;
use PowerComponents\LivewirePowerGrid\Components\SetUp\Exportable;
use PowerComponents\LivewirePowerGrid\Facades\Rule;
use Livewire\Attributes\On;
use WireUi\Traits\WireUiActions;
final class BranchTable extends PowerGridComponent
{
public string $tableName = 'branch-table';
public bool $canCreate;
public bool $canEdit;
public bool $canDownload;
public bool $canDelect;
public bool $showFilters = false;
public function boot(): void
{
config(['livewire-powergrid.filter' => 'outside']);
//權限設定
$this->canCreate = Auth::user()?->can('room-edit') ?? false;
$this->canEdit = Auth::user()?->can('room-edit') ?? false;
$this->canDownload=Auth::user()?->can('room-delete') ?? false;
$this->canDelect = Auth::user()?->can('room-delete') ?? false;
}
public function setUp(): array
{
if($this->canDownload || $this->canDelect){
$this->showCheckBox();
}
$actions = [];
if($this->canDownload){
$actions[]=PowerGrid::exportable(fileName: 'branch-file')
->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV);
}
$header = PowerGrid::header()
->withoutLoading()
->showToggleColumns();
//->showSoftDeletes()
//->showSearchInput()
if($this->canCreate){
$header->includeViewOnTop('livewire.admin.branch-header') ;
}
$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 (<span x-text="window.pgBulkActions.count(\'' . $this->tableName . '\')"></span>)')
->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 Branch::query();
}
public function relationSearch(): array
{
return [];
}
public function fields(): PowerGridFields
{
return PowerGrid::fields()
->add('id')
->add('name')
->add('external_ip')
->add('enable')
->add('created_at_formatted', fn (Branch $model) => Carbon::parse($model->created_at)->format('d/m/Y H:i:s'));
}
public function columns(): array
{
$column=[];
$column[]=Column::make(__('branches.no'), 'id');
$column[]=Column::make(__('branches.name'), 'name')->sortable()->searchable()
->editOnClick(
hasPermission: $this->canEdit,
dataField: 'name',
fallback: 'N/A',
saveOnMouseOut: true
);
$column[]=Column::make(__('branches.external_ip'), 'external_ip')->sortable()->searchable()
->editOnClick(
hasPermission: $this->canEdit,
dataField: 'external_ip',
fallback: 'N/A',
saveOnMouseOut: true
);
$column[]=Column::make(__('branches.enable'), 'enable')
->toggleable(
hasPermission: $this->canEdit,
trueLabel: 'yes',
falseLabel: 'no'
);
$column[]=Column::make('Created at', 'created_at_formatted', 'created_at')->sortable()->hidden(true, false);
$column[]=Column::action(__('branches.actions'));
return $column;
}
#[On('bulkDelete.{tableName}')]
public function bulkDelete(): void
{
if ($this->canDelect) {
$this->js('alert(window.pgBulkActions.get(\'' . $this->tableName . '\'))');
if($this->checkboxValues){
Branch::destroy($this->checkboxValues);
$this->js('window.pgBulkActions.clearAll()'); // clear the count on the interface.
}
}
}
#[On('categoryChanged')]
public function categoryChanged($value,$fieldName, $modelId): void
{
// dd($value,$fieldName, $modelId);
if ($fieldName == 'category' && $this->canEdit) {
$this->noUpdated($modelId,$fieldName,$value);
}
}
#[On('onUpdatedEditable')]
public function onUpdatedEditable($id, $field, $value): void
{
if ($field === 'name' && $this->canEdit) {
$this->noUpdated($id,$field,$value);
}
}
#[On('onUpdatedToggleable')]
public function onUpdatedToggleable($id, $field, $value): void
{
if (in_array($field,['enable']) && $this->canEdit) {
$this->noUpdated($id,$field,$value);
}
}
private function noUpdated($id,$field,$value){
$branch = Branch::find($id);
if ($branch) {
$branch->{$field} = $value;
$branch->save(); // 明確觸發 saving
}
$this->notification()->send([
'icon' => 'success',
'title' => $id.'.'.__('branches.'.$field).':'.$value,
'description' => '已經寫入',
]);
}
public function filters(): array
{
return [
Filter::inputText('name')->placeholder(__('branches.name')),
Filter::inputText('external_ip')->placeholder(__('branches.external_ip')),
Filter::boolean('enable')->label('✅', '❌'),
Filter::datetimepicker('created_at'),
];
}
public function actions(Branch $row): array
{
$actions = [];
if ($this->canEdit) {
$actions[] =Button::add('edit')
->slot(__('branches.edit'))
->icon('solid-pencil-square')
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
->dispatchTo('admin.branch-form', 'openModal', ['id' => $row->id]);
}
if($this->canDelect){
$actions[] =Button::add('delete')
->slot(__('branches.delete'))
->icon('solid-trash')
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
->dispatchTo('admin.branch-form', 'deleteBranch', ['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(),
];
}
*/
}

View File

@ -1,108 +0,0 @@
<?php
namespace App\Livewire\Admin;
use App\Models\Branches;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Builder;
use PowerComponents\LivewirePowerGrid\Button;
use PowerComponents\LivewirePowerGrid\Column;
use PowerComponents\LivewirePowerGrid\Facades\Filter;
use PowerComponents\LivewirePowerGrid\Facades\PowerGrid;
use PowerComponents\LivewirePowerGrid\PowerGridFields;
use PowerComponents\LivewirePowerGrid\PowerGridComponent;
final class BranchesTable extends PowerGridComponent
{
public string $tableName = 'branches-table-lcqb7y-table';
public function setUp(): array
{
$this->showCheckBox();
return [
PowerGrid::header()
->showSearchInput(),
PowerGrid::footer()
->showPerPage()
->showRecordCount(),
];
}
public function datasource(): Builder
{
return Branches::query();
}
public function relationSearch(): array
{
return [];
}
public function fields(): PowerGridFields
{
return PowerGrid::fields()
->add('id')
->add('name')
->add('external_ip')
->add('created_at');
}
public function columns(): array
{
return [
Column::make('Id', 'id'),
Column::make('Name', 'name')
->sortable()
->searchable(),
Column::make('External ip', 'external_ip')
->sortable()
->searchable(),
Column::make('Created at', 'created_at_formatted', 'created_at')
->sortable(),
Column::make('Created at', 'created_at')
->sortable()
->searchable(),
Column::action('Action')
];
}
public function filters(): array
{
return [
];
}
#[\Livewire\Attributes\On('edit')]
public function edit($rowId): void
{
$this->js('alert('.$rowId.')');
}
public function actions(Branches $row): array
{
return [
Button::add('edit')
->slot('Edit: '.$row->id)
->id()
->class('pg-btn-white dark:ring-pg-primary-600 dark:border-pg-primary-600 dark:hover:bg-pg-primary-700 dark:ring-offset-pg-primary-800 dark:text-pg-primary-300 dark:bg-pg-primary-700')
->dispatch('edit', ['rowId' => $row->id])
];
}
/*
public function actionRules($row): array
{
return [
// Hide button edit for ID 1
Rule::button('edit')
->when(fn($row) => $row->id === 1)
->hide(),
];
}
*/
}

View File

@ -13,8 +13,9 @@ return new class extends Migration
{
Schema::create('branches', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->ipAddress('external_ip'); // 對外 IP
$table->string('name')->comment('店名');
$table->ipAddress('external_ip')->comment('對外IP'); // 對外 IP
$table->tinyInteger('enable')->default(1)->comment('狀態');
$table->timestamps();
});
}

View File

@ -13,13 +13,13 @@ return new class extends Migration
{
Schema::create('rooms', function (Blueprint $table) {
$table->id();
$table->foreignId('branch_id')->constrained()->onDelete('cascade'); // 關聯分店
$table->string('name'); // 包廂名稱
$table->string('internal_ip'); // 內部 IP
$table->unsignedSmallInteger('port'); // 通訊 Port
$table->enum('status', ['active', 'closed', 'error', 'maintenance']); // 狀態:啟用中 / 已結束
$table->dateTime('started_at'); // 開始時間
$table->dateTime('ended_at')->nullable(); // 結束時間
$table->foreignId('branch_id')->constrained()->onDelete('cascade')->comment('關聯分店');
$table->string('name')->comment('包廂名稱');
$table->string('internal_ip')->comment('內部 IP');
$table->unsignedSmallInteger('port')->comment('通訊 Port');
$table->enum('status', ['active', 'closed', 'error', 'maintenance'])->comment('狀態'); // :啟用中 / 已結束
$table->dateTime('started_at')->comment('開始時間'); //
$table->dateTime('ended_at')->nullable()->comment('結束時間'); //
$table->timestamps();
});
}

View File

@ -0,0 +1,23 @@
<?php
return [
'management' => '分店管理',
'list' => '分店列表',
'CreateNew' => '新增分店',
'EditBranch' => '編輯分店',
'ImportData' => '滙入分店',
'create_edit' => '新增 / 編輯',
'create' => '新增',
'edit' => '編輯',
'delete' => '刪除',
'no' => '編號',
'name' => '名稱',
'external_ip' => '對外IP',
'enable' => '狀態',
'actions' => '操作',
'view' => '查看',
'submit' => '提交',
'cancel' => '取消',
];

View File

@ -0,0 +1,15 @@
<x-wireui:modal-card title="{{ $branchId ? __('branches.EditBranch') : __('branches.CreateNew') }}" blur wire:model.defer="showModal">
<div class="space-y-4">
<x-wireui:input label="{{__('branches.name')}}" wire:model.defer="fields.name" />
<x-wireui:input label="{{__('branches.external_ip')}}" wire:model.defer="fields.external_ip" />
<x-wireui:toggle label="{{__('branches.enable')}}" wire:model.defer="fields.enable" />
</div>
<x-slot name="footer">
<div class="flex justify-between w-full">
<x-wireui:button flat label="{{__('branches.cancel')}}" wire:click="closeModal" />
<x-wireui:button primary label="{{__('branches.submit')}}" wire:click="save" />
</div>
</x-slot>
</x-wireui:modal-card>

View File

@ -0,0 +1,15 @@
<div class="flex justify-end mb-2 mr-2 mt-2 sm:mt-0 gap-3">
<x-wireui:button
wire:click="$dispatchTo('admin.branch-form', 'openModal')"
icon="plus"
label="{{ __('branches.CreateNew') }}"
class="bg-blue-600 text-white"
/>
<x-wireui:button
wire:click="$dispatchTo('admin.branch-import-data','openModal')"
icon="document-plus"
label="{{ __('branches.ImportData') }}"
class="bg-green-600 text-white"
/>
</div>

View File

@ -0,0 +1,62 @@
<x-wireui:modal-card title="{{ __('branches.ImportData') }}" blur wire:model.defer="showModal" hide-close>
{{-- 說明區塊 --}}
<div class="mb-4 p-4 bg-gray-100 border border-gray-300 rounded text-sm text-gray-700">
<p class="font-semibold mb-2">匯入格式說明</p>
<p class="mb-2">請依下列表格格式準備 Excel CSV 檔案:</p>
<div class="overflow-x-auto mb-2">
<table class="min-w-full text-sm text-left border border-collapse border-gray-300">
<thead class="bg-gray-200">
<tr>
<th class="border border-gray-300 px-3 py-1">欄位名稱</th>
<th class="border border-gray-300 px-3 py-1">說明</th>
<th class="border border-gray-300 px-3 py-1">範例</th>
</tr>
</thead>
<tbody>
<tr>
<td class="border border-gray-300 px-3 py-1">??</td>
<td class="border border-gray-300 px-3 py-1">??</td>
<td class="border border-gray-300 px-3 py-1">??</td>
</tr>
</tbody>
</table>
</div>
</div>
{{-- 檔案上傳 --}}
<div x-data="{
fileName: '',
updateFileInfo(event) {
const file = event.target.files[0];
if (file) this.fileName = file.name+ '('+(file.size / 1024 / 1024).toFixed(2) + ' MB'+')';
}
}"
>
<div x-show="$wire.file === null" >
<input type="file" wire:model="file" accept=".csv, .xls, .xlsx" class="mb-2 w-full" @change="updateFileInfo" />
<p class="text-xs text-gray-500 mb-2" >
系統限制:最大上傳 {{ $maxUploadSize }}
</p>
</div>
<!-- 檔案資訊顯示 -->
<div wire:loading.remove wire:target="file" class="text-sm text-green-600 flex items-center space-x-1" x-show="$wire.file != null">
<x-wireui:icon name="check-circle" class="w-5 h-5 text-green-500" />
<strong x-text="fileName"></strong>
</div>
<!-- 上傳中提示 -->
<div wire:loading wire:target="file" class="text-sm text-blue-500">
檔案上傳中,請稍候...
</div>
</div>
<x-slot name="footer">
<div class="flex justify-between w-full">
<x-wireui:button flat label="{{ __('branches.cancel') }}" wire:click="closeModal" />
<x-wireui:button primary label="{{ __('branches.submit') }}" wire:click="import" />
</div>
</x-slot>
</x-wireui:modal-card>

View File

@ -0,0 +1,7 @@
<x-layouts.admin>
<x-wireui:notifications/>
<livewire:admin.branch-table />
<livewire:admin.branch-form />
<livewire:admin.branch-import-data />
</x-layouts.admin>

View File

@ -7,6 +7,7 @@
['label' => 'User', 'route' => 'admin.users', 'icon' => 'user-circle', 'permission' => 'user-list'],
['label' => 'Artist', 'route' => 'admin.artists', 'icon' => 'musical-note', 'permission' => 'song-list'],
['label' => 'Song', 'route' => 'admin.songs', 'icon' => 'musical-note', 'permission' => 'song-list'],
['label' => 'Branche', 'route' => 'admin.branches', 'icon' => 'building-library', 'permission' => 'room-list'],
];
@endphp