調整User 新版 20250513

This commit is contained in:
allen.yan 2025-05-13 10:59:22 +08:00
parent a8caec12e6
commit 34d4748cd2
38 changed files with 2175 additions and 217 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -0,0 +1,20 @@
<?php
namespace App\Enums\Traits;
use Illuminate\Support\Collection;
trait HasLabels
{
public static function options(): Collection
{
return collect(self::cases())->mapWithKeys(function (self $case) {
return [$case->value => $case->labels()];
});
}
public function labelPowergridFilter(): string
{
return $this->labels();
}
}

View File

@ -2,8 +2,12 @@
namespace App\Enums; namespace App\Enums;
use App\Enums\Traits\HasLabels;
enum UserGender: string enum UserGender: string
{ {
use HasLabels;
case Male = 'male'; case Male = 'male';
case Female = 'female'; case Female = 'female';
case Other = 'other'; case Other = 'other';
@ -19,9 +23,4 @@ enum UserGender: string
self::Unset => __('enums.user.gender.Unset'), self::Unset => __('enums.user.gender.Unset'),
}; };
} }
public function labelPowergridFilter(): String
{
return $this -> labels();
}
} }

View File

@ -2,8 +2,12 @@
namespace App\Enums; namespace App\Enums;
use App\Enums\Traits\HasLabels;
enum UserStatus: int enum UserStatus: int
{ {
use HasLabels;
case Active = 0; // 正常 case Active = 0; // 正常
case Suspended = 1; // 停權 case Suspended = 1; // 停權
case Deleting = 2; // 刪除中 case Deleting = 2; // 刪除中
@ -17,9 +21,4 @@ enum UserStatus: int
self::Deleting => __('enums.user.status.Deleting'), self::Deleting => __('enums.user.status.Deleting'),
}; };
} }
public function labelPowergridFilter(): String
{
return $this -> labels();
}
} }

View File

@ -0,0 +1,45 @@
<?php
namespace App\Imports;
use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Maatwebsite\Excel\Concerns\WithChunkReading;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Imports\HeadingRowFormatter;
use Illuminate\Support\Facades\Log;
use App\Jobs\ImportUserChunkJob;
class DataImport implements ToCollection, WithHeadingRow, WithChunkReading
{
protected int $con=0;
protected string $modelName;
public function __construct(string $modelName)
{
HeadingRowFormatter::default('none');
$this->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;
}
}

59
app/Jobs/ImportJob.php Normal file
View File

@ -0,0 +1,59 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Support\Facades\Storage;
use Maatwebsite\Excel\Facades\Excel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Log;
use App\Imports\DataImport;
class ImportJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected string $modelName;
protected string $filePath;
public $timeout = 3600;
public $tries = 1;
/**
* Create a new job instance.
*/
public function __construct(string $filePath,string $modelName)
{
$this->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(),
]);
}
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace App\Jobs;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class ImportUserChunkJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected Collection $rows;
protected String $id;
public function __construct(Collection $rows,String $id)
{
$this->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'));
}
}

View File

@ -0,0 +1,118 @@
<?php
namespace App\Livewire\Admin;
use App\Models\ActivityLog;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Builder;
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 Spatie\Activitylog\Models\Activity;
final class ActivityLogTable extends PowerGridComponent
{
use WithExport;
public string $tableName = 'activity-log-table';
public bool $canDownload;
public bool $showFilters = false;
public function boot(): void
{
config(['livewire-powergrid.filter' => '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[] = "<strong>{$key}</strong>: {$oldValue}{$newValue}";
}
}
//dd(implode('<br>', $changes));
return implode('<br>', $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('動作'),
];
}
}

View File

@ -2,15 +2,25 @@
namespace App\Livewire\Admin; namespace App\Livewire\Admin;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Auth;
use Livewire\Component; use Livewire\Component;
use WireUi\Traits\WireUiActions;
use Spatie\Permission\Models\Role; use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission; use Spatie\Permission\Models\Permission;
class RoleForm extends Component class RoleForm extends Component
{ {
use WireUiActions;
protected $listeners = ['openCreateRoleModal','openEditRoleModal', 'deleteRole']; protected $listeners = ['openCreateRoleModal','openEditRoleModal', 'deleteRole'];
public bool $canCreate;
public bool $canEdit;
public bool $canDelect;
public $showCreateModal=false; public $showCreateModal=false;
public ?int $roleId = null; public ?int $roleId = null;
public $name = ''; public $name = '';
@ -22,6 +32,9 @@ class RoleForm extends Component
public function mount() public function mount()
{ {
$this->permissions = Permission::all(); $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() public function openCreateRoleModal()
@ -47,24 +60,44 @@ class RoleForm extends Component
]); ]);
if ($this->roleId) { if ($this->roleId) {
$role = Role::findOrFail($this->roleId); if ($this->canEdit) {
$role->update(['name' => $this->name]); $role = Role::findOrFail($this->roleId);
$role->syncPermissions($this->selectedPermissions); $role->update(['name' => $this->name]);
session()->flash('message', '角色已更新'); $role->syncPermissions($this->selectedPermissions);
$this->notification()->send([
'icon' => 'success',
'title' => '成功',
'description' => '角色已更新',
]);
}
} else { } else {
$role = Role::create(['name' => $this->name]); if ($this->canCreate) {
$role->syncPermissions($this->selectedPermissions); $role = Role::create(['name' => $this->name]);
session()->flash('message', '角色已新增'); $role->syncPermissions($this->selectedPermissions);
$this->notification()->send([
'icon' => 'success',
'title' => '成功',
'description' => '角色已新增',
]);
}
} }
$this->resetFields(); $this->resetFields();
$this->showCreateModal = false; $this->showCreateModal = false;
$this->dispatch('pg:eventRefresh-role-table');
} }
public function deleteRole($id) public function deleteRole($id)
{ {
Role::findOrFail($id)->delete(); if ($this->canDelect) {
session()->flash('message', '角色已刪除'); Role::findOrFail($id)->delete();
$this->notification()->send([
'icon' => 'success',
'title' => '成功',
'description' => '角色已刪除',
]);
$this->dispatch('pg:eventRefresh-role-table');
}
} }
public function resetFields() public function resetFields()

