diff --git a/.DS_Store b/.DS_Store index 1996be4..ce09d97 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/app/Enums/Traits/HasLabels.php b/app/Enums/Traits/HasLabels.php new file mode 100644 index 0000000..ca0662a --- /dev/null +++ b/app/Enums/Traits/HasLabels.php @@ -0,0 +1,20 @@ +mapWithKeys(function (self $case) { + return [$case->value => $case->labels()]; + }); + } + + public function labelPowergridFilter(): string + { + return $this->labels(); + } +} \ No newline at end of file diff --git a/app/Enums/UserGender.php b/app/Enums/UserGender.php index 1f3e12f..07e5058 100644 --- a/app/Enums/UserGender.php +++ b/app/Enums/UserGender.php @@ -2,8 +2,12 @@ namespace App\Enums; +use App\Enums\Traits\HasLabels; + enum UserGender: string { + use HasLabels; + case Male = 'male'; case Female = 'female'; case Other = 'other'; @@ -19,9 +23,4 @@ enum UserGender: string self::Unset => __('enums.user.gender.Unset'), }; } - - public function labelPowergridFilter(): String - { - return $this -> labels(); - } } \ No newline at end of file diff --git a/app/Enums/UserStatus.php b/app/Enums/UserStatus.php index 6f147cc..36bea9f 100644 --- a/app/Enums/UserStatus.php +++ b/app/Enums/UserStatus.php @@ -2,8 +2,12 @@ namespace App\Enums; +use App\Enums\Traits\HasLabels; + enum UserStatus: int { + use HasLabels; + case Active = 0; // 正常 case Suspended = 1; // 停權 case Deleting = 2; // 刪除中 @@ -17,9 +21,4 @@ enum UserStatus: int self::Deleting => __('enums.user.status.Deleting'), }; } - public function labelPowergridFilter(): String - { - return $this -> labels(); - } - } \ No newline at end of file diff --git a/app/Imports/DataImport.php b/app/Imports/DataImport.php new file mode 100644 index 0000000..369dcb3 --- /dev/null +++ b/app/Imports/DataImport.php @@ -0,0 +1,45 @@ +modelName= $modelName; + } + public function collection(Collection $rows) + { + + Log::warning('匯入啟動', [ + 'model' => $this->modelName, + 'rows_id' =>++$this->con, + 'rows_count' => $rows->count() + ]); + if($this->modelName=='User'){ + ImportUserChunkJob::dispatch($rows,$this->con); + }else{ + Log::warning('未知的 modelName', ['model' => $this->modelName]); + } + } + public function chunkSize(): int + { + return 1000; + } + + public function headingRow(): int + { + return 1; + } +} \ No newline at end of file diff --git a/app/Jobs/ImportJob.php b/app/Jobs/ImportJob.php new file mode 100644 index 0000000..8264354 --- /dev/null +++ b/app/Jobs/ImportJob.php @@ -0,0 +1,59 @@ +filePath = $filePath; + $this->modelName= $modelName; + } + + /** + * Execute the job. + */ + public function handle(): void + { + ini_set('memory_limit', '512M'); // ✅ 增加記憶體限制 + Log::info('[ImportJob] 開始處理檔案:' . $this->filePath); + + try { + if (!Storage::exists($this->filePath)) { + Log::warning('[ImportJob] 檔案不存在:' . $this->filePath); + return; + } + + Excel::import(new DataImport($this->modelName), $this->filePath); + Log::info('[ImportJob] 已提交所有 chunk 匯入任務。'); + + Storage::delete($this->filePath); + Log::info('[ImportJob] 已刪除檔案:' . $this->filePath); + } catch (\Throwable $e) { + Log::error("[ImportJob] 匯入失敗:{$e->getMessage()}", [ + 'file' => $this->filePath, + 'trace' => $e->getTraceAsString(), + ]); + } + } +} \ No newline at end of file diff --git a/app/Jobs/ImportUserChunkJob.php b/app/Jobs/ImportUserChunkJob.php new file mode 100644 index 0000000..d96461f --- /dev/null +++ b/app/Jobs/ImportUserChunkJob.php @@ -0,0 +1,67 @@ +rows = $rows; + $this->id = $id; + } + + public function handle(): void + { + Log::warning('匯入啟動', [ + 'model' => "ImportUserChunkJob", + 'rows_id' =>$this->id, + ]); + $now = now(); + foreach ($this->rows as $index => $row) { + try { + $name = $this->normalizeName($row['歌手姓名'] ?? ''); + + if (empty($name) || User::where('name', $name)->exists()) { + continue; + } + // 準備 song 資料 + $toInsert[] = [ + 'name' => $name, + 'category' => ArtistCategory::tryFrom(trim($row['歌手分類'] ?? '未定義')) ?? ArtistCategory::Unset, + 'simplified' => $simplified, + 'phonetic_abbr' => $phoneticAbbr, + 'pinyin_abbr' => $pinyinAbbr, + 'strokes_abbr' => $strokesAbbr, + 'enable' =>trim($row['狀態'] ?? 1), + 'created_at' => $now, + 'updated_at' => $now, + ]; + } catch (\Throwable $e) { + \Log::error("Row {$index} failed: {$e->getMessage()}", [ + 'row' => $row, + 'trace' => $e->getTraceAsString() + ]); + } + } + User::insert($toInsert); + } + + public function normalizeName(?string $str): string + { + return strtoupper(mb_convert_kana(trim($str ?? ''), 'as')); + } +} \ No newline at end of file diff --git a/app/Livewire/Admin/ActivityLogTable.php b/app/Livewire/Admin/ActivityLogTable.php new file mode 100644 index 0000000..ad22c8b --- /dev/null +++ b/app/Livewire/Admin/ActivityLogTable.php @@ -0,0 +1,118 @@ + 'outside']); + //權限設定 + $this->canDownload=true; + } + + public function setUp(): array + { + if($this->canDownload ){ + $this->showCheckBox(); + } + $actions = []; + if($this->canDownload){ + $actions[]=PowerGrid::exportable(fileName: $this->tableName.'-file') + ->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV); + } + $header = PowerGrid::header() + ->withoutLoading() + ->showToggleColumns(); + //->showSoftDeletes() + //->showSearchInput() + $actions[]=$header; + $actions[]=PowerGrid::footer()->showPerPage()->showRecordCount(); + return $actions; + } + + public function datasource(): Builder + { + return Activity::with(['causer'])->latest(); + } + + public function relationSearch(): array + { + return []; + } + + public function fields(): PowerGridFields + { + return PowerGrid::fields() + ->add('id') + ->add('created_at_formatted', fn (Activity $model) => Carbon::parse($model->created_at)->format('Y-m-d H:i:s')) + ->add('causer_name', fn (Activity $model) => optional($model->causer)->name) + ->add('subject_type_label', fn (Activity $model) => class_basename($model->subject_type)) + ->add('subject_type') + ->add('subject_id') + ->add('description') + ->add('changes_str', function (Activity $model) { + $old = $model->properties['old'] ?? []; + $new = $model->properties['attributes'] ?? []; + + $changes = []; + + foreach ($new as $key => $newValue) { + if (in_array($key, ['updated_at', 'created_at'])) continue; + $oldValue = $old[$key] ?? '(空)'; + if ($newValue != $oldValue) { + $changes[] = "{$key}: {$oldValue} → {$newValue}"; + } + } + //dd(implode('
', $changes)); + return implode('
', $changes); + }) + ; + + } + + public function columns(): array + { + $column=[]; + $column[]=Column::make('時間', 'created_at_formatted', 'created_at')->sortable()->searchable(); + $column[]=Column::make('操作者', 'causer_name')->sortable()->searchable()->bodyAttribute('whitespace-nowrap'); + $column[]=Column::make('模型', 'subject_type_label')->sortable()->searchable(); + $column[]=Column::make('模型 ID', 'subject_id')->sortable()->searchable(); + $column[]=Column::make('動作', 'description')->sortable()->searchable(); + $column[]=Column::make('變更內容', 'changes_str')->sortable(false)->searchable(false)->bodyAttribute('whitespace-normal text-sm text-gray-700'); + return $column; + } + + public function filters(): array + { + return [ + Filter::datetimepicker('created_at'), + Filter::inputText('causer_name')->placeholder('操作者'), + Filter::inputText('subject_type_label')->placeholder('模型'), + Filter::number('subject_id'), + Filter::inputText('description')->placeholder('動作'), + ]; + } + + + + +} diff --git a/app/Livewire/Admin/RoleForm.php b/app/Livewire/Admin/RoleForm.php index 482af39..d954564 100644 --- a/app/Livewire/Admin/RoleForm.php +++ b/app/Livewire/Admin/RoleForm.php @@ -2,15 +2,25 @@ namespace App\Livewire\Admin; +use Illuminate\Validation\Rule; +use Illuminate\Support\Facades\Auth; + use Livewire\Component; +use WireUi\Traits\WireUiActions; use Spatie\Permission\Models\Role; use Spatie\Permission\Models\Permission; class RoleForm extends Component { + use WireUiActions; + protected $listeners = ['openCreateRoleModal','openEditRoleModal', 'deleteRole']; + public bool $canCreate; + public bool $canEdit; + public bool $canDelect; + public $showCreateModal=false; public ?int $roleId = null; public $name = ''; @@ -22,6 +32,9 @@ class RoleForm extends Component public function mount() { $this->permissions = Permission::all(); + $this->canCreate = Auth::user()?->can('role-edit') ?? false; + $this->canEdit = Auth::user()?->can('role-edit') ?? false; + $this->canDelect = Auth::user()?->can('role-delete') ?? false; } public function openCreateRoleModal() @@ -47,24 +60,44 @@ class RoleForm extends Component ]); if ($this->roleId) { - $role = Role::findOrFail($this->roleId); - $role->update(['name' => $this->name]); - $role->syncPermissions($this->selectedPermissions); - session()->flash('message', '角色已更新'); + if ($this->canEdit) { + $role = Role::findOrFail($this->roleId); + $role->update(['name' => $this->name]); + $role->syncPermissions($this->selectedPermissions); + $this->notification()->send([ + 'icon' => 'success', + 'title' => '成功', + 'description' => '角色已更新', + ]); + } } else { - $role = Role::create(['name' => $this->name]); - $role->syncPermissions($this->selectedPermissions); - session()->flash('message', '角色已新增'); + if ($this->canCreate) { + $role = Role::create(['name' => $this->name]); + $role->syncPermissions($this->selectedPermissions); + $this->notification()->send([ + 'icon' => 'success', + 'title' => '成功', + 'description' => '角色已新增', + ]); + } } $this->resetFields(); $this->showCreateModal = false; + $this->dispatch('pg:eventRefresh-role-table'); } public function deleteRole($id) { - Role::findOrFail($id)->delete(); - session()->flash('message', '角色已刪除'); + if ($this->canDelect) { + Role::findOrFail($id)->delete(); + $this->notification()->send([ + 'icon' => 'success', + 'title' => '成功', + 'description' => '角色已刪除', + ]); + $this->dispatch('pg:eventRefresh-role-table'); + } } public function resetFields() diff --git a/app/Livewire/Admin/RoleTable.php b/app/Livewire/Admin/RoleTable.php index 35aab28..b575225 100644 --- a/app/Livewire/Admin/RoleTable.php +++ b/app/Livewire/Admin/RoleTable.php @@ -3,7 +3,9 @@ namespace App\Livewire\Admin; use Spatie\Permission\Models\Role; +use Spatie\Permission\Models\Permission; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Auth; use Illuminate\Database\Eloquent\Builder; use PowerComponents\LivewirePowerGrid\Button; use PowerComponents\LivewirePowerGrid\Column; @@ -11,28 +13,55 @@ use PowerComponents\LivewirePowerGrid\Facades\Filter; use PowerComponents\LivewirePowerGrid\Facades\PowerGrid; use PowerComponents\LivewirePowerGrid\PowerGridFields; use PowerComponents\LivewirePowerGrid\PowerGridComponent; +use Livewire\Attributes\On; +use WireUi\Traits\WireUiActions; final class RoleTable extends PowerGridComponent { + use WireUiActions; + public string $tableName = 'role-table'; + public bool $canCreate; + public bool $canEdit; + public bool $canDelect; public function boot(): void { config(['livewire-powergrid.filter' => 'outside']); + //權限設定 + $this->canCreate = Auth::user()?->can('role-edit') ?? false; + $this->canEdit = Auth::user()?->can('role-edit') ?? false; + $this->canDelect = Auth::user()?->can('role-delete') ?? false; } public function setUp(): array { - //$this->showCheckBox(); - - return [ - //PowerGrid::header() - // ->showSearchInput(), - //PowerGrid::footer() - // ->showPerPage() - // ->showRecordCount(), - ]; + if($this->canDelect){ + $this->showCheckBox(); + } + $actions = []; + $header =PowerGrid::header(); + if($this->canCreate){ + $header->includeViewOnTop('livewire.admin.role-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 ()') + ->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 { @@ -47,27 +76,72 @@ final class RoleTable extends PowerGridComponent public function fields(): PowerGridFields { + $allPermissions = Permission::pluck('name')->sort()->values(); return PowerGrid::fields() ->add('id') ->add('name') - ->add('permissions_list' ,fn(Role $model)=> $model->permissions->pluck('name')->implode(', ')) - ->add('created_at_formatted', fn (Role $model) => Carbon::parse($model->created_at)->format('d/m/Y H:i:s')); + ->add('permissions_list', function (Role $model) use ($allPermissions) { + $rolePermissions = $model->permissions->pluck('name')->sort()->values(); + + if ($rolePermissions->count() === $allPermissions->count() && $rolePermissions->values()->all() === $allPermissions->values()->all()) { + return 'all'; + } + + return $rolePermissions->implode(', '); + }) + ->add('created_at_formatted', fn (Role $model) => Carbon::parse($model->created_at)->format('Y-m-d H:i:s')); } public function columns(): array { - return [ - Column::make(__('roles.no'), 'id')->sortable()->searchable(), - Column::make(__('roles.name'), 'name')->sortable()->searchable(), - Column::make(__('roles.permissions'), 'permissions_list'), - Column::make('Created at', 'created_at_formatted', 'created_at')->sortable(), - Column::action('Action') - ]; + $column=[]; + $column[]=Column::make(__('roles.no'), 'id')->sortable()->searchable(); + $column[]=Column::make(__('roles.name'), 'name')->sortable()->searchable() + ->editOnClick( + hasPermission: $this->canEdit, + dataField: 'name', + fallback: 'N/A', + saveOnMouseOut: true + ); + $column[]=Column::make(__('roles.permissions'), 'permissions_list'); + $column[]=Column::make('Created at', 'created_at_formatted', 'created_at')->sortable(); + $column[]=Column::action('Action'); + return $column; + } + #[On('bulkDelete.{tableName}')] + public function bulkDelete(): void + { + if ($this->canDelect) { + $this->js('alert(window.pgBulkActions.get(\'' . $this->tableName . '\'))'); + if($this->checkboxValues){ + Role::destroy($this->checkboxValues); + $this->js('window.pgBulkActions.clearAll()'); // clear the count on the interface. + } + } + } + #[On('onUpdatedEditable')] + public function onUpdatedEditable($id, $field, $value): void + { + if ($field === 'name' && $this->canEdit) { + $this->noUpdated($id,$field,$value); + } + } + private function noUpdated($id,$field,$value){ + $role = Role::find($id); + if ($role) { + $role->{$field} = $value; + $role->save(); // 明確觸發 saving + } + $this->notification()->send([ + 'icon' => 'success', + 'title' => $id.'.'.__('roles.'.$field).':'.$value, + 'description' => '已經寫入', + ]); } - public function filters(): array { return [ + Filter::inputText('name')->placeholder(__('roles.name')), Filter::datetimepicker('created_at'), ]; } @@ -75,18 +149,22 @@ final class RoleTable extends PowerGridComponent public function actions(Role $row): array { - return [ - Button::add('edit') + $actions = []; + if ($this->canEdit) { + $actions[] =Button::add('edit') ->slot(__('roles.edit')) ->icon('solid-pencil-square') ->class('inline-flex items-center gap-1 px-3 py-1 rounded ') - ->dispatchTo('admin.role-form', 'openEditRoleModal', ['id' => $row->id]), - Button::add('delete') - ->slot(__('delete')) + ->dispatchTo('admin.role-form', 'openEditRoleModal', ['id' => $row->id]); + } + if($this->canDelect){ + $actions[] =Button::add('delete') + ->slot(__('roles.delete')) ->icon('solid-trash') ->class('inline-flex items-center gap-1 px-3 py-1 rounded ') - ->dispatchTo('admin.role-form', 'deleteRole', ['id' => $row->id]), - ]; + ->dispatchTo('admin.role-form', 'deleteRole', ['id' => $row->id]); + } + return $actions; } /* diff --git a/app/Livewire/Admin/UserForm.php b/app/Livewire/Admin/UserForm.php index 71037c1..8094c9e 100644 --- a/app/Livewire/Admin/UserForm.php +++ b/app/Livewire/Admin/UserForm.php @@ -2,7 +2,11 @@ namespace App\Livewire\Admin; +use Illuminate\Validation\Rule; +use Illuminate\Support\Facades\Auth; + use Livewire\Component; +use WireUi\Traits\WireUiActions; use App\Models\User; use App\Enums\UserGender; @@ -11,17 +15,21 @@ use Spatie\Permission\Models\Role; class UserForm extends Component { - protected $listeners = ['openCreateUserModal','openEditUserModal', 'deleteUser','bulkDeleteUser']; + use WireUiActions; + + protected $listeners = ['openModal','closeModal', 'deleteUser']; + + public bool $canCreate; + public bool $canEdit; + public bool $canDelect; + + public bool $showModal = false; - public bool $showCreateModal = false; - public array $genderOptions =[]; public array $statusOptions =[]; public $rolesOptions = []; // 所有角色清單 public $selectedRoles = []; // 表單中選到的權限 - public ?int $userId = null; - public array $fields = [ 'name' =>'', 'email' => '', @@ -30,6 +38,7 @@ class UserForm extends Component 'gender' => 'unset', 'status' => 0, ]; + protected $rules = [ 'fields.name' => 'required|string|max:255', @@ -52,48 +61,72 @@ class UserForm extends Component 'value' => $status->value, ])->toArray(); $this->rolesOptions = Role::all(); + $this->canCreate = Auth::user()?->can('user-edit') ?? false; + $this->canEdit = Auth::user()?->can('user-edit') ?? false; + $this->canDelect = Auth::user()?->can('user-delete') ?? false; } - public function openCreateUserModal() + public function openModal($id = null) { $this->resetFields(); - $this->showCreateModal = true; + if($id){ + $obj = User::findOrFail($id); + $this->userId = $obj->id; + $this->fields = $obj->only(array_keys($this->fields)); + $this->selectedRoles = $obj->roles()->pluck('id')->toArray(); + } + $this->showModal = true; } - - public function openEditUserModal($id) + + public function closeModal() { - $user = User::findOrFail($id); - $this->userId = $user->id; - $this->fields = $user->only(array_keys($this->fields)); - - $this->selectedRoles = $user->roles()->pluck('id')->toArray(); - $this->showCreateModal = true; + $this->resetFields(); + $this->showModal = false; } public function save() { - $this->validate(); + //$this->validate(); if ($this->userId) { - $user = User::findOrFail($this->userId); - $user->update($this->fields); - $user->syncRoles($this->selectedRoles); - session()->flash('message', '使用者已更新'); + if ($this->canEdit) { + $obj = User::findOrFail($this->userId); + $obj->update($this->fields); + $obj->syncRoles($this->selectedRoles); + $this->notification()->send([ + 'icon' => 'success', + 'title' => '成功', + 'description' => '使用者已更新', + ]); + } } else { - $user = User::create($this->fields); - $user->syncRoles($this->selectedRoles); - session()->flash('message', '使用者已新增'); + if ($this->canCreate) { + $obj = User::create($this->fields); + $obj->syncRoles($this->selectedRoles); + $this->notification()->send([ + 'icon' => 'success', + 'title' => '成功', + 'description' => '使用者已新增', + ]); + } } $this->resetFields(); - $this->showCreateModal = false; + $this->showModal = false; $this->dispatch('pg:eventRefresh-user-table'); } public function deleteUser($id) { - User::findOrFail($id)->delete(); - session()->flash('message', '使用者已刪除'); + if ($this->canDelect) { + User::findOrFail($id)->delete(); + $this->notification()->send([ + 'icon' => 'success', + 'title' => '成功', + 'description' => '使用者已刪除', + ]); + $this->dispatch('pg:eventRefresh-user-table'); + } } public function resetFields() diff --git a/app/Livewire/Admin/UserImportData.php b/app/Livewire/Admin/UserImportData.php new file mode 100644 index 0000000..c3d3997 --- /dev/null +++ b/app/Livewire/Admin/UserImportData.php @@ -0,0 +1,114 @@ +canCreate = Auth::user()?->can('user-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,'User'); + + $this->notification()->send([ + 'icon' => 'info', + 'title' => $this->file->getClientOriginalName(), + 'description' => '已排入背景匯入作業,請稍候查看結果', + ]); + $this->deleteTmpFile(); // 匯入後也順便刪除 tmp 檔 + $this->reset(['file']); + $this->showModal = false; + } + } + protected function deleteTmpFile() + { + if($this->file!=null){ + $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.user-import-data'); + } + +} \ No newline at end of file diff --git a/app/Livewire/Admin/UserTable.php b/app/Livewire/Admin/UserTable.php index cc65c2e..8d86f8d 100644 --- a/app/Livewire/Admin/UserTable.php +++ b/app/Livewire/Admin/UserTable.php @@ -6,6 +6,8 @@ use App\Models\User; use App\Enums\UserGender; use App\Enums\UserStatus; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Blade; use Illuminate\Database\Eloquent\Builder; use PowerComponents\LivewirePowerGrid\Button; use PowerComponents\LivewirePowerGrid\Column; @@ -13,48 +15,63 @@ 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\Traits\WithExport; use PowerComponents\LivewirePowerGrid\Components\SetUp\Exportable; use PowerComponents\LivewirePowerGrid\Facades\Rule; use Livewire\Attributes\On; +use WireUi\Traits\WireUiActions; final class UserTable extends PowerGridComponent { - //use WithExport ; + use WithExport, WireUiActions; public string $tableName = 'user-table'; public bool $showFilters = false; + public bool $canCreate; + public bool $canEdit; + public bool $canDownload; + public bool $canDelect; public function boot(): void { config(['livewire-powergrid.filter' => 'outside']); + //權限設定 + $this->canCreate = Auth::user()?->can('user-edit') ?? false; + $this->canEdit = Auth::user()?->can('user-edit') ?? false; + $this->canDownload=Auth::user()?->can('user-delete') ?? false; + $this->canDelect = Auth::user()?->can('user-delete') ?? false; } public function setUp(): array { - $this->showCheckBox(); - - return [ - PowerGrid::exportable(fileName: 'my-export-file') - ->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV), - PowerGrid::header() - //->showSoftDeletes() - ->showToggleColumns() - ->showSearchInput(), - PowerGrid::footer()->showPerPage()->showRecordCount(), - ]; + if($this->canDownload || $this->canDelect){ + $this->showCheckBox(); + } + $actions = []; + $actions[] =PowerGrid::exportable(fileName: $this->tableName.'-file') + ->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV); + $header = PowerGrid::header() + ->showToggleColumns(); + if($this->canCreate){ + $header->includeViewOnTop('livewire.admin.user-header'); + } + $actions[]=$header; + $actions[]=PowerGrid::footer()->showPerPage()->showRecordCount(); + return $actions; } public function header(): array { - return [ - Button::add('bulk-delete') + $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, []), - ]; + ->dispatch('bulkDelete.' . $this->tableName, []); + } + return $actions; } public function datasource(): Builder @@ -75,8 +92,38 @@ final class UserTable extends PowerGridComponent ->add('email') ->add('phone') ->add('birthday_formatted',fn (User $model) => Carbon::parse($model->birthday)->format('Y-m-d')) - ->add('gender', fn (User $model) => UserGender::from($model->gender)->labels()) - ->add('status', fn (User $model) => UserStatus::from($model->status)->labels()) + ->add('gender_str', function (User $model) { + if ($this->canEdit) { + return Blade::render( + '', + [ + 'options' => UserGender::options(), + 'modelId' => intval($model->id), + 'fieldName'=>'gender', + 'selected' => $model->gender->value + ] + ); + } + // 沒有權限就顯示對應的文字 + + return $model->gender->labelPowergridFilter(); // 假設 label() 會回傳顯示文字 + } ) + ->add('status_str', function (User $model) { + if ($this->canEdit) { + return Blade::render( + '', + [ + 'options' => UserStatus::options(), + 'modelId' => intval($model->id), + 'fieldName'=>'status', + 'selected' => $model->status->value + ] + ); + } + // 沒有權限就顯示對應的文字 + + return $model->status->labelPowergridFilter(); // 假設 label() 會回傳顯示文字 + } ) ->add('roles' ,fn(User $model)=> $model->roles->pluck('name')->implode(', ')) ->add('created_at_formatted', fn (User $model) => Carbon::parse($model->created_at)->format('Y-m-d H:i:s')); } @@ -89,7 +136,7 @@ final class UserTable extends PowerGridComponent ->sortable() ->searchable() ->editOnClick( - hasPermission: true, + hasPermission: $this->canEdit, dataField: 'name', fallback: 'N/A', saveOnMouseOut: true @@ -98,7 +145,7 @@ final class UserTable extends PowerGridComponent ->sortable() ->searchable() ->editOnClick( - hasPermission: true, + hasPermission: $this->canEdit, dataField: 'email', fallback: 'N/A', saveOnMouseOut: true @@ -107,20 +154,65 @@ final class UserTable extends PowerGridComponent ->sortable() ->searchable() ->editOnClick( - hasPermission: true, + hasPermission: $this->canEdit, dataField: 'phone', fallback: 'N/A', saveOnMouseOut: true ), - Column::make(__('users.gender'), 'gender','users.gender'), + Column::make(__('users.gender'), 'gender_str','users.gender'), Column::make(__('users.birthday'), 'birthday_formatted')->sortable()->searchable(), - Column::make(__('users.status'), 'status','users.status'), + Column::make(__('users.status'), 'status_str','users.status'), Column::make(__('users.role'), 'roles'), Column::make('建立時間', 'created_at_formatted', 'created_at')->sortable(), Column::action('操作') ]; } + #[On('bulkDelete.{tableName}')] + public function bulkDelete(): void + { + if ($this->canDelect) { + $this->js('alert(window.pgBulkActions.get(\'' . $this->tableName . '\'))'); + if($this->checkboxValues){ + User::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 (in_array($fieldName,['gender','status']) && $this->canEdit) { + $this->noUpdated($modelId,$fieldName,$value); + } + } + #[On('onUpdatedEditable')] + public function onUpdatedEditable($id, $field, $value): void + { + if (in_array($field,['name','email','phone']) && $this->canEdit) { + $this->noUpdated($id,$field,$value); + } + } + #[On('onUpdatedToggleable')] + public function onUpdatedToggleable($id, $field, $value): void + { + if (in_array($field,[]) && $this->canEdit) { + $this->noUpdated($id,$field,$value); + } + } + private function noUpdated($id,$field,$value){ + $user = User::find($id); + if ($user) { + $user->{$field} = $value; + $user->save(); // 明確觸發 saving + } + $this->notification()->send([ + 'icon' => 'success', + 'title' => $id.'.'.__('users.'.$field).':'.$value, + 'description' => '已經寫入', + ]); + } public function filters(): array { @@ -128,11 +220,11 @@ final class UserTable extends PowerGridComponent Filter::inputText('name')->placeholder(__('users.name')), Filter::inputText('email')->placeholder('Email'), Filter::inputText('phone')->placeholder(__('users.phone')), - Filter::enumSelect('gender','users.gender') + Filter::enumSelect('gender_str','users.gender') ->datasource(UserGender::cases()) ->optionLabel('users.gender'), Filter::datepicker('birthday'), - Filter::enumSelect('status', 'users.status') + Filter::enumSelect('status_str', 'users.status') ->datasource(UserStatus::cases()) ->optionLabel('users.status'), Filter::datetimepicker('created_at'), @@ -141,47 +233,22 @@ final class UserTable extends PowerGridComponent public function actions(User $row): array { - return [ - - Button::add('edit') + $actions = []; + if ($this->canEdit) { + $actions[]=Button::add('edit') ->slot(__('users.edit')) ->icon('solid-pencil-square') ->class('inline-flex items-center gap-1 px-3 py-1 rounded ') - ->dispatchTo('admin.user-form', 'openEditUserModal', ['id' => $row->id]), - Button::add('delete') + ->dispatchTo('admin.user-form', 'openModal', ['id' => $row->id]); + } + if($this->canDelect){ + $actions[]=Button::add('delete') ->slot(__('users.delete')) ->icon('solid-trash') ->class('inline-flex items-center gap-1 px-3 py-1 rounded ') - ->dispatchTo('admin.user-form', 'deleteUser', ['id' => $row->id]), - ]; - } - - public function onUpdatedEditable($id, $field, $value): void - { - $updated = User::query()->where('id', $id)->update([ - $field => $value, - ]); - if ($updated) { - $this->fillData(); - } - } - public function onUpdatedToggleable($id, $field, $value): void - { - $updated = User::query()->where('id', $id)->update([ - $field => $value, - ]); - if ($updated) { - $this->fillData(); - } - } - #[On('bulkDelete.{tableName}')] - public function bulkDelete(): void - { - $this->js('alert(window.pgBulkActions.get(\'' . $this->tableName . '\'))'); - if($this->checkboxValues){ - User::destroy($this->checkboxValues); - $this->js('window.pgBulkActions.clearAll()'); // clear the count on the interface. + ->dispatchTo('admin.user-form', 'deleteUser', ['id' => $row->id]); } + return $actions; } diff --git a/app/Models/User.php b/app/Models/User.php index b799f3a..3346083 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -7,11 +7,13 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Spatie\Permission\Traits\HasRoles; +use App\Traits\LogsModelActivity; +use Spatie\Activitylog\Traits\CausesActivity; class User extends Authenticatable { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable, HasRoles; + use HasFactory, Notifiable, HasRoles, LogsModelActivity,CausesActivity; /** * The attributes that are mass assignable. @@ -48,7 +50,9 @@ class User extends Authenticatable return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', - 'birthday' => 'date' + 'birthday' => 'date', + 'gender' => \App\Enums\UserGender::class, + 'status' => \App\Enums\UserStatus::class, ]; } } diff --git a/app/Traits/LogsModelActivity.php b/app/Traits/LogsModelActivity.php new file mode 100644 index 0000000..7c68b62 --- /dev/null +++ b/app/Traits/LogsModelActivity.php @@ -0,0 +1,23 @@ +useLogName(strtolower(class_basename(static::class))) + ->logOnly($this->getFillable()) + ->logOnlyDirty() + ->dontSubmitEmptyLogs() + ->setDescriptionForEvent(function (string $eventName) { + return class_basename(static::class) . " 已 {$eventName}"; + }); + } +} \ No newline at end of file diff --git a/app/View/Components/SelectCategory.php b/app/View/Components/SelectCategory.php new file mode 100644 index 0000000..b2837aa --- /dev/null +++ b/app/View/Components/SelectCategory.php @@ -0,0 +1,27 @@ + env('ACTIVITY_LOGGER_ENABLED', true), + + /* + * When the clean-command is executed, all recording activities older than + * the number of days specified here will be deleted. + */ + 'delete_records_older_than_days' => 365, + + /* + * If no log name is passed to the activity() helper + * we use this default log name. + */ + 'default_log_name' => 'default', + + /* + * You can specify an auth driver here that gets user models. + * If this is null we'll use the current Laravel auth driver. + */ + 'default_auth_driver' => null, + + /* + * If set to true, the subject returns soft deleted models. + */ + 'subject_returns_soft_deleted_models' => false, + + /* + * This model will be used to log activity. + * It should implement the Spatie\Activitylog\Contracts\Activity interface + * and extend Illuminate\Database\Eloquent\Model. + */ + 'activity_model' => \Spatie\Activitylog\Models\Activity::class, + + /* + * This is the name of the table that will be created by the migration and + * used by the Activity model shipped with this package. + */ + 'table_name' => env('ACTIVITY_LOGGER_TABLE_NAME', 'activity_log'), + + /* + * This is the database connection that will be used by the migration and + * the Activity model shipped with this package. In case it's not set + * Laravel's database.default will be used instead. + */ + 'database_connection' => env('ACTIVITY_LOGGER_DB_CONNECTION'), +]; diff --git a/config/app.php b/config/app.php index 324b513..f467267 100644 --- a/config/app.php +++ b/config/app.php @@ -65,7 +65,7 @@ return [ | */ - 'timezone' => 'UTC', + 'timezone' => env('APP_TIMEZONE', 'UTC'), /* |-------------------------------------------------------------------------- diff --git a/config/excel.php b/config/excel.php new file mode 100644 index 0000000..c1fd34a --- /dev/null +++ b/config/excel.php @@ -0,0 +1,380 @@ + [ + + /* + |-------------------------------------------------------------------------- + | Chunk size + |-------------------------------------------------------------------------- + | + | When using FromQuery, the query is automatically chunked. + | Here you can specify how big the chunk should be. + | + */ + 'chunk_size' => 1000, + + /* + |-------------------------------------------------------------------------- + | Pre-calculate formulas during export + |-------------------------------------------------------------------------- + */ + 'pre_calculate_formulas' => false, + + /* + |-------------------------------------------------------------------------- + | Enable strict null comparison + |-------------------------------------------------------------------------- + | + | When enabling strict null comparison empty cells ('') will + | be added to the sheet. + */ + 'strict_null_comparison' => false, + + /* + |-------------------------------------------------------------------------- + | CSV Settings + |-------------------------------------------------------------------------- + | + | Configure e.g. delimiter, enclosure and line ending for CSV exports. + | + */ + 'csv' => [ + 'delimiter' => ',', + 'enclosure' => '"', + 'line_ending' => PHP_EOL, + 'use_bom' => false, + 'include_separator_line' => false, + 'excel_compatibility' => false, + 'output_encoding' => '', + 'test_auto_detect' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Worksheet properties + |-------------------------------------------------------------------------- + | + | Configure e.g. default title, creator, subject,... + | + */ + 'properties' => [ + 'creator' => '', + 'lastModifiedBy' => '', + 'title' => '', + 'description' => '', + 'subject' => '', + 'keywords' => '', + 'category' => '', + 'manager' => '', + 'company' => '', + ], + ], + + 'imports' => [ + + /* + |-------------------------------------------------------------------------- + | Read Only + |-------------------------------------------------------------------------- + | + | When dealing with imports, you might only be interested in the + | data that the sheet exists. By default we ignore all styles, + | however if you want to do some logic based on style data + | you can enable it by setting read_only to false. + | + */ + 'read_only' => true, + + /* + |-------------------------------------------------------------------------- + | Ignore Empty + |-------------------------------------------------------------------------- + | + | When dealing with imports, you might be interested in ignoring + | rows that have null values or empty strings. By default rows + | containing empty strings or empty values are not ignored but can be + | ignored by enabling the setting ignore_empty to true. + | + */ + 'ignore_empty' => false, + + /* + |-------------------------------------------------------------------------- + | Heading Row Formatter + |-------------------------------------------------------------------------- + | + | Configure the heading row formatter. + | Available options: none|slug|custom + | + */ + 'heading_row' => [ + 'formatter' => 'slug', + ], + + /* + |-------------------------------------------------------------------------- + | CSV Settings + |-------------------------------------------------------------------------- + | + | Configure e.g. delimiter, enclosure and line ending for CSV imports. + | + */ + 'csv' => [ + 'delimiter' => null, + 'enclosure' => '"', + 'escape_character' => '\\', + 'contiguous' => false, + 'input_encoding' => Csv::GUESS_ENCODING, + ], + + /* + |-------------------------------------------------------------------------- + | Worksheet properties + |-------------------------------------------------------------------------- + | + | Configure e.g. default title, creator, subject,... + | + */ + 'properties' => [ + 'creator' => '', + 'lastModifiedBy' => '', + 'title' => '', + 'description' => '', + 'subject' => '', + 'keywords' => '', + 'category' => '', + 'manager' => '', + 'company' => '', + ], + + /* + |-------------------------------------------------------------------------- + | Cell Middleware + |-------------------------------------------------------------------------- + | + | Configure middleware that is executed on getting a cell value + | + */ + 'cells' => [ + 'middleware' => [ + //\Maatwebsite\Excel\Middleware\TrimCellValue::class, + //\Maatwebsite\Excel\Middleware\ConvertEmptyCellValuesToNull::class, + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Extension detector + |-------------------------------------------------------------------------- + | + | Configure here which writer/reader type should be used when the package + | needs to guess the correct type based on the extension alone. + | + */ + 'extension_detector' => [ + 'xlsx' => Excel::XLSX, + 'xlsm' => Excel::XLSX, + 'xltx' => Excel::XLSX, + 'xltm' => Excel::XLSX, + 'xls' => Excel::XLS, + 'xlt' => Excel::XLS, + 'ods' => Excel::ODS, + 'ots' => Excel::ODS, + 'slk' => Excel::SLK, + 'xml' => Excel::XML, + 'gnumeric' => Excel::GNUMERIC, + 'htm' => Excel::HTML, + 'html' => Excel::HTML, + 'csv' => Excel::CSV, + 'tsv' => Excel::TSV, + + /* + |-------------------------------------------------------------------------- + | PDF Extension + |-------------------------------------------------------------------------- + | + | Configure here which Pdf driver should be used by default. + | Available options: Excel::MPDF | Excel::TCPDF | Excel::DOMPDF + | + */ + 'pdf' => Excel::DOMPDF, + ], + + /* + |-------------------------------------------------------------------------- + | Value Binder + |-------------------------------------------------------------------------- + | + | PhpSpreadsheet offers a way to hook into the process of a value being + | written to a cell. In there some assumptions are made on how the + | value should be formatted. If you want to change those defaults, + | you can implement your own default value binder. + | + | Possible value binders: + | + | [x] Maatwebsite\Excel\DefaultValueBinder::class + | [x] PhpOffice\PhpSpreadsheet\Cell\StringValueBinder::class + | [x] PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder::class + | + */ + 'value_binder' => [ + 'default' => Maatwebsite\Excel\DefaultValueBinder::class, + ], + + 'cache' => [ + /* + |-------------------------------------------------------------------------- + | Default cell caching driver + |-------------------------------------------------------------------------- + | + | By default PhpSpreadsheet keeps all cell values in memory, however when + | dealing with large files, this might result into memory issues. If you + | want to mitigate that, you can configure a cell caching driver here. + | When using the illuminate driver, it will store each value in the + | cache store. This can slow down the process, because it needs to + | store each value. You can use the "batch" store if you want to + | only persist to the store when the memory limit is reached. + | + | Drivers: memory|illuminate|batch + | + */ + 'driver' => 'memory', + + /* + |-------------------------------------------------------------------------- + | Batch memory caching + |-------------------------------------------------------------------------- + | + | When dealing with the "batch" caching driver, it will only + | persist to the store when the memory limit is reached. + | Here you can tweak the memory limit to your liking. + | + */ + 'batch' => [ + 'memory_limit' => 60000, + ], + + /* + |-------------------------------------------------------------------------- + | Illuminate cache + |-------------------------------------------------------------------------- + | + | When using the "illuminate" caching driver, it will automatically use + | your default cache store. However if you prefer to have the cell + | cache on a separate store, you can configure the store name here. + | You can use any store defined in your cache config. When leaving + | at "null" it will use the default store. + | + */ + 'illuminate' => [ + 'store' => null, + ], + + /* + |-------------------------------------------------------------------------- + | Cache Time-to-live (TTL) + |-------------------------------------------------------------------------- + | + | The TTL of items written to cache. If you want to keep the items cached + | indefinitely, set this to null. Otherwise, set a number of seconds, + | a \DateInterval, or a callable. + | + | Allowable types: callable|\DateInterval|int|null + | + */ + 'default_ttl' => 10800, + ], + + /* + |-------------------------------------------------------------------------- + | Transaction Handler + |-------------------------------------------------------------------------- + | + | By default the import is wrapped in a transaction. This is useful + | for when an import may fail and you want to retry it. With the + | transactions, the previous import gets rolled-back. + | + | You can disable the transaction handler by setting this to null. + | Or you can choose a custom made transaction handler here. + | + | Supported handlers: null|db + | + */ + 'transactions' => [ + 'handler' => 'db', + 'db' => [ + 'connection' => null, + ], + ], + + 'temporary_files' => [ + + /* + |-------------------------------------------------------------------------- + | Local Temporary Path + |-------------------------------------------------------------------------- + | + | When exporting and importing files, we use a temporary file, before + | storing reading or downloading. Here you can customize that path. + | permissions is an array with the permission flags for the directory (dir) + | and the create file (file). + | + */ + 'local_path' => storage_path('framework/cache/laravel-excel'), + + /* + |-------------------------------------------------------------------------- + | Local Temporary Path Permissions + |-------------------------------------------------------------------------- + | + | Permissions is an array with the permission flags for the directory (dir) + | and the create file (file). + | If omitted the default permissions of the filesystem will be used. + | + */ + 'local_permissions' => [ + // 'dir' => 0755, + // 'file' => 0644, + ], + + /* + |-------------------------------------------------------------------------- + | Remote Temporary Disk + |-------------------------------------------------------------------------- + | + | When dealing with a multi server setup with queues in which you + | cannot rely on having a shared local temporary path, you might + | want to store the temporary file on a shared disk. During the + | queue executing, we'll retrieve the temporary file from that + | location instead. When left to null, it will always use + | the local path. This setting only has effect when using + | in conjunction with queued imports and exports. + | + */ + 'remote_disk' => null, + 'remote_prefix' => null, + + /* + |-------------------------------------------------------------------------- + | Force Resync + |-------------------------------------------------------------------------- + | + | When dealing with a multi server setup as above, it's possible + | for the clean up that occurs after entire queue has been run to only + | cleanup the server that the last AfterImportJob runs on. The rest of the server + | would still have the local temporary file stored on it. In this case your + | local storage limits can be exceeded and future imports won't be processed. + | To mitigate this you can set this config value to be true, so that after every + | queued chunk is processed the local temporary file is deleted on the server that + | processed it. + | + */ + 'force_resync_remote' => null, + ], +]; diff --git a/database/migrations/2025_05_13_012858_create_activity_log_table.php b/database/migrations/2025_05_13_012858_create_activity_log_table.php new file mode 100644 index 0000000..7c05bc8 --- /dev/null +++ b/database/migrations/2025_05_13_012858_create_activity_log_table.php @@ -0,0 +1,27 @@ +create(config('activitylog.table_name'), function (Blueprint $table) { + $table->bigIncrements('id'); + $table->string('log_name')->nullable(); + $table->text('description'); + $table->nullableMorphs('subject', 'subject'); + $table->nullableMorphs('causer', 'causer'); + $table->json('properties')->nullable(); + $table->timestamps(); + $table->index('log_name'); + }); + } + + public function down() + { + Schema::connection(config('activitylog.database_connection'))->dropIfExists(config('activitylog.table_name')); + } +} diff --git a/database/migrations/2025_05_13_012859_add_event_column_to_activity_log_table.php b/database/migrations/2025_05_13_012859_add_event_column_to_activity_log_table.php new file mode 100644 index 0000000..7b797fd --- /dev/null +++ b/database/migrations/2025_05_13_012859_add_event_column_to_activity_log_table.php @@ -0,0 +1,22 @@ +table(config('activitylog.table_name'), function (Blueprint $table) { + $table->string('event')->nullable()->after('subject_type'); + }); + } + + public function down() + { + Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) { + $table->dropColumn('event'); + }); + } +} diff --git a/database/migrations/2025_05_13_012900_add_batch_uuid_column_to_activity_log_table.php b/database/migrations/2025_05_13_012900_add_batch_uuid_column_to_activity_log_table.php new file mode 100644 index 0000000..8f7db66 --- /dev/null +++ b/database/migrations/2025_05_13_012900_add_batch_uuid_column_to_activity_log_table.php @@ -0,0 +1,22 @@ +table(config('activitylog.table_name'), function (Blueprint $table) { + $table->uuid('batch_uuid')->nullable()->after('properties'); + }); + } + + public function down() + { + Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) { + $table->dropColumn('batch_uuid'); + }); + } +} diff --git a/resources/lang/zh-TW/roles.php b/resources/lang/zh-TW/roles.php index 6bf372b..e6e0be1 100644 --- a/resources/lang/zh-TW/roles.php +++ b/resources/lang/zh-TW/roles.php @@ -2,13 +2,17 @@ return [ 'list' => '角色列表', - 'CreateNewRole' => '新增角色', + 'CreateNew' => '新增角色', + 'EditRole' => '編輯角色', 'edit' => '編輯', 'delete' => '刪除', 'no' => '編號', 'name' => '名稱', 'permissions' => '權限', + + 'role_name' =>'角色名稱', + 'select_permissions'=>'選擇權限', 'create' => '新增', 'action' => '操作', diff --git a/resources/lang/zh-TW/users.php b/resources/lang/zh-TW/users.php index 9750b20..529243a 100644 --- a/resources/lang/zh-TW/users.php +++ b/resources/lang/zh-TW/users.php @@ -2,7 +2,9 @@ return [ 'list' => '使用者列表', - 'CreateNewRole' => '新增使用者', + 'CreateNew' => '新增使用者', + 'EditUser' => '編輯使用者', + 'ImportData' => '滙入使用者', 'edit' => '編輯', 'delete' => '刪除', @@ -13,6 +15,10 @@ return [ 'birthday' => '生日', 'status' => '狀態', 'role' =>'角色', + + 'select_gender'=>'選擇性別', + 'select_status'=>'選擇狀態', + 'select_role'=>'選擇角色', 'create' => '新增', 'action' => '操作', diff --git a/resources/views/components/select-category.blade.php b/resources/views/components/select-category.blade.php new file mode 100644 index 0000000..5fde8ab --- /dev/null +++ b/resources/views/components/select-category.blade.php @@ -0,0 +1,16 @@ +@props(['selected','fieldName', 'modelId']) +
+ +
\ No newline at end of file diff --git a/resources/views/livewire/admin/activity-log.blade.php b/resources/views/livewire/admin/activity-log.blade.php new file mode 100644 index 0000000..3879e0a --- /dev/null +++ b/resources/views/livewire/admin/activity-log.blade.php @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/resources/views/livewire/admin/role-form.blade.php b/resources/views/livewire/admin/role-form.blade.php index 2488d7c..1d55768 100644 --- a/resources/views/livewire/admin/role-form.blade.php +++ b/resources/views/livewire/admin/role-form.blade.php @@ -1,25 +1,21 @@ -
-@if ($showCreateModal) - -
- - -
+ +
+ + +
- -
- - -
-
-
- @endif -
+ +
+ + +
+
+ \ No newline at end of file diff --git a/resources/views/livewire/admin/role-header.blade.php b/resources/views/livewire/admin/role-header.blade.php new file mode 100644 index 0000000..a155538 --- /dev/null +++ b/resources/views/livewire/admin/role-header.blade.php @@ -0,0 +1,8 @@ +
+ +
\ No newline at end of file diff --git a/resources/views/livewire/admin/roles.blade.php b/resources/views/livewire/admin/roles.blade.php index 190defc..04ed419 100644 --- a/resources/views/livewire/admin/roles.blade.php +++ b/resources/views/livewire/admin/roles.blade.php @@ -1,21 +1,5 @@ - - - 角色管理 - - - @if (session()->has('message')) - - - @endif - - {{-- 單一 Livewire 元件,內含資料表與 Modal --}} + \ No newline at end of file diff --git a/resources/views/livewire/admin/user-form.blade.php b/resources/views/livewire/admin/user-form.blade.php index d44fed3..1e69c56 100644 --- a/resources/views/livewire/admin/user-form.blade.php +++ b/resources/views/livewire/admin/user-form.blade.php @@ -1,29 +1,29 @@ - +
- + -
- +
+
diff --git a/resources/views/livewire/admin/user-header.blade.php b/resources/views/livewire/admin/user-header.blade.php new file mode 100644 index 0000000..83378ef --- /dev/null +++ b/resources/views/livewire/admin/user-header.blade.php @@ -0,0 +1,16 @@ +
+ + + + +
\ No newline at end of file diff --git a/resources/views/livewire/admin/user-import-data.blade.php b/resources/views/livewire/admin/user-import-data.blade.php new file mode 100644 index 0000000..6415b7e --- /dev/null +++ b/resources/views/livewire/admin/user-import-data.blade.php @@ -0,0 +1,63 @@ + + + {{-- 說明區塊 --}} +
+

匯入格式說明

+

請依下列表格格式準備 Excel 或 CSV 檔案:

+ +
+ + + + + + + + + + + + + + + + +
欄位名稱說明範例
?????????
+
+
+ + + {{-- 檔案上傳 --}} +
+
+ +

+ 系統限制:最大上傳 {{ $maxUploadSize }} +

+
+ +
+ + +
+ +
+ 檔案上傳中,請稍候... +
+
+ + + +
+ + +
+
+
\ No newline at end of file diff --git a/resources/views/livewire/admin/users.blade.php b/resources/views/livewire/admin/users.blade.php index 2228e77..6d3ebcb 100644 --- a/resources/views/livewire/admin/users.blade.php +++ b/resources/views/livewire/admin/users.blade.php @@ -1,21 +1,6 @@ - - - 使用者管理 - - - @if (session()->has('message')) - - - @endif - - {{-- 單一 Livewire 元件,內含資料表與 Modal --}} + + \ 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 94625fe..782d827 100644 --- a/resources/views/livewire/layout/admin/sidebar.blade.php +++ b/resources/views/livewire/layout/admin/sidebar.blade.php @@ -3,6 +3,7 @@ $menus = [ ['label' => 'Dashboard', 'route' => 'admin.dashboard', 'icon' => 'home', 'permission' => null], + ['label' => 'ActivityLog', 'route' => 'admin.activity-log', 'icon' => 'clock', 'permission' => null], ['label' => 'Role', 'route' => 'admin.roles', 'icon' => 'user-circle', 'permission' => 'role-list'], ['label' => 'User', 'route' => 'admin.users', 'icon' => 'user-circle', 'permission' => 'user-list'], ]; diff --git a/routes/web.php b/routes/web.php index cf6a279..1efc7ff 100644 --- a/routes/web.php +++ b/routes/web.php @@ -20,15 +20,8 @@ require __DIR__.'/auth.php'; Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () { Route::get('/dashboard', AdminDashboard::class)->name('dashboard'); - - Route::get('/roles', function () { - return view('livewire.admin.roles'); - })->name('roles'); - Route::get('/roles-table', RoleTable::class)->name('roles-table'); - - Route::get('/users', function () { - return view('livewire.admin.users'); - })->name('users'); - Route::get('/users-table', UserTable::class)->name('users-table'); + Route::get('/activity-log', function () {return view('livewire.admin.activity-log');})->name('activity-log'); + Route::get('/roles', function () {return view('livewire.admin.roles');})->name('roles'); + Route::get('/users', function () {return view('livewire.admin.users');})->name('users'); }); \ No newline at end of file diff --git a/開發手冊.ini b/開發手冊.ini index 1a44d47..cb59e08 100644 --- a/開發手冊.ini +++ b/開發手冊.ini @@ -60,13 +60,21 @@ composer require wireui/wireui php artisan vendor:publish --tag="wireui.config" php artisan make:livewire Admin/Roles/Index -php artisan make:livewire Admin/Roles/CreateRole -php artisan make:livewire Admin/Roles/EditRole php artisan make:livewire Admin/Users php artisan make:component Table +操作記錄 +composer require spatie/laravel-activitylog +php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-migrations" +php artisan migrate +php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-config" +php artisan make:model ActivityLog + +Laravel Excel +composer require maatwebsite/excel + composer require power-components/livewire-powergrid php artisan vendor:publish --tag=livewire-powergrid-config