View File

@ -3,7 +3,9 @@
namespace App\Livewire\Admin; namespace App\Livewire\Admin;
use Spatie\Permission\Models\Role; use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use PowerComponents\LivewirePowerGrid\Button; use PowerComponents\LivewirePowerGrid\Button;
use PowerComponents\LivewirePowerGrid\Column; use PowerComponents\LivewirePowerGrid\Column;
@ -11,28 +13,55 @@ use PowerComponents\LivewirePowerGrid\Facades\Filter;
use PowerComponents\LivewirePowerGrid\Facades\PowerGrid; use PowerComponents\LivewirePowerGrid\Facades\PowerGrid;
use PowerComponents\LivewirePowerGrid\PowerGridFields; use PowerComponents\LivewirePowerGrid\PowerGridFields;
use PowerComponents\LivewirePowerGrid\PowerGridComponent; use PowerComponents\LivewirePowerGrid\PowerGridComponent;
use Livewire\Attributes\On;
use WireUi\Traits\WireUiActions;
final class RoleTable extends PowerGridComponent final class RoleTable extends PowerGridComponent
{ {
use WireUiActions;
public string $tableName = 'role-table'; public string $tableName = 'role-table';
public bool $canCreate;
public bool $canEdit;
public bool $canDelect;
public function boot(): void public function boot(): void
{ {
config(['livewire-powergrid.filter' => 'outside']); 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 public function setUp(): array
{ {
//$this->showCheckBox(); if($this->canDelect){
$this->showCheckBox();
return [ }
//PowerGrid::header() $actions = [];
// ->showSearchInput(), $header =PowerGrid::header();
//PowerGrid::footer() if($this->canCreate){
// ->showPerPage() $header->includeViewOnTop('livewire.admin.role-header');
// ->showRecordCount(), }
]; $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 public function datasource(): Builder
{ {
@ -47,27 +76,72 @@ final class RoleTable extends PowerGridComponent
public function fields(): PowerGridFields public function fields(): PowerGridFields
{ {
$allPermissions = Permission::pluck('name')->sort()->values();
return PowerGrid::fields() return PowerGrid::fields()
->add('id') ->add('id')
->add('name') ->add('name')
->add('permissions_list' ,fn(Role $model)=> $model->permissions->pluck('name')->implode(', ')) ->add('permissions_list', function (Role $model) use ($allPermissions) {
->add('created_at_formatted', fn (Role $model) => Carbon::parse($model->created_at)->format('d/m/Y H:i:s')); $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 public function columns(): array
{ {
return [ $column=[];
Column::make(__('roles.no'), 'id')->sortable()->searchable(), $column[]=Column::make(__('roles.no'), 'id')->sortable()->searchable();
Column::make(__('roles.name'), 'name')->sortable()->searchable(), $column[]=Column::make(__('roles.name'), 'name')->sortable()->searchable()
Column::make(__('roles.permissions'), 'permissions_list'), ->editOnClick(
Column::make('Created at', 'created_at_formatted', 'created_at')->sortable(), hasPermission: $this->canEdit,
Column::action('Action') 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 public function filters(): array
{ {
return [ return [
Filter::inputText('name')->placeholder(__('roles.name')),
Filter::datetimepicker('created_at'), Filter::datetimepicker('created_at'),
]; ];
} }
@ -75,18 +149,22 @@ final class RoleTable extends PowerGridComponent
public function actions(Role $row): array public function actions(Role $row): array
{ {
return [ $actions = [];
Button::add('edit') if ($this->canEdit) {
$actions[] =Button::add('edit')
->slot(__('roles.edit')) ->slot(__('roles.edit'))
->icon('solid-pencil-square') ->icon('solid-pencil-square')
->class('inline-flex items-center gap-1 px-3 py-1 rounded ') ->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
->dispatchTo('admin.role-form', 'openEditRoleModal', ['id' => $row->id]), ->dispatchTo('admin.role-form', 'openEditRoleModal', ['id' => $row->id]);
Button::add('delete') }
->slot(__('delete')) if($this->canDelect){
$actions[] =Button::add('delete')
->slot(__('roles.delete'))
->icon('solid-trash') ->icon('solid-trash')
->class('inline-flex items-center gap-1 px-3 py-1 rounded ') ->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;
} }
/* /*

View File

@ -2,7 +2,11 @@
namespace App\Livewire\Admin; namespace App\Livewire\Admin;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Auth;
use Livewire\Component; use Livewire\Component;
use WireUi\Traits\WireUiActions;
use App\Models\User; use App\Models\User;
use App\Enums\UserGender; use App\Enums\UserGender;
@ -11,17 +15,21 @@ use Spatie\Permission\Models\Role;
class UserForm extends Component 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 $genderOptions =[];
public array $statusOptions =[]; public array $statusOptions =[];
public $rolesOptions = []; // 所有角色清單 public $rolesOptions = []; // 所有角色清單
public $selectedRoles = []; // 表單中選到的權限 public $selectedRoles = []; // 表單中選到的權限
public ?int $userId = null; public ?int $userId = null;
public array $fields = [ public array $fields = [
'name' =>'', 'name' =>'',
'email' => '', 'email' => '',
@ -30,6 +38,7 @@ class UserForm extends Component
'gender' => 'unset', 'gender' => 'unset',
'status' => 0, 'status' => 0,
]; ];
protected $rules = [ protected $rules = [
'fields.name' => 'required|string|max:255', 'fields.name' => 'required|string|max:255',
@ -52,48 +61,72 @@ class UserForm extends Component
'value' => $status->value, 'value' => $status->value,
])->toArray(); ])->toArray();
$this->rolesOptions = Role::all(); $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->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->resetFields();
$this->userId = $user->id; $this->showModal = false;
$this->fields = $user->only(array_keys($this->fields));
$this->selectedRoles = $user->roles()->pluck('id')->toArray();
$this->showCreateModal = true;
} }
public function save() public function save()
{ {
$this->validate(); //$this->validate();
if ($this->userId) { if ($this->userId) {
$user = User::findOrFail($this->userId); if ($this->canEdit) {
$user->update($this->fields); $obj = User::findOrFail($this->userId);
$user->syncRoles($this->selectedRoles); $obj->update($this->fields);
session()->flash('message', '使用者已更新'); $obj->syncRoles($this->selectedRoles);
$this->notification()->send([
'icon' => 'success',
'title' => '成功',
'description' => '使用者已更新',
]);
}
} else { } else {
$user = User::create($this->fields); if ($this->canCreate) {
$user->syncRoles($this->selectedRoles); $obj = User::create($this->fields);
session()->flash('message', '使用者已新增'); $obj->syncRoles($this->selectedRoles);
$this->notification()->send([
'icon' => 'success',
'title' => '成功',
'description' => '使用者已新增',
]);
}
} }
$this->resetFields(); $this->resetFields();
$this->showCreateModal = false; $this->showModal = false;
$this->dispatch('pg:eventRefresh-user-table'); $this->dispatch('pg:eventRefresh-user-table');
} }
public function deleteUser($id) public function deleteUser($id)
{ {
User::findOrFail($id)->delete(); if ($this->canDelect) {
session()->flash('message', '使用者已刪除'); User::findOrFail($id)->delete();
$this->notification()->send([
'icon' => 'success',
'title' => '成功',
'description' => '使用者已刪除',
]);
$this->dispatch('pg:eventRefresh-user-table');
}
} }
public function resetFields() public function resetFields()

View File

@ -0,0 +1,114 @@
<?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 UserImportData 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('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');
}
}

View File

@ -6,6 +6,8 @@ use App\Models\User;
use App\Enums\UserGender; use App\Enums\UserGender;
use App\Enums\UserStatus; use App\Enums\UserStatus;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Blade;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use PowerComponents\LivewirePowerGrid\Button; use PowerComponents\LivewirePowerGrid\Button;
use PowerComponents\LivewirePowerGrid\Column; use PowerComponents\LivewirePowerGrid\Column;
@ -13,48 +15,63 @@ use PowerComponents\LivewirePowerGrid\Facades\Filter;
use PowerComponents\LivewirePowerGrid\Facades\PowerGrid; use PowerComponents\LivewirePowerGrid\Facades\PowerGrid;
use PowerComponents\LivewirePowerGrid\PowerGridFields; use PowerComponents\LivewirePowerGrid\PowerGridFields;
use PowerComponents\LivewirePowerGrid\PowerGridComponent; use PowerComponents\LivewirePowerGrid\PowerGridComponent;
//use PowerComponents\LivewirePowerGrid\Traits\WithExport; use PowerComponents\LivewirePowerGrid\Traits\WithExport;
use PowerComponents\LivewirePowerGrid\Components\SetUp\Exportable; use PowerComponents\LivewirePowerGrid\Components\SetUp\Exportable;
use PowerComponents\LivewirePowerGrid\Facades\Rule; use PowerComponents\LivewirePowerGrid\Facades\Rule;
use Livewire\Attributes\On; use Livewire\Attributes\On;
use WireUi\Traits\WireUiActions;
final class UserTable extends PowerGridComponent final class UserTable extends PowerGridComponent
{ {
//use WithExport ; use WithExport, WireUiActions;
public string $tableName = 'user-table'; public string $tableName = 'user-table';
public bool $showFilters = false; public bool $showFilters = false;
public bool $canCreate;
public bool $canEdit;
public bool $canDownload;
public bool $canDelect;
public function boot(): void public function boot(): void
{ {
config(['livewire-powergrid.filter' => 'outside']); 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 public function setUp(): array
{ {
$this->showCheckBox(); if($this->canDownload || $this->canDelect){
$this->showCheckBox();
return [ }
PowerGrid::exportable(fileName: 'my-export-file') $actions = [];
->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV), $actions[] =PowerGrid::exportable(fileName: $this->tableName.'-file')
PowerGrid::header() ->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV);
//->showSoftDeletes() $header = PowerGrid::header()
->showToggleColumns() ->showToggleColumns();
->showSearchInput(), if($this->canCreate){
PowerGrid::footer()->showPerPage()->showRecordCount(), $header->includeViewOnTop('livewire.admin.user-header');
]; }
$actions[]=$header;
$actions[]=PowerGrid::footer()->showPerPage()->showRecordCount();
return $actions;
} }
public function header(): array public function header(): array
{ {
return [ $actions = [];
Button::add('bulk-delete') if ($this->canDelect) {
$actions[]=Button::add('bulk-delete')
->slot('Bulk delete (<span x-text="window.pgBulkActions.count(\'' . $this->tableName . '\')"></span>)') ->slot('Bulk delete (<span x-text="window.pgBulkActions.count(\'' . $this->tableName . '\')"></span>)')
->icon('solid-trash',['id' => 'my-custom-icon-id', 'class' => 'font-bold']) ->icon('solid-trash',['id' => 'my-custom-icon-id', 'class' => 'font-bold'])
->class('inline-flex items-center gap-1 px-3 py-1 rounded ') ->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 public function datasource(): Builder
@ -75,8 +92,38 @@ final class UserTable extends PowerGridComponent
->add('email') ->add('email')
->add('phone') ->add('phone')
->add('birthday_formatted',fn (User $model) => Carbon::parse($model->birthday)->format('Y-m-d')) ->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('gender_str', function (User $model) {
->add('status', fn (User $model) => UserStatus::from($model->status)->labels()) if ($this->canEdit) {
return Blade::render(
'<x-select-category type="occurrence" :options=$options :modelId=$modelId :fieldName=$fieldName :selected=$selected/>',
[
'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(
'<x-select-category type="occurrence" :options=$options :modelId=$modelId :fieldName=$fieldName :selected=$selected/>',
[
'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('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')); ->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() ->sortable()
->searchable() ->searchable()
->editOnClick( ->editOnClick(
hasPermission: true, hasPermission: $this->canEdit,
dataField: 'name', dataField: 'name',
fallback: 'N/A', fallback: 'N/A',
saveOnMouseOut: true saveOnMouseOut: true
@ -98,7 +145,7 @@ final class UserTable extends PowerGridComponent
->sortable() ->sortable()
->searchable() ->searchable()
->editOnClick( ->editOnClick(
hasPermission: true, hasPermission: $this->canEdit,
dataField: 'email', dataField: 'email',
fallback: 'N/A', fallback: 'N/A',
saveOnMouseOut: true saveOnMouseOut: true
@ -107,20 +154,65 @@ final class UserTable extends PowerGridComponent
->sortable() ->sortable()
->searchable() ->searchable()
->editOnClick( ->editOnClick(
hasPermission: true, hasPermission: $this->canEdit,
dataField: 'phone', dataField: 'phone',
fallback: 'N/A', fallback: 'N/A',
saveOnMouseOut: true 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.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(__('users.role'), 'roles'),
Column::make('建立時間', 'created_at_formatted', 'created_at')->sortable(), Column::make('建立時間', 'created_at_formatted', 'created_at')->sortable(),
Column::action('操作') 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 public function filters(): array
{ {
@ -128,11 +220,11 @@ final class UserTable extends PowerGridComponent
Filter::inputText('name')->placeholder(__('users.name')), Filter::inputText('name')->placeholder(__('users.name')),
Filter::inputText('email')->placeholder('Email'), Filter::inputText('email')->placeholder('Email'),
Filter::inputText('phone')->placeholder(__('users.phone')), Filter::inputText('phone')->placeholder(__('users.phone')),
Filter::enumSelect('gender','users.gender') Filter::enumSelect('gender_str','users.gender')
->datasource(UserGender::cases()) ->datasource(UserGender::cases())
->optionLabel('users.gender'), ->optionLabel('users.gender'),
Filter::datepicker('birthday'), Filter::datepicker('birthday'),
Filter::enumSelect('status', 'users.status') Filter::enumSelect('status_str', 'users.status')
->datasource(UserStatus::cases()) ->datasource(UserStatus::cases())
->optionLabel('users.status'), ->optionLabel('users.status'),
Filter::datetimepicker('created_at'), Filter::datetimepicker('created_at'),
@ -141,47 +233,22 @@ final class UserTable extends PowerGridComponent
public function actions(User $row): array public function actions(User $row): array
{ {
return [ $actions = [];
if ($this->canEdit) {
Button::add('edit') $actions[]=Button::add('edit')
->slot(__('users.edit')) ->slot(__('users.edit'))
->icon('solid-pencil-square') ->icon('solid-pencil-square')
->class('inline-flex items-center gap-1 px-3 py-1 rounded ') ->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
->dispatchTo('admin.user-form', 'openEditUserModal', ['id' => $row->id]), ->dispatchTo('admin.user-form', 'openModal', ['id' => $row->id]);
Button::add('delete') }
if($this->canDelect){
$actions[]=Button::add('delete')
->slot(__('users.delete')) ->slot(__('users.delete'))
->icon('solid-trash') ->icon('solid-trash')
->class('inline-flex items-center gap-1 px-3 py-1 rounded ') ->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
->dispatchTo('admin.user-form', 'deleteUser', ['id' => $row->id]), ->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.
} }
return $actions;
} }

View File

@ -7,11 +7,13 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Spatie\Permission\Traits\HasRoles; use Spatie\Permission\Traits\HasRoles;
use App\Traits\LogsModelActivity;
use Spatie\Activitylog\Traits\CausesActivity;
class User extends Authenticatable class User extends Authenticatable
{ {
/** @use HasFactory<\Database\Factories\UserFactory> */ /** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable, HasRoles; use HasFactory, Notifiable, HasRoles, LogsModelActivity,CausesActivity;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
@ -48,7 +50,9 @@ class User extends Authenticatable
return [ return [
'email_verified_at' => 'datetime', 'email_verified_at' => 'datetime',
'password' => 'hashed', 'password' => 'hashed',
'birthday' => 'date' 'birthday' => 'date',
'gender' => \App\Enums\UserGender::class,
'status' => \App\Enums\UserStatus::class,
]; ];
} }
} }

View File

@ -0,0 +1,23 @@
<?php
namespace App\Traits;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
trait LogsModelActivity
{
use LogsActivity;
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->useLogName(strtolower(class_basename(static::class)))
->logOnly($this->getFillable())
->logOnlyDirty()
->dontSubmitEmptyLogs()
->setDescriptionForEvent(function (string $eventName) {
return class_basename(static::class) . "{$eventName}";
});
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection;
use Illuminate\View\Component;
class SelectCategory extends Component
{
/**
* Create a new component instance.
*/
public function __construct(public Collection $options, public int $modelId,public string $fieldName, public string $selected)
{
//dd($options,$modelId,$fieldName,$selected);
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.select-category');
}
}

View File

@ -11,8 +11,10 @@
"laravel/tinker": "^2.10.1", "laravel/tinker": "^2.10.1",
"livewire/livewire": "^3.4", "livewire/livewire": "^3.4",
"livewire/volt": "^1.7.0", "livewire/volt": "^1.7.0",
"maatwebsite/excel": "^3.1",
"openspout/openspout": "^4.0", "openspout/openspout": "^4.0",
"power-components/livewire-powergrid": "^6.3", "power-components/livewire-powergrid": "^6.3",
"spatie/laravel-activitylog": "^4.10",
"spatie/laravel-permission": "^6.17", "spatie/laravel-permission": "^6.17",
"wire-elements/modal": "^2.0", "wire-elements/modal": "^2.0",
"wireui/wireui": "^2.4" "wireui/wireui": "^2.4"

686
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "357c14558fec81a259ca41d9f582db48", "content-hash": "6ee6a3628fc42da4568e5a2564abd9f0",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@ -135,6 +135,166 @@
], ],
"time": "2024-02-09T16:56:22+00:00" "time": "2024-02-09T16:56:22+00:00"
}, },
{
"name": "composer/pcre",
"version": "3.3.2",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"conflict": {
"phpstan/phpstan": "<1.11.10"
},
"require-dev": {
"phpstan/phpstan": "^1.12 || ^2",
"phpstan/phpstan-strict-rules": "^1 || ^2",
"phpunit/phpunit": "^8 || ^9"
},
"type": "library",
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
},
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Pcre\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
}
],
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
"keywords": [
"PCRE",
"preg",
"regex",
"regular expression"
],
"support": {
"issues": "https://github.com/composer/pcre/issues",
"source": "https://github.com/composer/pcre/tree/3.3.2"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2024-11-12T16:29:46+00:00"
},
{
"name": "composer/semver",
"version": "3.4.3",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
"reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
"reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
"shasum": ""
},
"require": {
"php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^1.11",
"symfony/phpunit-bridge": "^3 || ^7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Semver\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nils Adermann",
"email": "naderman@naderman.de",
"homepage": "http://www.naderman.de"
},
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
},
{
"name": "Rob Bast",
"email": "rob.bast@gmail.com",
"homepage": "http://robbast.nl"
}
],
"description": "Semver library that offers utilities, version constraint parsing and validation.",
"keywords": [
"semantic",
"semver",
"validation",
"versioning"
],
"support": {
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/semver/issues",
"source": "https://github.com/composer/semver/tree/3.4.3"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2024-09-19T14:15:21+00:00"
},
{ {
"name": "dflydev/dot-access-data", "name": "dflydev/dot-access-data",
"version": "v3.0.3", "version": "v3.0.3",
@ -510,6 +670,67 @@
], ],
"time": "2025-03-06T22:45:56+00:00" "time": "2025-03-06T22:45:56+00:00"
}, },
{
"name": "ezyang/htmlpurifier",
"version": "v4.18.0",
"source": {
"type": "git",
"url": "https://github.com/ezyang/htmlpurifier.git",
"reference": "cb56001e54359df7ae76dc522d08845dc741621b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/cb56001e54359df7ae76dc522d08845dc741621b",
"reference": "cb56001e54359df7ae76dc522d08845dc741621b",
"shasum": ""
},
"require": {
"php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
},
"require-dev": {
"cerdic/css-tidy": "^1.7 || ^2.0",
"simpletest/simpletest": "dev-master"
},
"suggest": {
"cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
"ext-bcmath": "Used for unit conversion and imagecrash protection",
"ext-iconv": "Converts text to and from non-UTF-8 encodings",
"ext-tidy": "Used for pretty-printing HTML"
},
"type": "library",
"autoload": {
"files": [
"library/HTMLPurifier.composer.php"
],
"psr-0": {
"HTMLPurifier": "library/"
},
"exclude-from-classmap": [
"/library/HTMLPurifier/Language/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-or-later"
],
"authors": [
{
"name": "Edward Z. Yang",
"email": "admin@htmlpurifier.org",
"homepage": "http://ezyang.com"
}
],
"description": "Standards compliant HTML filter written in PHP",
"homepage": "http://htmlpurifier.org/",
"keywords": [
"html"
],
"support": {
"issues": "https://github.com/ezyang/htmlpurifier/issues",
"source": "https://github.com/ezyang/htmlpurifier/tree/v4.18.0"
},
"time": "2024-11-01T03:51:45+00:00"
},
{ {
"name": "fruitcake/php-cors", "name": "fruitcake/php-cors",
"version": "v1.3.0", "version": "v1.3.0",
@ -2154,6 +2375,272 @@
}, },
"time": "2025-04-08T15:13:36+00:00" "time": "2025-04-08T15:13:36+00:00"
}, },
{
"name": "maatwebsite/excel",
"version": "3.1.64",
"source": {
"type": "git",
"url": "https://github.com/SpartnerNL/Laravel-Excel.git",
"reference": "e25d44a2d91da9179cd2d7fec952313548597a79"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/e25d44a2d91da9179cd2d7fec952313548597a79",
"reference": "e25d44a2d91da9179cd2d7fec952313548597a79",
"shasum": ""
},
"require": {
"composer/semver": "^3.3",
"ext-json": "*",
"illuminate/support": "5.8.*||^6.0||^7.0||^8.0||^9.0||^10.0||^11.0||^12.0",
"php": "^7.0||^8.0",
"phpoffice/phpspreadsheet": "^1.29.9",
"psr/simple-cache": "^1.0||^2.0||^3.0"
},
"require-dev": {
"laravel/scout": "^7.0||^8.0||^9.0||^10.0",
"orchestra/testbench": "^6.0||^7.0||^8.0||^9.0||^10.0",
"predis/predis": "^1.1"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Excel": "Maatwebsite\\Excel\\Facades\\Excel"
},
"providers": [
"Maatwebsite\\Excel\\ExcelServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Maatwebsite\\Excel\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Patrick Brouwers",
"email": "patrick@spartner.nl"
}
],
"description": "Supercharged Excel exports and imports in Laravel",
"keywords": [
"PHPExcel",
"batch",
"csv",
"excel",
"export",
"import",
"laravel",
"php",
"phpspreadsheet"
],
"support": {
"issues": "https://github.com/SpartnerNL/Laravel-Excel/issues",
"source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.64"
},
"funding": [
{
"url": "https://laravel-excel.com/commercial-support",
"type": "custom"
},
{
"url": "https://github.com/patrickbrouwers",
"type": "github"
}
],
"time": "2025-02-24T11:12:50+00:00"
},
{
"name": "maennchen/zipstream-php",
"version": "3.1.2",
"source": {
"type": "git",
"url": "https://github.com/maennchen/ZipStream-PHP.git",
"reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f",
"reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"ext-zlib": "*",
"php-64bit": "^8.2"
},
"require-dev": {
"brianium/paratest": "^7.7",
"ext-zip": "*",
"friendsofphp/php-cs-fixer": "^3.16",
"guzzlehttp/guzzle": "^7.5",
"mikey179/vfsstream": "^1.6",
"php-coveralls/php-coveralls": "^2.5",
"phpunit/phpunit": "^11.0",
"vimeo/psalm": "^6.0"
},
"suggest": {
"guzzlehttp/psr7": "^2.4",
"psr/http-message": "^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"ZipStream\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paul Duncan",
"email": "pabs@pablotron.org"
},
{
"name": "Jonatan Männchen",
"email": "jonatan@maennchen.ch"
},
{
"name": "Jesse Donat",
"email": "donatj@gmail.com"
},
{
"name": "András Kolesár",
"email": "kolesar@kolesar.hu"
}
],
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
"keywords": [
"stream",
"zip"
],
"support": {
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2"
},
"funding": [
{
"url": "https://github.com/maennchen",
"type": "github"
}
],
"time": "2025-01-27T12:07:53+00:00"
},
{
"name": "markbaker/complex",
"version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPComplex.git",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Complex\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@lange.demon.co.uk"
}
],
"description": "PHP Class for working with complex numbers",
"homepage": "https://github.com/MarkBaker/PHPComplex",
"keywords": [
"complex",
"mathematics"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPComplex/issues",
"source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
},
"time": "2022-12-06T16:21:08+00:00"
},
{
"name": "markbaker/matrix",
"version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPMatrix.git",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpdocumentor/phpdocumentor": "2.*",
"phploc/phploc": "^4.0",
"phpmd/phpmd": "2.*",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"sebastian/phpcpd": "^4.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Matrix\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@demon-angel.eu"
}
],
"description": "PHP Class for working with matrices",
"homepage": "https://github.com/MarkBaker/PHPMatrix",
"keywords": [
"mathematics",
"matrix",
"vector"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPMatrix/issues",
"source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
},
"time": "2022-12-02T22:17:43+00:00"
},
{ {
"name": "monolog/monolog", "name": "monolog/monolog",
"version": "3.9.0", "version": "3.9.0",
@ -2749,6 +3236,112 @@
], ],
"time": "2025-03-11T14:40:46+00:00" "time": "2025-03-11T14:40:46+00:00"
}, },
{
"name": "phpoffice/phpspreadsheet",
"version": "1.29.10",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "c80041b1628c4f18030407134fe88303661d4e4e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/c80041b1628c4f18030407134fe88303661d4e4e",
"reference": "c80041b1628c4f18030407134fe88303661d4e4e",
"shasum": ""
},
"require": {
"composer/pcre": "^1||^2||^3",
"ext-ctype": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-iconv": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-simplexml": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"ext-zip": "*",
"ext-zlib": "*",
"ezyang/htmlpurifier": "^4.15",
"maennchen/zipstream-php": "^2.1 || ^3.0",
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
"php": "^7.4 || ^8.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
"dompdf/dompdf": "^1.0 || ^2.0 || ^3.0",
"friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.3",
"mpdf/mpdf": "^8.1.1",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^1.1",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^8.5 || ^9.0",
"squizlabs/php_codesniffer": "^3.7",
"tecnickcom/tcpdf": "^6.5"
},
"suggest": {
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
"ext-intl": "PHP Internationalization Functions",
"mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
},
"type": "library",
"autoload": {
"psr-4": {
"PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Maarten Balliauw",
"homepage": "https://blog.maartenballiauw.be"
},
{
"name": "Mark Baker",
"homepage": "https://markbakeruk.net"
},
{
"name": "Franck Lefevre",
"homepage": "https://rootslabs.net"
},
{
"name": "Erik Tilt"
},
{
"name": "Adrien Crivelli"
}
],
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
"homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
"keywords": [
"OpenXML",
"excel",
"gnumeric",
"ods",
"php",
"spreadsheet",
"xls",
"xlsx"
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.10"
},
"time": "2025-02-08T02:56:14+00:00"
},
{ {
"name": "phpoption/phpoption", "name": "phpoption/phpoption",
"version": "1.9.3", "version": "1.9.3",
@ -3605,6 +4198,97 @@
], ],
"time": "2024-04-27T21:32:50+00:00" "time": "2024-04-27T21:32:50+00:00"
}, },
{
"name": "spatie/laravel-activitylog",
"version": "4.10.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-activitylog.git",
"reference": "466f30f7245fe3a6e328ad5e6812bd43b4bddea5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/466f30f7245fe3a6e328ad5e6812bd43b4bddea5",
"reference": "466f30f7245fe3a6e328ad5e6812bd43b4bddea5",
"shasum": ""
},
"require": {
"illuminate/config": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0",
"illuminate/database": "^8.69 || ^9.27 || ^10.0 || ^11.0 || ^12.0",
"illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0",
"php": "^8.1",
"spatie/laravel-package-tools": "^1.6.3"
},
"require-dev": {
"ext-json": "*",
"orchestra/testbench": "^6.23 || ^7.0 || ^8.0 || ^9.0 || ^10.0",
"pestphp/pest": "^1.20 || ^2.0 || ^3.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Spatie\\Activitylog\\ActivitylogServiceProvider"
]
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"Spatie\\Activitylog\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
},
{
"name": "Sebastian De Deyne",
"email": "sebastian@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
},
{
"name": "Tom Witkowski",
"email": "dev.gummibeer@gmail.com",
"homepage": "https://gummibeer.de",
"role": "Developer"
}
],
"description": "A very simple activity logger to monitor the users of your website or application",
"homepage": "https://github.com/spatie/activitylog",
"keywords": [
"activity",
"laravel",
"log",
"spatie",
"user"
],
"support": {
"issues": "https://github.com/spatie/laravel-activitylog/issues",
"source": "https://github.com/spatie/laravel-activitylog/tree/4.10.1"
},
"funding": [
{
"url": "https://spatie.be/open-source/support-us",
"type": "custom"
},
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2025-02-10T15:38:25+00:00"
},
{ {
"name": "spatie/laravel-package-tools", "name": "spatie/laravel-package-tools",
"version": "1.92.4", "version": "1.92.4",

52
config/activitylog.php Normal file
View File

@ -0,0 +1,52 @@
<?php
return [
/*
* If set to false, no activities will be saved to the database.
*/
'enabled' => 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'),
];

View File

@ -65,7 +65,7 @@ return [
| |
*/ */
'timezone' => 'UTC', 'timezone' => env('APP_TIMEZONE', 'UTC'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

380
config/excel.php Normal file
View File

@ -0,0 +1,380 @@
<?php
use Maatwebsite\Excel\Excel;
use PhpOffice\PhpSpreadsheet\Reader\Csv;
return [
'exports' => [
/*
|--------------------------------------------------------------------------
| 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,
],
];

View File

@ -0,0 +1,27 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateActivityLogTable extends Migration
{
public function up()
{
Schema::connection(config('activitylog.database_connection'))->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'));
}
}

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddEventColumnToActivityLogTable extends Migration
{
public function up()
{
Schema::connection(config('activitylog.database_connection'))->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');
});
}
}

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddBatchUuidColumnToActivityLogTable extends Migration
{
public function up()
{
Schema::connection(config('activitylog.database_connection'))->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');
});
}
}

View File

@ -2,13 +2,17 @@
return [ return [
'list' => '角色列表', 'list' => '角色列表',
'CreateNewRole' => '新增角色', 'CreateNew' => '新增角色',
'EditRole' => '編輯角色',
'edit' => '編輯', 'edit' => '編輯',
'delete' => '刪除', 'delete' => '刪除',
'no' => '編號', 'no' => '編號',
'name' => '名稱', 'name' => '名稱',
'permissions' => '權限', 'permissions' => '權限',
'role_name' =>'角色名稱',
'select_permissions'=>'選擇權限',
'create' => '新增', 'create' => '新增',
'action' => '操作', 'action' => '操作',

View File

@ -2,7 +2,9 @@
return [ return [
'list' => '使用者列表', 'list' => '使用者列表',
'CreateNewRole' => '新增使用者', 'CreateNew' => '新增使用者',
'EditUser' => '編輯使用者',
'ImportData' => '滙入使用者',
'edit' => '編輯', 'edit' => '編輯',
'delete' => '刪除', 'delete' => '刪除',
@ -13,6 +15,10 @@ return [
'birthday' => '生日', 'birthday' => '生日',
'status' => '狀態', 'status' => '狀態',
'role' =>'角色', 'role' =>'角色',
'select_gender'=>'選擇性別',
'select_status'=>'選擇狀態',
'select_role'=>'選擇角色',
'create' => '新增', 'create' => '新增',
'action' => '操作', 'action' => '操作',

View File

@ -0,0 +1,16 @@
@props(['selected','fieldName', 'modelId'])
<div>
<select wire:change="categoryChanged($event.target.value,'{{ $fieldName}}', {{ $modelId }})">
@foreach ($options as $id => $name)
<option
value="{{ $id }}"
@if ($id == $selected)
selected="selected"
@endif
>
{{ $name }}
</option>
@endforeach
</select>
</div>

View File

@ -0,0 +1,3 @@
<x-layouts.admin>
<livewire:admin.activity-log-table />
</x-layouts.admin>

View File

@ -1,25 +1,21 @@
<div> <x-wireui:modal-card title="{{ $roleId ? __('roles.EditRole') : __('roles.CreateNew') }}" blur wire:model.defer="showCreateModal">
@if ($showCreateModal) <div class="space-y-4">
<x-wireui:modal-card title="{{ $roleId ? '編輯角色' : '新增角色' }}" blur wire:model.defer="showCreateModal"> <x-wireui:input label="{{__('roles.role_name')}}" wire:model.defer="name" />
<div class="space-y-4"> <x-wireui:select
<x-wireui:input label="角色名稱" wire:model.defer="name" /> label="{{__('roles.permissions')}}"
<x-wireui:select wire:model.defer="selectedPermissions"
label="權限" placeholder="{{__('roles.select_permissions')}}"
wire:model.defer="selectedPermissions" multiselect
placeholder="選擇權限" option-label="label"
multiselect option-value="value"
option-label="label" :options="$permissions->map(fn($p) => ['value' => $p->id, 'label' => $p->name])->toArray()"
option-value="value" />
:options="$permissions->map(fn($p) => ['value' => $p->id, 'label' => $p->name])->toArray()" </div>
/>
</div>
<x-slot name="footer"> <x-slot name="footer">
<div class="flex justify-center gap-2"> <div class="flex justify-between w-full">
<x-wireui:button primary label="{{__('roles.cancel')}}" x-on:click="$dispatch('close')" /> <x-wireui:button flat label="{{__('roles.cancel')}}" @click="$wire.showCreateModal = false" />
<x-wireui:button primary label="{{__('roles.submit')}}" wire:click="save" /> <x-wireui:button primary label="{{__('roles.submit')}}" wire:click="save" />
</div> </div>
</x-slot> </x-slot>
</x-wireui:modal-card> </x-wireui:modal-card>
@endif
</div>

View File

@ -0,0 +1,8 @@
<div class="flex justify-end mb-2">
<x-wireui:button
wire:click="$dispatchTo('admin.role-form', 'openCreateRoleModal')"
icon="plus"
label="{{ __('roles.CreateNew') }}"
class="bg-blue-600 text-white"
/>
</div>

View File

@ -1,21 +1,5 @@
<x-layouts.admin> <x-layouts.admin>
<x-slot name="header"> <x-wireui:notifications/>
角色管理
</x-slot>
@if (session()->has('message'))
<x-wireui:notifications />
<script>
window.$wireui.notify({
title: '提示',
description: '{{ session('message') }}',
icon: 'success'
});
</script>
@endif
{{-- 單一 Livewire 元件,內含資料表與 Modal --}}
<livewire:admin.role-table /> <livewire:admin.role-table />
<livewire:admin.role-form /> <livewire:admin.role-form />
</x-layouts.admin> </x-layouts.admin>

View File

@ -1,29 +1,29 @@
<x-wireui:modal-card title="{{ $userId ? '編輯使用者' : '新增使用者' }}" blur wire:model.defer="showCreateModal"> <x-wireui:modal-card title="{{ $userId ? __('users.EditUser') : __('users.CreateNew') }}" blur wire:model.defer="showModal">
<div class="space-y-4"> <div class="space-y-4">
<x-wireui:input label="名稱" wire:model.defer="fields.name" required /> <x-wireui:input label="{{__('users.name')}}" wire:model.defer="fields.name" required />
<x-wireui:input label="Email" wire:model.defer="fields.email" required /> <x-wireui:input label="Email" wire:model.defer="fields.email" required />
<x-wireui:input label="Phone" wire:model.defer="fields.phone" /> <x-wireui:input label="Phone" wire:model.defer="fields.phone" />
<x-wireui:select <x-wireui:select
label="性別" label="{{__('users.gender')}}"
wire:model.defer="fields.gender" wire:model.defer="fields.gender"
placeholder="選擇性別" placeholder="{{__('users.select_gender')}}"
:options="$genderOptions" :options="$genderOptions"
option-label="name" option-label="name"
option-value="value" option-value="value"
/> />
<x-wireui:select <x-wireui:select
label="狀態" label="{{__('users.status')}}"
wire:model.defer="fields.status" wire:model.defer="fields.status"
placeholder="選擇狀態" placeholder="{{__('users.select_status')}}"
:options="$statusOptions" :options="$statusOptions"
option-label="name" option-label="name"
option-value="value" option-value="value"
/> />
<x-wireui:select <x-wireui:select
label="角色" label="{{__('users.role')}}"
wire:model.defer="selectedRoles" wire:model.defer="selectedRoles"
placeholder="選擇角色" placeholder="{{__('users.select_role')}}"
multiselect multiselect
option-label="label" option-label="label"
option-value="value" option-value="value"
@ -32,8 +32,8 @@
</div> </div>
<x-slot name="footer"> <x-slot name="footer">
<div class="flex justify-center gap-2"> <div class="flex justify-between w-full">
<x-wireui:button primary label="{{__('users.cancel')}}" x-on:click="$dispatch('close')" /> <x-wireui:button flat label="{{__('users.cancel')}}" wire:click="closeModal" />
<x-wireui:button primary label="{{__('users.submit')}}" wire:click="save" /> <x-wireui:button primary label="{{__('users.submit')}}" wire:click="save" />
</div> </div>
</x-slot> </x-slot>

View File

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

View File

@ -0,0 +1,63 @@
<x-wireui:modal-card title="{{ __('users.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="{{ __('users.cancel') }}" wire:click="closeModal" />
<x-wireui:button primary label="{{ __('users.submit') }}" wire:click="import" />
</div>
</x-slot>
</x-wireui:modal-card>

View File

@ -1,21 +1,6 @@
<x-layouts.admin> <x-layouts.admin>
<x-slot name="header"> <x-wireui:notifications/>
使用者管理
</x-slot>
@if (session()->has('message'))
<x-wireui:notifications />
<script>
window.$wireui.notify({
title: '提示',
description: '{{ session('message') }}',
icon: 'success'
});
</script>
@endif
{{-- 單一 Livewire 元件,內含資料表與 Modal --}}
<livewire:admin.user-table /> <livewire:admin.user-table />
<livewire:admin.user-form /> <livewire:admin.user-form />
<livewire:admin.user-import-data />
</x-layouts.admin> </x-layouts.admin>

View File

@ -3,6 +3,7 @@
$menus = [ $menus = [
['label' => 'Dashboard', 'route' => 'admin.dashboard', 'icon' => 'home', 'permission' => null], ['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' => 'Role', 'route' => 'admin.roles', 'icon' => 'user-circle', 'permission' => 'role-list'],
['label' => 'User', 'route' => 'admin.users', 'icon' => 'user-circle', 'permission' => 'user-list'], ['label' => 'User', 'route' => 'admin.users', 'icon' => 'user-circle', 'permission' => 'user-list'],
]; ];

View File

@ -20,15 +20,8 @@ require __DIR__.'/auth.php';
Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () { Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () {
Route::get('/dashboard', AdminDashboard::class)->name('dashboard'); Route::get('/dashboard', AdminDashboard::class)->name('dashboard');
Route::get('/activity-log', function () {return view('livewire.admin.activity-log');})->name('activity-log');
Route::get('/roles', function () { Route::get('/roles', function () {return view('livewire.admin.roles');})->name('roles');
return view('livewire.admin.roles'); Route::get('/users', function () {return view('livewire.admin.users');})->name('users');
})->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');
}); });

View File

@ -60,13 +60,21 @@ composer require wireui/wireui
php artisan vendor:publish --tag="wireui.config" php artisan vendor:publish --tag="wireui.config"
php artisan make:livewire Admin/Roles/Index 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:livewire Admin/Users
php artisan make:component Table 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 composer require power-components/livewire-powergrid
php artisan vendor:publish --tag=livewire-powergrid-config php artisan vendor:publish --tag=livewire-powergrid-config